diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/catalog.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/catalog.ts index 76b32f502be9..675826ccca63 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/catalog.ts +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/catalog.ts @@ -55,7 +55,7 @@ export const mockedCatalogPlanMonth: order.publicOrder.Plan = { }, ], invoiceName: 'invoiceName1', - planCode: 'ai-notebook.flavorCPUId.minute.consumption', + planCode: 'ai-notebook.flavorBisCPUId.minute.consumption', pricingType: order.cart.GenericProductPricingTypeEnum.consumption, pricings: [mockedPricing], product: 'product', @@ -75,7 +75,7 @@ export const mockedCatalogStorageMonth: order.publicOrder.Plan = { }, ], invoiceName: 'invoiceName2', - planCode: 'ai-notebook.flavorCPUId.minute.consumption', + planCode: 'ai-notebook.flavorGPUId.minute.consumption', pricingType: order.cart.GenericProductPricingTypeEnum.consumption, pricings: [mockedPricing], product: 'product', @@ -95,7 +95,7 @@ export const mockedCatalogStorageHour: order.publicOrder.Plan = { }, ], invoiceName: 'invoiceName3', - planCode: 'ai-notebook.flavorCPUId.minute.consumption', + planCode: 'ai-notebook.flavorOtherCpuId.minute.consumption', pricingType: order.cart.GenericProductPricingTypeEnum.consumption, pricings: [mockedPricing], product: 'product', diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/editor.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/editor.ts index 4fc28f0f6e21..c7c191ad54df 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/editor.ts +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/editor.ts @@ -3,7 +3,7 @@ import * as ai from '@/types/cloud/project/ai'; export const mockedEditor: ai.capabilities.notebook.Editor = { description: 'description', docUrl: 'docURl', - id: 'editorId', + id: 'jupyterlab', logoUrl: 'logo', name: 'EditorName', versions: ['version'], diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/framework.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/framework.ts index bb0ed6111084..089dba6c6a81 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/framework.ts +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/notebook/framework.ts @@ -3,7 +3,7 @@ import * as ai from '@/types/cloud/project/ai'; export const mockedFramework: ai.notebook.Framework = { description: 'description', docUrl: 'docURl', - id: 'frameworkId', + id: 'one-for-all', logoUrl: 'logo', name: 'FrameworkName', versions: ['version'], diff --git a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/suggestion.ts b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/suggestion.ts index f71f897bd437..107085cd1f78 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/suggestion.ts +++ b/packages/manager/apps/pci-ai-notebooks/src/__tests__/helpers/mocks/suggestion.ts @@ -5,7 +5,7 @@ export const mockedSuggestion: Suggestions[] = [ region: 'GRA', ressources: { nb: 1, - flavor: 'l4-1-gpu', + flavor: 'flavorCPUId', }, framework: { id: 'one-for-all', diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DataTable.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DataTable.tsx new file mode 100644 index 000000000000..7d6e61660b93 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DataTable.tsx @@ -0,0 +1,123 @@ +import React, { ReactElement, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { flexRender } from '@tanstack/react-table'; +import { ChevronDown, ChevronUp } from 'lucide-react'; +import { useDataTableContext } from './DataTableContext'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { Button } from '../ui/button'; + +export const MENU_COLUMN_ID = 'actions'; + +interface DatatableProps { + renderRowExpansion?: (row: TData) => ReactElement | null; +} + +export function DataTable({ + renderRowExpansion, +}: DatatableProps) { + const { table, rows } = useDataTableContext(); + const { t } = useTranslation('pci-databases-analytics/components/data-table'); + const [expandedRows, setExpandedRows] = useState>({}); + + const toggleRowExpansion = (rowId: string) => { + setExpandedRows((prev) => ({ + ...prev, + [rowId]: !prev[rowId], + })); + }; + + const headerGroups = table.getHeaderGroups(); + return ( + + + {headerGroups.map((headerGroup) => ( + + {renderRowExpansion && ( + + )} + {headerGroup.headers.map((header, index) => { + const isEmptyHeader = header.id === MENU_COLUMN_ID; + // Get a reference to the previous header + const isEmptyNextHeader = + headerGroup.headers[index + 1]?.id === MENU_COLUMN_ID; + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {rows?.length ? ( + rows.map((row) => ( + + + {renderRowExpansion && ( + + + + )} + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + {expandedRows[row.id] && renderRowExpansion && ( + + + {renderRowExpansion(row.original as TData)} + + + )} + + )) + ) : ( + + + {t('noResult')} + + + )} + +
+ ); +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DataTableContext.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DataTableContext.tsx new file mode 100644 index 000000000000..dfb496a90459 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DataTableContext.tsx @@ -0,0 +1,122 @@ +import { + ColumnDef, + Row, + SortingState, + Table, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { ReactNode, createContext, useContext, useMemo, useState } from 'react'; +import { useColumnFilters } from './useColumnFilters.hook'; +import { applyFilters } from '@/lib/filters'; +import { ColumnFilter } from './DatatableDefaultFilterButton'; +import { DataTable } from './DataTable'; +import { DataTablePagination } from './DatatablePagination'; + +interface DataTableProviderProps { + columns: ColumnDef[]; + data: TData[]; + pageSize?: number; + itemNumber?: number; + filtersDefinition?: ColumnFilter[]; + children?: ReactNode; +} + +interface DataTableContextValue { + table: Table; + filtersDefinition?: ColumnFilter[]; + columnFilters: ReturnType; + globalFilter: string; + data: TData[]; + filteredData: TData[]; + sorting: SortingState; + rows: Row[]; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const DataTableContext = createContext | null>(null); + +export function DataTableProvider({ + columns, + data, + pageSize, + filtersDefinition, + children, +}: DataTableProviderProps) { + const [sorting, setSorting] = useState([ + { + id: columns[0]?.id as string, + desc: false, + }, + ]); + const [globalFilter, setGlobalFilter] = useState(''); + const columnFilters = useColumnFilters(); + + const filteredData = useMemo( + () => applyFilters(data || [], columnFilters.filters) as TData[], + [columnFilters.filters, data], + ); + const table = useReactTable({ + data: filteredData, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + state: { + sorting, + globalFilter, + }, + initialState: { + pagination: { pageSize: pageSize ?? 5 }, + }, + onGlobalFilterChange: (e) => { + setGlobalFilter(e); + }, + globalFilterFn: 'auto', + }); + + const rows = useMemo(() => table.getRowModel()?.rows, [ + table, + globalFilter, + columnFilters.filters, + data, + sorting, + ]); + + const contextValue: DataTableContextValue = { + table, + filtersDefinition, + columnFilters, + globalFilter, + data, + filteredData, + sorting, + rows, + }; + + return ( + + {children || ( + <> + + + + )} + + ); +} + +export function useDataTableContext() { + const context = useContext>(DataTableContext); + if (!context) { + throw new Error( + 'useDataTableContext must be used within a DataTableProvider', + ); + } + return context as DataTableContextValue; +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableAction.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableAction.tsx new file mode 100644 index 000000000000..26ca71567ba9 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableAction.tsx @@ -0,0 +1,7 @@ +import { ReactNode } from 'react'; + +const DatatableAction = ({ children }: { children: ReactNode }) => { + return <>{children || <>}; +}; + +export default DatatableAction; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableDefaultFilterButton.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableDefaultFilterButton.tsx new file mode 100644 index 000000000000..59af07312137 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableDefaultFilterButton.tsx @@ -0,0 +1,227 @@ +import { useTranslation } from 'react-i18next'; +import { ReactNode, useEffect, useMemo, useState } from 'react'; +import { CalendarIcon, FilterIcon } from 'lucide-react'; +import { Input } from '../ui/input'; +import { Label } from '../ui/label'; +import { Button } from '../ui/button'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '../ui/select'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import './translations'; +import { Calendar } from '../ui/calendar'; +import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; +import FormattedDate from '../formatted-date/FormattedDate.component'; +import { useDateFnsLocale } from '@/hooks/useDateFnsLocale.hook'; +import { Filter, FilterCategories, FilterComparator } from '@/lib/filters'; + +export type ColumnFilter = { + id: string; + label: string; + comparators: FilterComparator[]; + options?: { label: string | ReactNode; value: string }[]; +}; + +export type DataTableDefaultFilterButtonProps = { + columns: ColumnFilter[]; + onAddFilter: (filter: Filter, column: ColumnFilter) => void; +}; +const DataTableDefaultFilterButton = ({ + columns, + onAddFilter, +}: DataTableDefaultFilterButtonProps) => { + const { t } = useTranslation('filters'); + const dateLocale = useDateFnsLocale(); + const [filtersOpen, setFiltersOpen] = useState(false); + + const [selectedId, setSelectedId] = useState(columns?.[0]?.id || ''); + const [selectedComparator, setSelectedComparator] = useState( + columns?.[0]?.comparators?.[0] || FilterComparator.IsEqual, + ); + const [value, setValue] = useState(''); + const [dateValue, setDateValue] = useState(new Date()); + + const selectedColumn = useMemo( + () => columns.find(({ id }) => selectedId === id), + [columns, selectedId], + ); + + useEffect(() => { + if (!selectedColumn.comparators.includes(selectedComparator)) { + setSelectedComparator(selectedColumn.comparators[0]); + } + }, [selectedColumn]); + + const isInputDate = selectedColumn?.comparators === FilterCategories.Date; + const isInputNumeric = + selectedColumn?.comparators === FilterCategories.Numeric; + const isInputSelect = + selectedColumn?.comparators === FilterCategories.Options && + selectedColumn?.options?.length > 0; + const isInputString = + selectedColumn?.comparators === FilterCategories.String && !isInputSelect; + + const submitAddFilter = () => { + onAddFilter( + { + key: selectedId, + comparator: selectedComparator, + value: isInputDate ? dateValue.toString() : value, + }, + selectedColumn, + ); + setValue(''); + setDateValue(new Date()); + setFiltersOpen(false); + setSelectedId(columns[0].id); + }; + + return ( + + + + + +
+ + +
+
+ + +
+
+ + {isInputDate && ( + + + + + + setDateValue(day)} + locale={dateLocale} + initialFocus + /> + + + )} + {isInputSelect && ( + + )} + {isInputString && ( + setValue(`${e.target.value}`)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + submitAddFilter(); + } + }} + /> + )} + {isInputNumeric && ( + setValue(`${e.target.value}`)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + submitAddFilter(); + } + }} + /> + )} +
+
+ +
+
+
+ ); +}; + +export default DataTableDefaultFilterButton; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableFiltersButton.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableFiltersButton.tsx new file mode 100644 index 000000000000..95360f370af8 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableFiltersButton.tsx @@ -0,0 +1,25 @@ +import { ReactNode } from 'react'; +import { useDataTableContext } from './DataTableContext'; +import DataTableDefaultFilterButton from './DatatableDefaultFilterButton'; + +const DatatableFiltersButton = ({ children }: { children?: ReactNode }) => { + const { filtersDefinition, columnFilters } = useDataTableContext(); + if (!filtersDefinition?.length) return <>; + return ( + <> + {children || ( + { + columnFilters.addFilter({ + ...addedFilter, + label: column.label, + }); + }} + /> + )} + + ); +}; + +export default DatatableFiltersButton; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableFiltersList.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableFiltersList.tsx new file mode 100644 index 000000000000..da21810c9c7c --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableFiltersList.tsx @@ -0,0 +1,90 @@ +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { X } from 'lucide-react'; +import { Badge } from '../ui/badge'; +import { Button } from '../ui/button'; +import './translations'; +import { useLocale } from '@/hooks/useLocale'; +import { useDataTableContext } from './DataTableContext'; + +export declare enum FilterComparator { + Includes = 'includes', + StartsWith = 'starts_with', + EndsWith = 'ends_with', + IsEqual = 'is_equal', + IsDifferent = 'is_different', + IsLower = 'is_lower', + IsHigher = 'is_higher', + IsBefore = 'is_before', + IsAfter = 'is_after', + IsIn = 'is_in', +} + +export type Filter = { + key: string; + value: string | string[]; + comparator: FilterComparator; +}; + +export type FilterWithLabel = Filter & { label: string }; + +export type FilterListProps = { + filters: FilterWithLabel[]; + onRemoveFilter: (filter: FilterWithLabel) => void; +}; + +export function DatatableFiltersList() { + const { t } = useTranslation('filters'); + const { columnFilters } = useDataTableContext(); + const locale = useLocale(); + const formater = useMemo( + () => new Intl.DateTimeFormat(locale.replace('_', '-')), + [locale], + ); + const tComp = (comparator: string) => + t(`common_criteria_adder_operator_${comparator}`); + + const isValidDate = (value: string): boolean => { + // Reject purely numerical strings + if (!Number.isNaN(Number(value))) { + return false; + } + const date = new Date(value); + return !Number.isNaN(date.getTime()); + }; + const getFilterContent = (filter: FilterWithLabel) => { + const label = `${ + filter.label ? `${filter.label} ${tComp(filter.comparator)} ` : '' + }`; + let formattedValue; + if (Array.isArray(filter.value)) { + // Handle array values + formattedValue = filter.value.join(', '); + } else if (typeof filter.value === 'string' && isValidDate(filter.value)) { + // Format valid date strings + formattedValue = formater.format(new Date(filter.value)); + } else { + // Fallback for other types of strings or invalid dates + formattedValue = filter.value; + } + return `${label}${formattedValue}`; + }; + + if (!columnFilters?.filters.length) return <>; + return ( +
+ {columnFilters.filters?.map((filter, key) => ( + + {getFilterContent(filter)} + + + ))} +
+ ); +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableHeader.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableHeader.tsx new file mode 100644 index 000000000000..61882c389ca1 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableHeader.tsx @@ -0,0 +1,32 @@ +import { Children, ReactElement, ReactNode } from 'react'; +import DataTable from './index'; + +export function DatatableHeader({ children }: { children: ReactNode }) { + // Helper function to check if a child is a ReactElement + const isReactElement = (child: ReactNode): child is ReactElement => + !!child && typeof child === 'object' && 'type' in child; + + // Categorize children into `actionButton`, `searchbar`, and `filters` + const actionButton = Children.toArray(children).find( + (child): child is ReactElement => + isReactElement(child) && child.type === DataTable.Action, + ); + const searchbar = Children.toArray(children).find( + (child): child is ReactElement => + isReactElement(child) && child.type === DataTable.SearchBar, + ); + const filters = Children.toArray(children).find( + (child): child is ReactElement => + isReactElement(child) && child.type === DataTable.FiltersButton, + ); + + return ( +
+ {actionButton} +
+ {searchbar} + {filters} +
+
+ ); +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatablePagination.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatablePagination.tsx new file mode 100644 index 000000000000..2da6327a3a8e --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatablePagination.tsx @@ -0,0 +1,99 @@ +import { + ChevronLeft, + ChevronRight, + ChevronFirst, + ChevronLast, +} from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { Button } from '@/components/ui/button'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '../ui/select'; +import { useDataTableContext } from './DataTableContext'; + +export function DataTablePagination() { + const { t } = useTranslation('pci-databases-analytics/components/data-table'); + const { table } = useDataTableContext(); + const itemCount = table.getRowCount(); + if (itemCount === 0) return <>; + return ( +
+
+
+ {itemCount > 0 && ( +
+ {t('itemCount', { count: itemCount })} +
+ )} + +
+
+ + {t('currentPage', { + currentPage: table.getState().pagination.pageIndex + 1, + totalPagesCount: table.getPageCount(), + })} + +
+
+ + + + +
+
+
+ ); +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableSearchBar.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableSearchBar.tsx new file mode 100644 index 000000000000..5a441087af72 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableSearchBar.tsx @@ -0,0 +1,31 @@ +import { ReactNode } from 'react'; +import { Search } from 'lucide-react'; +import { Input } from '../ui/input'; +import { Button } from '../ui/button'; +import { useDataTableContext } from './DataTableContext'; + +const DatatableSearchBar = ({ children }: { children?: ReactNode }) => { + const { table, globalFilter } = useDataTableContext(); + return ( + <> + {children || ( +
+ table.setGlobalFilter(String(e.target.value))} + placeholder="Search..." + className="max-w-full sm:max-w-72 rounded-r-none focus-visible:ring-transparent focus-visible:bg-primary-50" + /> + +
+ )} + + ); +}; + +export default DatatableSearchBar; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableSkeleton.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableSkeleton.tsx new file mode 100644 index 000000000000..89a52b1cf2e8 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableSkeleton.tsx @@ -0,0 +1,56 @@ +import { Skeleton } from '../ui/skeleton'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; + +interface DataTableSkeletonProps { + rows?: number; + columns?: number; + height?: number; + width?: number; +} +export function DatatableSkeleton({ + height = 16, + width = 80, + rows = 5, + columns = 5, +}: DataTableSkeletonProps) { + return ( + + + + {Array.from({ length: columns }).map((colHead, iColHead) => ( + + + + ))} + + + + {Array.from({ length: rows }).map((row, iRow) => ( + + {Array.from({ length: columns }).map((col, iCol) => ( + + + + ))} + + ))} + +
+ ); +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableSortableHeader.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableSortableHeader.tsx new file mode 100644 index 000000000000..92ecfc22e7b9 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/DatatableSortableHeader.tsx @@ -0,0 +1,36 @@ +import { ChevronDown, ChevronUp, ChevronsUpDown } from 'lucide-react'; +import { SortingColumn } from '@tanstack/react-table'; +import { Button } from '@/components/ui/button'; + +interface SortableHeaderProps { + column: SortingColumn; + children: React.ReactNode; +} +export function DatatableSortableHeader({ + column, + children, +}: SortableHeaderProps) { + const sort = column.getIsSorted(); + let icon = ; + if (sort === 'asc') { + icon = ; + } else if (sort === 'desc') { + icon = ; + } + + const buttonClass = `px-0 font-semibold ${ + sort + ? 'text-primary-500 hover:text-primary-500' + : 'text-primary-800 hover:text-primary-800' + }`; + return ( + + ); +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/index.ts b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/index.ts new file mode 100644 index 000000000000..ac65ed29fb23 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/index.ts @@ -0,0 +1,25 @@ +import { DataTable as Table } from './DataTable'; +import { DataTableProvider } from './DataTableContext'; +import DatatableAction from './DatatableAction'; +import DatatableFiltersButton from './DatatableFiltersButton'; +import { DatatableFiltersList } from './DatatableFiltersList'; +import { DatatableHeader } from './DatatableHeader'; +import { DataTablePagination } from './DatatablePagination'; +import DatatableSearchBar from './DatatableSearchBar'; +import { DatatableSkeleton } from './DatatableSkeleton'; +import { DatatableSortableHeader } from './DatatableSortableHeader'; + +const DataTable = { + Provider: DataTableProvider, + Header: DatatableHeader, + Action: DatatableAction, + SearchBar: DatatableSearchBar, + FiltersButton: DatatableFiltersButton, + FiltersList: DatatableFiltersList, + Table, + Pagination: DataTablePagination, + Skeleton: DatatableSkeleton, + SortableHeader: DatatableSortableHeader, +}; + +export default DataTable; diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_de_DE.json b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_de_DE.json new file mode 100644 index 000000000000..ca298dabe59a --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_de_DE.json @@ -0,0 +1,18 @@ +{ + "common_criteria_adder_filter_label": "Filtern", + "common_criteria_adder_column_label": "Spalte", + "common_criteria_adder_operator_label": "Bedingung", + "common_criteria_adder_operator_includes": "enthält", + "common_criteria_adder_operator_starts_with": "Beginnt mit", + "common_criteria_adder_operator_ends_with": "Endet mit", + "common_criteria_adder_operator_is_equal": "ist gleich", + "common_criteria_adder_operator_is_different": "ist nicht gleich", + "common_criteria_adder_operator_is_lower": "ist kleiner als", + "common_criteria_adder_operator_is_higher": "ist größer als", + "common_criteria_adder_operator_is_before": "ist davor", + "common_criteria_adder_operator_is_after": "Ist nach", + "common_criteria_adder_true_label": "Ja", + "common_criteria_adder_false_label": "Nein", + "common_criteria_adder_value_label": "Wert", + "common_criteria_adder_submit_label": "Hinzufügen" +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_en_GB.json b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_en_GB.json new file mode 100644 index 000000000000..185ec0041733 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_en_GB.json @@ -0,0 +1,18 @@ +{ + "common_criteria_adder_filter_label": "Sort", + "common_criteria_adder_column_label": "Column", + "common_criteria_adder_operator_label": "Condition", + "common_criteria_adder_operator_includes": "contains", + "common_criteria_adder_operator_starts_with": "starts with", + "common_criteria_adder_operator_ends_with": "ends with", + "common_criteria_adder_operator_is_equal": "equals", + "common_criteria_adder_operator_is_different": "is not equal to", + "common_criteria_adder_operator_is_lower": "is less than", + "common_criteria_adder_operator_is_higher": "is greater than", + "common_criteria_adder_operator_is_before": "is before", + "common_criteria_adder_operator_is_after": "is after", + "common_criteria_adder_true_label": "Yes", + "common_criteria_adder_false_label": "No", + "common_criteria_adder_value_label": "Value", + "common_criteria_adder_submit_label": "Add" +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_es_ES.json b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_es_ES.json new file mode 100644 index 000000000000..c2ae6a5bf8a2 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_es_ES.json @@ -0,0 +1,18 @@ +{ + "common_criteria_adder_filter_label": "Filtrar", + "common_criteria_adder_column_label": "Columna", + "common_criteria_adder_operator_label": "Condición", + "common_criteria_adder_operator_includes": "contiene", + "common_criteria_adder_operator_starts_with": "empieza por", + "common_criteria_adder_operator_ends_with": "terminado por", + "common_criteria_adder_operator_is_equal": "es igual a", + "common_criteria_adder_operator_is_different": "es diferente de", + "common_criteria_adder_operator_is_lower": "es inferior a", + "common_criteria_adder_operator_is_higher": "es superior a", + "common_criteria_adder_operator_is_before": "es anterior", + "common_criteria_adder_operator_is_after": "es posterior a", + "common_criteria_adder_true_label": "Sí", + "common_criteria_adder_false_label": "No", + "common_criteria_adder_value_label": "Valor", + "common_criteria_adder_submit_label": "Añadir" +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_fr_CA.json b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_fr_CA.json new file mode 100644 index 000000000000..b7a0bf321471 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_fr_CA.json @@ -0,0 +1,18 @@ +{ + "common_criteria_adder_filter_label": "Filtrer", + "common_criteria_adder_column_label": "Colonne", + "common_criteria_adder_operator_label": "Condition", + "common_criteria_adder_operator_includes": "contient", + "common_criteria_adder_operator_starts_with": "débute par", + "common_criteria_adder_operator_ends_with": "termine par", + "common_criteria_adder_operator_is_equal": "est égal à", + "common_criteria_adder_operator_is_different": "est différent de", + "common_criteria_adder_operator_is_lower": "est inférieur à", + "common_criteria_adder_operator_is_higher": "est supérieur à", + "common_criteria_adder_operator_is_before": "est avant", + "common_criteria_adder_operator_is_after": "est après", + "common_criteria_adder_true_label": "Oui", + "common_criteria_adder_false_label": "Non", + "common_criteria_adder_value_label": "Valeur", + "common_criteria_adder_submit_label": "Ajouter" +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_fr_FR.json b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_fr_FR.json new file mode 100644 index 000000000000..b7a0bf321471 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_fr_FR.json @@ -0,0 +1,18 @@ +{ + "common_criteria_adder_filter_label": "Filtrer", + "common_criteria_adder_column_label": "Colonne", + "common_criteria_adder_operator_label": "Condition", + "common_criteria_adder_operator_includes": "contient", + "common_criteria_adder_operator_starts_with": "débute par", + "common_criteria_adder_operator_ends_with": "termine par", + "common_criteria_adder_operator_is_equal": "est égal à", + "common_criteria_adder_operator_is_different": "est différent de", + "common_criteria_adder_operator_is_lower": "est inférieur à", + "common_criteria_adder_operator_is_higher": "est supérieur à", + "common_criteria_adder_operator_is_before": "est avant", + "common_criteria_adder_operator_is_after": "est après", + "common_criteria_adder_true_label": "Oui", + "common_criteria_adder_false_label": "Non", + "common_criteria_adder_value_label": "Valeur", + "common_criteria_adder_submit_label": "Ajouter" +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_it_IT.json b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_it_IT.json new file mode 100644 index 000000000000..5234ecc7f0b8 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_it_IT.json @@ -0,0 +1,18 @@ +{ + "common_criteria_adder_filter_label": "Aggiungi filtri", + "common_criteria_adder_column_label": "Colonna", + "common_criteria_adder_operator_label": "Condizione", + "common_criteria_adder_operator_includes": "contiene", + "common_criteria_adder_operator_starts_with": "inizia per", + "common_criteria_adder_operator_ends_with": "termina per", + "common_criteria_adder_operator_is_equal": "è uguale a", + "common_criteria_adder_operator_is_different": "è diverso da", + "common_criteria_adder_operator_is_lower": "è inferiore a", + "common_criteria_adder_operator_is_higher": "è superiore a", + "common_criteria_adder_operator_is_before": "è prima", + "common_criteria_adder_operator_is_after": "è dopo", + "common_criteria_adder_true_label": "Sì", + "common_criteria_adder_false_label": "No", + "common_criteria_adder_value_label": "Valore", + "common_criteria_adder_submit_label": "Aggiungi" +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_pl_PL.json b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_pl_PL.json new file mode 100644 index 000000000000..92eb00f8a53a --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_pl_PL.json @@ -0,0 +1,18 @@ +{ + "common_criteria_adder_filter_label": "Filtruj", + "common_criteria_adder_column_label": "Kolumna", + "common_criteria_adder_operator_label": "Warunek", + "common_criteria_adder_operator_includes": "zawiera", + "common_criteria_adder_operator_starts_with": "rozpoczyna się od", + "common_criteria_adder_operator_ends_with": "kończy się na", + "common_criteria_adder_operator_is_equal": "równa się", + "common_criteria_adder_operator_is_different": "nie jest", + "common_criteria_adder_operator_is_lower": "jest mniejszy niż", + "common_criteria_adder_operator_is_higher": "jest większy niż", + "common_criteria_adder_operator_is_before": "jest przed", + "common_criteria_adder_operator_is_after": "jest po", + "common_criteria_adder_true_label": "Tak", + "common_criteria_adder_false_label": "Nie", + "common_criteria_adder_value_label": "Wartość", + "common_criteria_adder_submit_label": "Dodaj" +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_pt_PT.json b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_pt_PT.json new file mode 100644 index 000000000000..0cac51c6c038 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/Messages_pt_PT.json @@ -0,0 +1,18 @@ +{ + "common_criteria_adder_filter_label": "Filtrar", + "common_criteria_adder_column_label": "Coluna", + "common_criteria_adder_operator_label": "Condição", + "common_criteria_adder_operator_includes": "contém", + "common_criteria_adder_operator_starts_with": "começa por", + "common_criteria_adder_operator_ends_with": "termina em", + "common_criteria_adder_operator_is_equal": "é igual a", + "common_criteria_adder_operator_is_different": "é diferente de", + "common_criteria_adder_operator_is_lower": "é inferior a", + "common_criteria_adder_operator_is_higher": "é superior a", + "common_criteria_adder_operator_is_before": "está antes", + "common_criteria_adder_operator_is_after": "está depois", + "common_criteria_adder_true_label": "Sim", + "common_criteria_adder_false_label": "Não", + "common_criteria_adder_value_label": "Valor", + "common_criteria_adder_submit_label": "Adicionar" +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/index.ts b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/index.ts new file mode 100644 index 000000000000..68cffeea719d --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/translations/index.ts @@ -0,0 +1,27 @@ +import i18next from 'i18next'; + +import de_DE from './Messages_de_DE.json'; +import en_GB from './Messages_en_GB.json'; +import es_ES from './Messages_es_ES.json'; +import fr_CA from './Messages_fr_CA.json'; +import fr_FR from './Messages_fr_FR.json'; +import it_IT from './Messages_it_IT.json'; +import pl_PL from './Messages_pl_PL.json'; +import pt_PT from './Messages_pt_PT.json'; + +function addTranslations() { + i18next.addResources('de_DE', 'filters', de_DE); + i18next.addResources('en_GB', 'filters', en_GB); + i18next.addResources('es_ES', 'filters', es_ES); + i18next.addResources('fr_CA', 'filters', fr_CA); + i18next.addResources('fr_FR', 'filters', fr_FR); + i18next.addResources('it_IT', 'filters', it_IT); + i18next.addResources('pl_PL', 'filters', pl_PL); + i18next.addResources('pt_PT', 'filters', pt_PT); +} + +if (i18next.isInitialized) { + addTranslations(); +} else { + i18next.on('initialized', addTranslations); +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/data-table/useColumnFilters.hook.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/useColumnFilters.hook.tsx new file mode 100644 index 000000000000..510b4a9d9d09 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/components/data-table/useColumnFilters.hook.tsx @@ -0,0 +1,32 @@ +import { useState } from 'react'; +import { Filter } from '@/lib/filters'; +import { FilterWithLabel } from './DatatableFiltersList'; + +const filterEquals = (a: Filter, b: Filter) => + a.key === b.key && a.value === b.value && a.comparator === b.comparator; + +export function useColumnFilters() { + const [filters, setFilters] = useState([]); + + return { + filters, + addFilter: (filter: FilterWithLabel) => { + if (filter.value) { + setFilters((previousFilters) => { + /** + * ? To remove the duplication from the filters + */ + if (previousFilters.some((f) => filterEquals(f, filter))) { + return previousFilters; + } + return [...previousFilters, filter]; + }); + } + }, + removeFilter: (filter: Filter) => { + setFilters((previousFilters) => + previousFilters.filter((f) => !filterEquals(f, filter)), + ); + }, + }; +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/components/ui/data-table.tsx b/packages/manager/apps/pci-ai-notebooks/src/components/ui/data-table.tsx deleted file mode 100644 index b3577ed56e1c..000000000000 --- a/packages/manager/apps/pci-ai-notebooks/src/components/ui/data-table.tsx +++ /dev/null @@ -1,263 +0,0 @@ -import { useState } from 'react'; -import { ChevronDown, ChevronUp, ChevronsUpDown, ChevronLeft, ChevronRight, ChevronFirst, ChevronLast } from 'lucide-react'; -import { - ColumnDef, - SortingColumn, - SortingState, - flexRender, - Table as TanStackTable, - getCoreRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from '@tanstack/react-table'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; -import { Button } from '@/components/ui/button'; -import { Skeleton } from './skeleton'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select'; - -interface DataTablePaginationProps { - table: TanStackTable -} -export function DataTablePagination({ - table, -}: DataTablePaginationProps) { - return ( -
-
-
- -
-
- Page {table.getState().pagination.pageIndex + 1} of{" "} - {table.getPageCount()} -
-
- - - - -
-
-
- ); -} - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; - pageSize?: number; -} - -export function DataTable({ - columns, - data, - pageSize, -}: DataTableProps) { - const [sorting, setSorting] = useState([]); - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - onSortingChange: setSorting, - getSortedRowModel: getSortedRowModel(), - state: { - sorting, - }, - initialState: { - pagination: { pageSize: pageSize ?? 5 }, - }, - }); - - return ( - <> -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - - )) - ) : ( - - - No results. - - - )} - -
-
- - - ); -} - -interface SortableHeaderProps { - column: SortingColumn; - children: React.ReactNode; -} -export function SortableHeader({ - column, - children, -}: SortableHeaderProps) { - const sort = column.getIsSorted(); - let icon = ; - if (sort === 'asc') { - icon = ; - } else if (sort === 'desc') { - icon = ; - } - - const buttonClass = `px-0 font-bold hover:bg-primary-100 ${ - sort - ? 'text-primary-500 hover:text-primary-500' - : 'text-primary-700 hover:text-primary-500' - }`; - return ( - - ); -} - -interface DataTableSkeletonProps { - columns?: number; - rows?: number; - height?: number; - width?: number; -} -DataTable.Skeleton = function DataTableSkeleton({ - columns = 5, - rows = 5, - height = 16, - width = 80, -}: DataTableSkeletonProps) { - return ( - - - - {Array.from({ length: columns }).map((colHead, iColHead) => ( - - - - ))} - - - - {Array.from({ length: rows }).map((row, iRow) => ( - - {Array.from({ length: columns }).map((col, iCol) => ( - - - - ))} - - ))} - -
- ); -}; \ No newline at end of file diff --git a/packages/manager/apps/pci-ai-notebooks/src/lib/filters.ts b/packages/manager/apps/pci-ai-notebooks/src/lib/filters.ts new file mode 100644 index 000000000000..29924ac1be86 --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/lib/filters.ts @@ -0,0 +1,99 @@ +export enum FilterComparator { + Includes = 'includes', + StartsWith = 'starts_with', + EndsWith = 'ends_with', + IsEqual = 'is_equal', + IsDifferent = 'is_different', + IsLower = 'is_lower', + IsHigher = 'is_higher', + IsBefore = 'is_before', + IsAfter = 'is_after', + IsIn = 'is_in', +} + +export type Filter = { + key: string; + value: string | string[]; + comparator: FilterComparator; +}; + +export const FilterCategories = { + Options: [FilterComparator.IsEqual, FilterComparator.IsDifferent], + Numeric: [ + FilterComparator.IsEqual, + FilterComparator.IsDifferent, + FilterComparator.IsLower, + FilterComparator.IsHigher, + ], + String: [ + FilterComparator.Includes, + FilterComparator.StartsWith, + FilterComparator.EndsWith, + FilterComparator.IsEqual, + FilterComparator.IsDifferent, + ], + Date: [ + FilterComparator.IsEqual, + FilterComparator.IsDifferent, + FilterComparator.IsBefore, + FilterComparator.IsAfter, + ], +}; + +export function applyFilters(items: T[] = [], filters: Filter[] = []) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const getNestedValue = (obj: any, path: string): any => { + return path.split('.').reduce((acc, key) => { + const arrayMatch = key.match(/(\w+)\[(\d+)\]/); + if (arrayMatch) { + const [, arrayKey, index] = arrayMatch; + return acc && acc[arrayKey] && acc[arrayKey][Number(index)]; + } + return acc && acc[key]; + }, obj); + }; + return items.filter((item) => { + let keep = true; + filters.forEach((filter) => { + const value = getNestedValue(item, filter.key as string); + const comp = filter.value as string; + switch (filter.comparator) { + case FilterComparator.Includes: + keep = keep && `${value}`.toLowerCase().includes(comp.toLowerCase()); + break; + case FilterComparator.StartsWith: + keep = + keep && `${value}`.toLowerCase().startsWith(comp.toLowerCase()); + break; + case FilterComparator.EndsWith: + keep = keep && `${value}`.toLowerCase().endsWith(comp.toLowerCase()); + break; + case FilterComparator.IsEqual: + keep = keep && `${value}`.toLowerCase() === comp.toLowerCase(); + break; + case FilterComparator.IsDifferent: + keep = keep && `${value}`.toLowerCase() !== comp.toLowerCase(); + break; + case FilterComparator.IsLower: + keep = keep && Number(value) < Number(comp); + break; + case FilterComparator.IsHigher: + keep = keep && Number(value) > Number(comp); + break; + case FilterComparator.IsBefore: + keep = keep && new Date(`${value}`) < new Date(comp); + break; + case FilterComparator.IsAfter: + keep = keep && new Date(`${value}`) > new Date(comp); + break; + case FilterComparator.IsIn: + keep = + keep && !!(filter.value as string[]).find((i) => i === `${value}`); + break; + default: + break; + } + }); + return keep; + }); +} diff --git a/packages/manager/apps/pci-ai-notebooks/src/lib/orderFunnelHelper.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/lib/orderFunnelHelper.spec.tsx index eafc15c78745..6642e878d7b2 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/lib/orderFunnelHelper.spec.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/lib/orderFunnelHelper.spec.tsx @@ -62,7 +62,7 @@ describe('orderFunnelHelper', () => { const notebookSpecInputCPU: ai.notebook.NotebookSpecInput = { env: { - editorId: 'editorId', + editorId: 'jupyterlab', frameworkId: 'noId', frameworkVersion: '', }, diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/Notebooks.page.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/Notebooks.page.tsx index 86b3d1d2f994..c2b8801755e4 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/Notebooks.page.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/Notebooks.page.tsx @@ -1,8 +1,5 @@ -import { Plus } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useParams, Outlet } from 'react-router-dom'; -import Link from '@/components/links/Link.component'; -import { Button } from '@/components/ui/button'; import { POLLING } from '@/configuration/polling.constants'; import { useUserActivityContext } from '@/contexts/UserActivityContext'; import { useGetNotebooks } from '@/hooks/api/ai/notebook/useGetNotebooks.hook'; @@ -27,17 +24,6 @@ const Notebooks = () => {

{t('title')}

- diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/_components/BackupsListColumns.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/_components/BackupsListColumns.component.tsx index 4cd7e421b417..fa704e3ab3f5 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/_components/BackupsListColumns.component.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/_components/BackupsListColumns.component.tsx @@ -9,8 +9,9 @@ import { DropdownMenuLabel, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { SortableHeader } from '@/components/ui/data-table'; + import * as ai from '@/types/cloud/project/ai'; +import DataTable from '@/components/data-table'; interface BackupsListColumnsProps { onForkClicked: (backup: ai.notebook.Backup) => void; @@ -23,25 +24,27 @@ export const getColumns = ({ onForkClicked }: BackupsListColumnsProps) => { id: 'Id', accessorFn: (row) => row.id, header: ({ column }) => ( - {t('tableHeaderId')} + + {t('tableHeaderId')} + ), }, { id: 'CreationDate', accessorFn: (row) => row.createdAt, header: ({ column }) => ( - + {t('tableHeaderCreationDate')} - + ), }, { id: 'UpdateDate', accessorFn: (row) => row.updatedAt, header: ({ column }) => ( - + {t('tableHeaderUpdateDate')} - + ), }, { diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/_components/BackupsListTable.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/_components/BackupsListTable.component.tsx index 9bcdf5ceb51b..57ba1e16538d 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/_components/BackupsListTable.component.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/backups/_components/BackupsListTable.component.tsx @@ -2,8 +2,8 @@ import { ColumnDef } from '@tanstack/react-table'; import { useNavigate } from 'react-router-dom'; import * as ai from '@/types/cloud/project/ai'; import { getColumns } from './BackupsListColumns.component'; -import { DataTable } from '@/components/ui/data-table'; import { Skeleton } from '@/components/ui/skeleton'; +import DataTable from '@/components/data-table'; interface BackupsListProps { backups: ai.notebook.Backup[]; @@ -18,7 +18,7 @@ export default function BackupsList({ backups }: Readonly) { }, }); - return ; + return ; } BackupsList.Skeleton = function BackupsListSkeleton() { diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/containers/_components/VolumesListColumns.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/containers/_components/VolumesListColumns.component.tsx index 0f828c7c5de1..c2e8f9b995f3 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/containers/_components/VolumesListColumns.component.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/[notebookId]/containers/_components/VolumesListColumns.component.tsx @@ -9,11 +9,11 @@ import { DropdownMenuLabel, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { SortableHeader } from '@/components/ui/data-table'; import * as ai from '@/types/cloud/project/ai'; import { useToast } from '@/components/ui/use-toast'; import { useNotebookData } from '../../Notebook.context'; import { isDataSyncNotebook } from '@/lib/notebookHelper'; +import DataTable from '@/components/data-table'; interface VolumesListColumnsProps { onDataSyncClicked: (volume: ai.volume.Volume) => void; @@ -30,29 +30,31 @@ export const getColumns = ({ onDataSyncClicked }: VolumesListColumnsProps) => { id: 'Alias', accessorFn: (row) => row.volumeSource.dataStore.alias, header: ({ column }) => ( - {t('tableHeaderAlias')} + + {t('tableHeaderAlias')} + ), }, { id: 'Container', accessorFn: (row) => row.volumeSource.dataStore.container, header: ({ column }) => ( - + {t('tableHeaderContainer')} - + ), }, { id: 'Mountpath', accessorFn: (row) => row.mountPath, header: ({ column }) => ( - + {t('tableHeaderMountPath')} - + ), cell: ({ row }) => { return ( -
+
+ + + + + + + + + ); } NotebooksList.Skeleton = function NotebooksListSkeleton() { diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/Create.page.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/Create.page.tsx index 8eeaadc9bff7..3950d300e733 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/Create.page.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/Create.page.tsx @@ -45,12 +45,12 @@ const Notebook = () => { }); const loading = - regionsQuery.isLoading || - catalogQuery.isLoading || - frameworkQuery.isLoading || - editorQuery.isLoading || - sshKeyQuery.isLoading || - suggestionsQuery.isLoading; + regionsQuery.isPending || + catalogQuery.isPending || + frameworkQuery.isPending || + editorQuery.isPending || + sshKeyQuery.isPending || + suggestionsQuery.isPending; return ( <> diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/Create.spec.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/Create.spec.tsx new file mode 100644 index 000000000000..39f0e3e31efd --- /dev/null +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/Create.spec.tsx @@ -0,0 +1,269 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + act, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; + +import Notebook, { + breadcrumb as Breadcrumb, +} from '@/pages/notebooks/create/Create.page'; + +import { Locale } from '@/hooks/useLocale'; +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; + +import { mockedUser } from '@/__tests__/helpers/mocks/user'; +import { mockedCatalog } from '@/__tests__/helpers/mocks/catalog'; +import { mockedPciProject } from '@/__tests__/helpers/mocks/project'; +import { mockedSuggestion } from '@/__tests__/helpers/mocks/suggestion'; +import { + mockedCapabilitiesRegionBHS, + mockedCapabilitiesRegionGRA, +} from '@/__tests__/helpers/mocks/region'; +import { + mockedEditor, + mockedEditorBis, +} from '@/__tests__/helpers/mocks/notebook/editor'; +import { + mockedFramework, + mockedFrameworkBis, +} from '@/__tests__/helpers/mocks/notebook/framework'; +import { + mockedSshKey, + mockedSshKeyBis, +} from '@/__tests__/helpers/mocks/sshkey'; +import { mockedCommand } from '@/__tests__/helpers/mocks/command'; +import { mockedCapabilitiesFlavorCPU } from '@/__tests__/helpers/mocks/flavor'; +import { + mockedDatastoreWithContainerGit, + mockedDatastoreWithContainerS3, +} from '@/__tests__/helpers/mocks/datastore'; +import * as notebookApi from '@/data/api/ai/notebook/notebook.api'; +import { apiErrorMock } from '@/__tests__/helpers/mocks/aiError'; +import { useToast } from '@/components/ui/use-toast'; + +const mockedUsedNavigate = vi.fn(); +describe('Order funnel page', () => { + beforeEach(() => { + vi.restoreAllMocks(); + + // Mock necessary hooks and dependencies + vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), + Trans: ({ children }: { children: React.ReactNode }) => children, + })); + vi.mock('@ovh-ux/manager-react-shell-client', async (importOriginal) => { + const mod = await importOriginal< + typeof import('@ovh-ux/manager-react-shell-client') + >(); + return { + ...mod, + useShell: vi.fn(() => ({ + i18n: { + getLocale: vi.fn(() => Locale.fr_FR), + onLocaleChange: vi.fn(), + setLocale: vi.fn(), + }, + environment: { + getEnvironment: vi.fn(() => ({ + getUser: vi.fn(() => mockedUser), + })), + }, + })), + }; + }); + + const ResizeObserverMock = vi.fn(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), + })); + vi.stubGlobal('ResizeObserver', ResizeObserverMock); + + vi.mock('react-router-dom', async () => { + const mod = await vi.importActual('react-router-dom'); + return { + ...mod, + useParams: () => ({ + projectId: 'projectId', + }), + useNavigate: () => mockedUsedNavigate, + }; + }); + + vi.mock('@/data/api/project/project.api', () => { + return { + getProject: vi.fn(() => mockedPciProject), + }; + }); + + vi.mock('@/data/api/ai/notebook/notebook.api', () => ({ + getCommand: vi.fn(() => mockedCommand), + addNotebook: vi.fn((notebook) => notebook), + })); + + vi.mock('@/data/api/ai/notebook/suggestions.api', () => ({ + getSuggestions: vi.fn(() => mockedSuggestion), + })); + + vi.mock('@/data/api/catalog/catalog.api', () => ({ + catalogApi: { + getCatalog: vi.fn(() => mockedCatalog), + }, + })); + + vi.mock('@/data/api/ai/capabilities.api', () => ({ + getRegions: vi.fn(() => [ + mockedCapabilitiesRegionGRA, + mockedCapabilitiesRegionBHS, + ]), + })); + + vi.mock('@/data/api/ai/notebook/capabilities/framework.api', () => ({ + getFramework: vi.fn(() => [mockedFramework, mockedFrameworkBis]), + })); + + vi.mock('@/data/api/ai/notebook/capabilities/editor.api', () => ({ + getEditor: vi.fn(() => [mockedEditor, mockedEditorBis]), + })); + + vi.mock('@/data/api/ai/capabilities.api', () => ({ + getRegions: vi.fn(() => [ + mockedCapabilitiesRegionGRA, + mockedCapabilitiesRegionBHS, + ]), + getFlavor: vi.fn(() => [mockedCapabilitiesFlavorCPU]), + })); + + vi.mock('@/data/api/ai/datastore.api', () => ({ + getDatastores: vi.fn(() => [ + mockedDatastoreWithContainerGit, + mockedDatastoreWithContainerS3, + ]), + })); + + vi.mock('@/data/api/sshkey/sshkey.api', () => ({ + getSshkey: vi.fn(() => [mockedSshKey, mockedSshKeyBis]), + })); + + vi.mock('@/components/ui/use-toast', () => { + const toastMock = vi.fn(); + return { + useToast: vi.fn(() => ({ + toast: toastMock, + })), + }; + }); + + const mockScrollIntoView = vi.fn(); + window.HTMLElement.prototype.scrollIntoView = mockScrollIntoView; + }); + afterEach(() => { + vi.clearAllMocks(); + }); + + it('renders the breadcrumb component', async () => { + const translationKey = 'breadcrumb'; + render(, { wrapper: RouterWithQueryClientWrapper }); + await waitFor(() => { + expect(screen.getByText(translationKey)).toBeInTheDocument(); + }); + }); + + it('renders the skeleton component while loading', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + await waitFor(() => { + expect(screen.getByTestId('order-funnel-skeleton')).toBeInTheDocument(); + }); + }); + + it('renders the order funnel', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + await waitFor(() => { + expect(screen.getByTestId('order-funnel-container')).toBeInTheDocument(); + expect(screen.getByTestId('name-section')).toBeInTheDocument(); + expect(screen.getByTestId('flavor-section')).toBeInTheDocument(); + expect(screen.getByTestId('region-section')).toBeInTheDocument(); + expect(screen.getByTestId('framework-section')).toBeInTheDocument(); + expect(screen.getByTestId('editor-section')).toBeInTheDocument(); + expect(screen.getByTestId('advance-config-section')).toBeInTheDocument(); + expect(screen.getByTestId('order-submit-button')).toBeInTheDocument(); + }); + }); + + it('trigger toast error on getCommand API Error', async () => { + vi.mocked(notebookApi.getCommand).mockImplementation(() => { + throw apiErrorMock; + }); + render(, { wrapper: RouterWithQueryClientWrapper }); + await waitFor(() => { + expect(screen.getByTestId('order-funnel-container')).toBeInTheDocument(); + }); + act(() => { + fireEvent.click(screen.getByTestId('advanced-config-button')); + }); + act(() => { + fireEvent.click(screen.getByTestId('cli-command-button')); + }); + await waitFor(() => { + expect(notebookApi.getCommand).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'errorGetCommandCli', + description: apiErrorMock.response.data.message, + variant: 'destructive', + }); + }); + }); + + it('trigger getCommand on Cli Command button click', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + await waitFor(() => { + expect(screen.getByTestId('order-funnel-container')).toBeInTheDocument(); + }); + act(() => { + fireEvent.click(screen.getByTestId('cli-command-button')); + }); + await waitFor(() => { + expect(notebookApi.getCommand).toHaveBeenCalled(); + }); + }); + + it('trigger toast error on addNotebook API Error', async () => { + vi.mocked(notebookApi.addNotebook).mockImplementation(() => { + throw apiErrorMock; + }); + render(, { wrapper: RouterWithQueryClientWrapper }); + await waitFor(() => { + expect(screen.getByTestId('order-funnel-container')).toBeInTheDocument(); + }); + act(() => { + fireEvent.click(screen.getByTestId('order-submit-button')); + }); + await waitFor(() => { + expect(notebookApi.addNotebook).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'errorCreatingNotebook', + description: apiErrorMock.response.data.message, + variant: 'destructive', + }); + }); + }); + + it('trigger add notebook on click', async () => { + render(, { wrapper: RouterWithQueryClientWrapper }); + await waitFor(() => { + expect(screen.getByTestId('order-funnel-container')).toBeInTheDocument(); + }); + act(() => { + fireEvent.click(screen.getByTestId('order-submit-button')); + }); + await waitFor(() => { + expect(notebookApi.addNotebook).toHaveBeenCalled(); + }); + expect(mockedUsedNavigate).toHaveBeenCalledWith('../undefined'); + }); +}); diff --git a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/_components/OrderFunnel.component.tsx b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/_components/OrderFunnel.component.tsx index 7d90374366f2..b03eaff23a5e 100644 --- a/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/_components/OrderFunnel.component.tsx +++ b/packages/manager/apps/pci-ai-notebooks/src/pages/notebooks/create/_components/OrderFunnel.component.tsx @@ -176,7 +176,7 @@ const OrderFunnel = ({ data-testid="order-funnel-container" className="col-span-1 md:col-span-3 divide-y-[24px] divide-transparent" > -
+

{t('fieldDimensionLabel')}

-
+
-
+
-
+

{t('fieldCaracteristicLabel')}

-
+
-
+
{/* Advanced configuration */} -
+