From bd271ec5a183c74ef65fa83f64fdba114c03606e Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Tue, 9 Apr 2024 08:08:14 +0300 Subject: [PATCH] refactor(registries): migrate tags table to react [EE-6452] (#10990) --- app/portainer/react/components/registries.ts | 10 ++ .../ItemView/TagsDatatable/TagsDatatable.tsx | 57 +++++++++ .../TagsDatatable/columns/actions.tsx | 114 ++++++++++++++++++ .../TagsDatatable/columns/buildCell.tsx | 57 +++++++++ .../ItemView/TagsDatatable/columns/helper.ts | 5 + .../TagsDatatable/columns/useColumns.ts | 41 +++++++ .../ItemView/TagsDatatable/manifestsToTag.ts | 74 ++++++++++++ .../ItemView/TagsDatatable/meta.ts | 21 ++++ .../ItemView/TagsDatatable/types.ts | 3 + .../ItemView/TagsDatatable/useRetagState.ts | 33 +++++ .../ItemView/TagsDatatable/view-model.ts | 41 +++++++ .../repositories/queries/queryKeys.ts | 31 +++++ .../repositories/queries/useTagDetails.ts | 82 +++++++++++++ 13 files changed, 569 insertions(+) create mode 100644 app/react/portainer/registries/repositories/ItemView/TagsDatatable/TagsDatatable.tsx create mode 100644 app/react/portainer/registries/repositories/ItemView/TagsDatatable/columns/actions.tsx create mode 100644 app/react/portainer/registries/repositories/ItemView/TagsDatatable/columns/buildCell.tsx create mode 100644 app/react/portainer/registries/repositories/ItemView/TagsDatatable/columns/helper.ts create mode 100644 app/react/portainer/registries/repositories/ItemView/TagsDatatable/columns/useColumns.ts create mode 100644 app/react/portainer/registries/repositories/ItemView/TagsDatatable/manifestsToTag.ts create mode 100644 app/react/portainer/registries/repositories/ItemView/TagsDatatable/meta.ts create mode 100644 app/react/portainer/registries/repositories/ItemView/TagsDatatable/types.ts create mode 100644 app/react/portainer/registries/repositories/ItemView/TagsDatatable/useRetagState.ts create mode 100644 app/react/portainer/registries/repositories/ItemView/TagsDatatable/view-model.ts create mode 100644 app/react/portainer/registries/repositories/queries/queryKeys.ts create mode 100644 app/react/portainer/registries/repositories/queries/useTagDetails.ts diff --git a/app/portainer/react/components/registries.ts b/app/portainer/react/components/registries.ts index b096f373a..991c37549 100644 --- a/app/portainer/react/components/registries.ts +++ b/app/portainer/react/components/registries.ts @@ -4,10 +4,20 @@ import { r2a } from '@/react-tools/react2angular'; import { withReactQuery } from '@/react-tools/withReactQuery'; import { withUIRouter } from '@/react-tools/withUIRouter'; import { RepositoriesDatatable } from '@/react/portainer/registries/repositories/ListView/RepositoriesDatatable'; +import { TagsDatatable } from '@/react/portainer/registries/repositories/ItemView/TagsDatatable/TagsDatatable'; export const registriesModule = angular .module('portainer.app.react.components.registries', []) .component( 'registryRepositoriesDatatable', r2a(withUIRouter(withReactQuery(RepositoriesDatatable)), ['dataset']) + ) + .component( + 'registriesRepositoryTagsDatatable', + r2a(withUIRouter(withReactQuery(TagsDatatable)), [ + 'dataset', + 'advancedFeaturesAvailable', + 'onRemove', + 'onRetag', + ]) ).name; diff --git a/app/react/portainer/registries/repositories/ItemView/TagsDatatable/TagsDatatable.tsx b/app/react/portainer/registries/repositories/ItemView/TagsDatatable/TagsDatatable.tsx new file mode 100644 index 000000000..36333af17 --- /dev/null +++ b/app/react/portainer/registries/repositories/ItemView/TagsDatatable/TagsDatatable.tsx @@ -0,0 +1,57 @@ +import { TagIcon } from 'lucide-react'; +import { useStore } from 'zustand'; + +import { Datatable } from '@@/datatables'; +import { useTableStateWithStorage } from '@@/datatables/useTableState'; +import { DeleteButton } from '@@/buttons/DeleteButton'; +import { withMeta } from '@@/datatables/extend-options/withMeta'; + +import { Tag } from './types'; +import { useColumns } from './columns/useColumns'; +import { newNamesStore } from './useRetagState'; +import { RepositoryTagViewModel } from './view-model'; + +export function TagsDatatable({ + dataset, + advancedFeaturesAvailable, + onRemove, + onRetag, +}: { + dataset?: Tag[]; + advancedFeaturesAvailable: boolean; + onRemove: (tags: Tag[]) => void; + onRetag: (tags: Record) => Promise; +}) { + const updatesState = useStore(newNamesStore); + + const tableState = useTableStateWithStorage('registryRepositoryTags', 'name'); + const columns = useColumns(advancedFeaturesAvailable); + + return ( + + advancedFeaturesAvailable && ( + onRemove(selectedItems)} + /> + ) + } + getRowId={(tag) => tag.Name} + extendTableOptions={withMeta({ + onUpdate: async () => { + await onRetag(updatesState.updates); + updatesState.clear(); + }, + table: 'registry-repository-tags', + })} + /> + ); +} diff --git a/app/react/portainer/registries/repositories/ItemView/TagsDatatable/columns/actions.tsx b/app/react/portainer/registries/repositories/ItemView/TagsDatatable/columns/actions.tsx new file mode 100644 index 000000000..2473f061a --- /dev/null +++ b/app/react/portainer/registries/repositories/ItemView/TagsDatatable/columns/actions.tsx @@ -0,0 +1,114 @@ +import { CellContext } from '@tanstack/react-table'; +import { X, Check, TagIcon } from 'lucide-react'; +import { Form, Formik } from 'formik'; +import { useStore } from 'zustand'; +import { object, string } from 'yup'; + +import { Button } from '@@/buttons'; +import { Tooltip } from '@@/Tip/Tooltip'; +import { Input } from '@@/form-components/Input'; +import { FormError } from '@@/form-components/FormError'; + +import { Tag } from '../types'; +import { newNamesStore } from '../useRetagState'; +import { getTableMeta } from '../meta'; + +import { helper } from './helper'; +import { useDetails } from './buildCell'; + +export const actions = helper.display({ + header: 'Actions', + cell: ActionsCell, +}); + +function ActionsCell({ + table, + row: { original: item }, +}: CellContext) { + const meta = getTableMeta(table.options.meta); + const detailsQuery = useDetails(item.Name); + const state = useStore(newNamesStore); + + const isEdit = state.updates[item.Name] !== undefined; + + if (!detailsQuery.data) { + return null; + } + + const tagDetails = detailsQuery.data; + + if (!isEdit) { + return ( + + ); + } + + return ( + state.setName(item.Name)} + onChange={(name) => + state.setName(item.Name, { + ...tagDetails, + Name: name, + }) + } + onSubmit={() => meta.onUpdate()} + /> + ); +} + +const schema = object().shape({ + name: string() + .required() + .matches(/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/), +}); + +function EditTag({ + initialName, + onCancel, + onChange, + onSubmit, +}: { + initialName: string; + onChange: (name: string) => void; + onCancel(): void; + onSubmit(): void; +}) { + return ( + + {({ values, errors, setFieldValue }) => ( +
+ + + { + setFieldValue('name', e.target.value); + onChange(e.target.value); + }} + autoFocus + onClick={(e) => e.stopPropagation()} + /> + + {errors.name && {errors.name}} + +