From 26bb028ace0531e238409a48e0a327fdb8fcf5b6 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Tue, 2 Apr 2024 22:26:22 +0300 Subject: [PATCH] refactor(kube/namespaces): migrate table to react [EE-4694] (#10988) --- .../resourcePoolsDatatable.html | 208 ------------------ .../resourcePoolsDatatable.js | 15 -- .../resourcePoolsDatatableController.js | 93 -------- app/kubernetes/helpers/namespaceHelper.js | 5 +- app/kubernetes/models/namespace/models.js | 1 - app/kubernetes/react/components/index.ts | 2 + app/kubernetes/react/components/namespaces.ts | 18 ++ .../views/resource-pools/resourcePools.html | 15 +- app/react/components/StatusBadge.stories.tsx | 67 ++++++ app/react/components/StatusBadge.tsx | 11 +- app/react/components/buttons/DeleteButton.tsx | 33 ++- .../kubernetes/namespaces/ListView/.keep | 0 .../ListView/NamespacesDatatable.tsx | 92 ++++++++ .../namespaces/ListView/columns/actions.tsx | 57 +++++ .../namespaces/ListView/columns/helper.ts | 5 + .../ListView/columns/useColumns.tsx | 95 ++++++++ .../kubernetes/namespaces/ListView/types.ts | 11 + .../namespaces/isDefaultNamespace.ts | 5 + 18 files changed, 387 insertions(+), 346 deletions(-) delete mode 100644 app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.html delete mode 100644 app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js delete mode 100644 app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js create mode 100644 app/kubernetes/react/components/namespaces.ts create mode 100644 app/react/components/StatusBadge.stories.tsx delete mode 100644 app/react/kubernetes/namespaces/ListView/.keep create mode 100644 app/react/kubernetes/namespaces/ListView/NamespacesDatatable.tsx create mode 100644 app/react/kubernetes/namespaces/ListView/columns/actions.tsx create mode 100644 app/react/kubernetes/namespaces/ListView/columns/helper.ts create mode 100644 app/react/kubernetes/namespaces/ListView/columns/useColumns.tsx create mode 100644 app/react/kubernetes/namespaces/ListView/types.ts create mode 100644 app/react/kubernetes/namespaces/isDefaultNamespace.ts diff --git a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.html b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.html deleted file mode 100644 index 69788628d..000000000 --- a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.html +++ /dev/null @@ -1,208 +0,0 @@ -
- - - -
-
-
-
- -
- Namespaces -
- -
- - - -
-
- - - - - - -
-
- -
- - -
System resources are hidden, this can be changed in the table settings.
-
-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - -
-
- - - - - - - -
- - - - - {{ item.Namespace.Name }} - system - - {{ item.Namespace.Status }} - - Enabled - - - {{ item.Namespace.CreationDate | getisodate }} {{ item.Namespace.ResourcePoolOwner ? 'by ' + item.Namespace.ResourcePoolOwner : '' }} - - - Manage access - - - -
Loading...
No namespace available.
-
- -
-
-
diff --git a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js deleted file mode 100644 index 93f49255e..000000000 --- a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js +++ /dev/null @@ -1,15 +0,0 @@ -angular.module('portainer.kubernetes').component('kubernetesResourcePoolsDatatable', { - templateUrl: './resourcePoolsDatatable.html', - controller: 'KubernetesResourcePoolsDatatableController', - bindings: { - restrictDefaultNamespace: '<', - titleText: '@', - titleIcon: '@', - dataset: '<', - tableKey: '@', - orderBy: '@', - reverseOrder: '<', - removeAction: '<', - refreshCallback: '<', - }, -}); diff --git a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js deleted file mode 100644 index f058f39ed..000000000 --- a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js +++ /dev/null @@ -1,93 +0,0 @@ -import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; - -angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableController', [ - '$scope', - '$controller', - 'Authentication', - 'DatatableService', - function ($scope, $controller, Authentication, DatatableService) { - angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); - - var ctrl = this; - - this.settings = Object.assign(this.settings, { - showSystem: false, - }); - - this.onSettingsShowSystemChange = function () { - DatatableService.setDataTableSettings(this.tableKey, this.settings); - }; - - this.canManageAccess = function (item) { - if (!this.restrictDefaultNamespace) { - return !KubernetesNamespaceHelper.isDefaultNamespace(item.Namespace.Name) && !this.isSystemNamespace(item); - } else { - return !this.isSystemNamespace(item); - } - }; - - this.disableRemove = function (item) { - return this.isSystemNamespace(item) || KubernetesNamespaceHelper.isDefaultNamespace(item.Namespace.Name); - }; - - this.isSystemNamespace = function (item) { - return KubernetesNamespaceHelper.isSystemNamespace(item.Namespace.Name); - }; - - this.isDisplayed = function (item) { - return !ctrl.isSystemNamespace(item) || (ctrl.settings.showSystem && ctrl.isAdmin); - }; - - this.namespaceStatusColor = function (status) { - switch (status.toLowerCase()) { - case 'active': - return 'success'; - case 'terminating': - return 'danger'; - default: - return 'primary'; - } - }; - - /** - * Do not allow system namespaces to be selected - */ - this.allowSelection = function (item) { - return !this.disableRemove(item); - }; - - this.$onInit = function () { - this.isAdmin = Authentication.isAdmin(); - this.setDefaults(); - this.prepareTableFromDataset(); - - this.state.orderBy = this.orderBy; - var storedOrder = DatatableService.getDataTableOrder(this.tableKey); - if (storedOrder !== null) { - this.state.reverseOrder = storedOrder.reverse; - this.state.orderBy = storedOrder.orderBy; - } - - var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); - if (textFilter !== null) { - this.state.textFilter = textFilter; - this.onTextFilterChange(); - } - - var storedFilters = DatatableService.getDataTableFilters(this.tableKey); - if (storedFilters !== null) { - this.filters = storedFilters; - } - if (this.filters && this.filters.state) { - this.filters.state.open = false; - } - - var storedSettings = DatatableService.getDataTableSettings(this.tableKey); - if (storedSettings !== null) { - this.settings = storedSettings; - this.settings.open = false; - } - this.onSettingsRepeaterChange(); - }; - }, -]); diff --git a/app/kubernetes/helpers/namespaceHelper.js b/app/kubernetes/helpers/namespaceHelper.js index dc7d03b20..fb67a36a6 100644 --- a/app/kubernetes/helpers/namespaceHelper.js +++ b/app/kubernetes/helpers/namespaceHelper.js @@ -1,7 +1,8 @@ import _ from 'lodash-es'; -import { KUBERNETES_DEFAULT_NAMESPACE, KUBERNETES_DEFAULT_SYSTEM_NAMESPACES } from 'Kubernetes/models/namespace/models'; +import { KUBERNETES_DEFAULT_SYSTEM_NAMESPACES } from 'Kubernetes/models/namespace/models'; import { isSystem } from 'Kubernetes/store/namespace'; +import { isDefaultNamespace } from '@/react/kubernetes/namespaces/isDefaultNamespace'; export default class KubernetesNamespaceHelper { /** @@ -19,7 +20,7 @@ export default class KubernetesNamespaceHelper { * @returns Boolean */ static isDefaultNamespace(namespace) { - return namespace === KUBERNETES_DEFAULT_NAMESPACE; + return isDefaultNamespace(namespace); } /** diff --git a/app/kubernetes/models/namespace/models.js b/app/kubernetes/models/namespace/models.js index 26af381d5..7888cdcbc 100644 --- a/app/kubernetes/models/namespace/models.js +++ b/app/kubernetes/models/namespace/models.js @@ -12,4 +12,3 @@ export class KubernetesNamespace { } export const KUBERNETES_DEFAULT_SYSTEM_NAMESPACES = ['kube-system', 'kube-public', 'kube-node-lease', 'portainer']; -export const KUBERNETES_DEFAULT_NAMESPACE = 'default'; diff --git a/app/kubernetes/react/components/index.ts b/app/kubernetes/react/components/index.ts index 1af275ac2..d8112d849 100644 --- a/app/kubernetes/react/components/index.ts +++ b/app/kubernetes/react/components/index.ts @@ -63,11 +63,13 @@ import { HelmInsightsBox } from '@/react/kubernetes/applications/ListView/Applic import { applicationsModule } from './applications'; import { volumesModule } from './volumes'; +import { namespacesModule } from './namespaces'; export const ngModule = angular .module('portainer.kubernetes.react.components', [ applicationsModule, volumesModule, + namespacesModule, ]) .component( 'ingressClassDatatable', diff --git a/app/kubernetes/react/components/namespaces.ts b/app/kubernetes/react/components/namespaces.ts new file mode 100644 index 000000000..0f42c5164 --- /dev/null +++ b/app/kubernetes/react/components/namespaces.ts @@ -0,0 +1,18 @@ +import angular from 'angular'; + +import { r2a } from '@/react-tools/react2angular'; +import { withUIRouter } from '@/react-tools/withUIRouter'; +import { NamespacesDatatable } from '@/react/kubernetes/namespaces/ListView/NamespacesDatatable'; +import { withCurrentUser } from '@/react-tools/withCurrentUser'; + +export const namespacesModule = angular + .module('portainer.kubernetes.react.components.namespaces', []) + + .component( + 'kubernetesNamespacesDatatable', + r2a(withUIRouter(withCurrentUser(NamespacesDatatable)), [ + 'dataset', + 'onRemove', + 'onRefresh', + ]) + ).name; diff --git a/app/kubernetes/views/resource-pools/resourcePools.html b/app/kubernetes/views/resource-pools/resourcePools.html index 9b8b56230..e995a16e9 100644 --- a/app/kubernetes/views/resource-pools/resourcePools.html +++ b/app/kubernetes/views/resource-pools/resourcePools.html @@ -3,18 +3,5 @@
-
-
- -
-
+
diff --git a/app/react/components/StatusBadge.stories.tsx b/app/react/components/StatusBadge.stories.tsx new file mode 100644 index 000000000..d2b1f763a --- /dev/null +++ b/app/react/components/StatusBadge.stories.tsx @@ -0,0 +1,67 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { Check } from 'lucide-react'; + +import { StatusBadge } from './StatusBadge'; + +const meta: Meta = { + title: 'Components/StatusBadge', + component: StatusBadge, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + children: 'Default', + }, +}; + +export const WithIcon: Story = { + args: { + icon: Check, + children: 'With Icon', + }, +}; + +export const Success: Story = { + args: { + color: 'success', + children: 'Success', + }, +}; + +export const Warning: Story = { + args: { + color: 'warning', + children: 'Warning', + }, +}; + +export const Danger: Story = { + args: { + color: 'danger', + children: 'Danger', + }, +}; + +export const WithAriaAttributes: Story = { + args: { + 'aria-label': 'Badge with Aria Attributes', + children: 'With Aria Attributes', + }, +}; + +export const WithChildren: Story = { + args: { + children: ( + <> + + ⭐️ + + With Children + + ), + }, +}; diff --git a/app/react/components/StatusBadge.tsx b/app/react/components/StatusBadge.tsx index 9f40d3ee9..81b03f6ad 100644 --- a/app/react/components/StatusBadge.tsx +++ b/app/react/components/StatusBadge.tsx @@ -12,20 +12,21 @@ export function StatusBadge({ }: PropsWithChildren< { className?: string; - color?: 'success' | 'danger' | 'warning' | 'default'; + color?: 'success' | 'danger' | 'warning' | 'info' | 'default'; icon?: IconProps['icon']; } & AriaAttributes >) { return ( | void; + onClick?: never; + } + | { + confirmMessage?: never; + onConfirmed?: never; + /** if onClick is set, will skip confirmation (confirmation should be done on the parent) */ + onClick(): void; + }; + export function DeleteButton({ disabled, - confirmMessage, - onConfirmed, size, children, -}: PropsWithChildren<{ - size?: ComponentProps['size']; - disabled?: boolean; - confirmMessage: ReactNode; - onConfirmed(): Promise | void; -}>) { + ...props +}: PropsWithChildren< + ConfirmOrClick & { + size?: ComponentProps['size']; + disabled?: boolean; + } +>) { return ( + ); + + function canManageAccess(item: NamespaceViewModel, environment: Environment) { + const name = item.Namespace.Name; + const isSystem = item.Namespace.IsSystem; + + return ( + !isSystem && + (!isDefaultNamespace(name) || + environment.Kubernetes.Configuration.RestrictDefaultNamespace) + ); + } +} diff --git a/app/react/kubernetes/namespaces/ListView/columns/helper.ts b/app/react/kubernetes/namespaces/ListView/columns/helper.ts new file mode 100644 index 000000000..7aecafcd7 --- /dev/null +++ b/app/react/kubernetes/namespaces/ListView/columns/helper.ts @@ -0,0 +1,5 @@ +import { createColumnHelper } from '@tanstack/react-table'; + +import { NamespaceViewModel } from '../types'; + +export const helper = createColumnHelper(); diff --git a/app/react/kubernetes/namespaces/ListView/columns/useColumns.tsx b/app/react/kubernetes/namespaces/ListView/columns/useColumns.tsx new file mode 100644 index 000000000..946c4e3ff --- /dev/null +++ b/app/react/kubernetes/namespaces/ListView/columns/useColumns.tsx @@ -0,0 +1,95 @@ +import _ from 'lodash'; +import { useMemo } from 'react'; + +import { isoDate } from '@/portainer/filters/filters'; +import { useAuthorizations } from '@/react/hooks/useUser'; + +import { Link } from '@@/Link'; +import { StatusBadge } from '@@/StatusBadge'; +import { Badge } from '@@/Badge'; +import { SystemBadge } from '@@/Badge/SystemBadge'; + +import { helper } from './helper'; +import { actions } from './actions'; + +export function useColumns() { + const hasAuthQuery = useAuthorizations( + 'K8sResourcePoolsAccessManagementRW', + undefined, + true + ); + return useMemo( + () => + _.compact([ + helper.accessor('Namespace.Name', { + header: 'Name', + id: 'Name', + cell: ({ getValue, row: { original: item } }) => { + const name = getValue(); + + return ( + <> + + {name} + + {item.Namespace.IsSystem && ( + + + + )} + + ); + }, + }), + helper.accessor('Namespace.Status', { + header: 'Status', + cell({ getValue }) { + const status = getValue(); + return {status}; + + function getColor(status: string) { + switch (status.toLowerCase()) { + case 'active': + return 'success'; + case 'terminating': + return 'danger'; + default: + return 'info'; + } + } + }, + }), + helper.accessor('Quota', { + cell({ getValue }) { + const quota = getValue(); + + if (!quota) { + return '-'; + } + + return Enabled; + }, + }), + helper.accessor('Namespace.CreationDate', { + header: 'Created', + cell({ row: { original: item } }) { + return ( + <> + {isoDate(item.Namespace.CreationDate)}{' '} + {item.Namespace.ResourcePoolOwner + ? ` by ${item.Namespace.ResourcePoolOwner}` + : ''} + + ); + }, + }), + hasAuthQuery.authorized && actions, + ]), + [hasAuthQuery.authorized] + ); +} diff --git a/app/react/kubernetes/namespaces/ListView/types.ts b/app/react/kubernetes/namespaces/ListView/types.ts new file mode 100644 index 000000000..ebc9f793a --- /dev/null +++ b/app/react/kubernetes/namespaces/ListView/types.ts @@ -0,0 +1,11 @@ +export interface NamespaceViewModel { + Namespace: { + Id: string; + Name: string; + Status: string; + CreationDate: number; + ResourcePoolOwner: string; + IsSystem: boolean; + }; + Quota: number; +} diff --git a/app/react/kubernetes/namespaces/isDefaultNamespace.ts b/app/react/kubernetes/namespaces/isDefaultNamespace.ts new file mode 100644 index 000000000..27b52c5a1 --- /dev/null +++ b/app/react/kubernetes/namespaces/isDefaultNamespace.ts @@ -0,0 +1,5 @@ +export const KUBERNETES_DEFAULT_NAMESPACE = 'default'; + +export function isDefaultNamespace(namespace: string) { + return namespace === KUBERNETES_DEFAULT_NAMESPACE; +}