From a5faddc56c1d25e5d4d3f97207010e7a51a41333 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Tue, 2 Apr 2024 23:12:34 +0300 Subject: [PATCH] refactor(kube/cluster): migrate node apps table to react [EE-4691] (#11016) --- .../nodeApplicationsDatatable.html | 168 ------------------ .../nodeApplicationsDatatable.js | 12 -- .../nodeApplicationsDatatableController.js | 54 ------ .../models/application/models/Application.ts | 13 +- .../application/models/ConfigurationVolume.ts | 8 +- .../application/models/PersistedFolder.ts | 6 +- .../react/components/clusterManagement.ts | 17 ++ app/kubernetes/react/components/index.ts | 2 + app/kubernetes/views/cluster/node/node.html | 20 +-- app/react/docker/swarm/NodeView/.keep | 0 .../NodeApplicationsDatatable.tsx | 56 ++++++ .../columns.helper.tsx | 5 + .../columns.name.tsx | 38 ++++ .../NodeApplicationsDatatable/columns.tsx | 57 ++++++ .../NodeApplicationsDatatable/types.ts | 6 + .../kubernetes/components/ExternalBadge.tsx | 5 + .../kubernetes/components/SystemBadge.tsx | 5 + .../queries/useIsSystemNamespace.ts | 12 ++ .../namespaces/queries/useNamespaceQuery.ts | 10 +- 19 files changed, 233 insertions(+), 261 deletions(-) delete mode 100644 app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.html delete mode 100644 app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.js delete mode 100644 app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatableController.js create mode 100644 app/kubernetes/react/components/clusterManagement.ts delete mode 100644 app/react/docker/swarm/NodeView/.keep create mode 100644 app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/NodeApplicationsDatatable.tsx create mode 100644 app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/columns.helper.tsx create mode 100644 app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/columns.name.tsx create mode 100644 app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/columns.tsx create mode 100644 app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/types.ts create mode 100644 app/react/kubernetes/components/ExternalBadge.tsx create mode 100644 app/react/kubernetes/components/SystemBadge.tsx create mode 100644 app/react/kubernetes/namespaces/queries/useIsSystemNamespace.ts diff --git a/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.html b/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.html deleted file mode 100644 index dd20be274..000000000 --- a/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.html +++ /dev/null @@ -1,168 +0,0 @@ -
- - -
-
-
- -
- - {{ $ctrl.titleText }} - -
- -
- - - - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - -
- {{ item.Name }} - system - external - {{ item.StackName || '-' }} - {{ item.ResourcePool }} - {{ item.Image | truncate: 64 }} + {{ item.Containers.length - 1 }}{{ item.CPU | kubernetesApplicationCPUValue }}{{ item.Memory | humansize }}
Loading...
No stack available.
-
- -
-
-
diff --git a/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.js b/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.js deleted file mode 100644 index a35bd41c2..000000000 --- a/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.js +++ /dev/null @@ -1,12 +0,0 @@ -angular.module('portainer.kubernetes').component('kubernetesNodeApplicationsDatatable', { - templateUrl: './nodeApplicationsDatatable.html', - controller: 'KubernetesNodeApplicationsDatatableController', - bindings: { - titleText: '@', - dataset: '<', - tableKey: '@', - orderBy: '@', - reverseOrder: '<', - refreshCallback: '<', - }, -}); diff --git a/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatableController.js b/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatableController.js deleted file mode 100644 index 3c9d19e24..000000000 --- a/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatableController.js +++ /dev/null @@ -1,54 +0,0 @@ -import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models/appConstants'; -import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; -import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; - -angular.module('portainer.docker').controller('KubernetesNodeApplicationsDatatableController', [ - '$scope', - '$controller', - 'DatatableService', - function ($scope, $controller, DatatableService) { - angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); - - this.isSystemNamespace = function (item) { - return KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool); - }; - - this.isExternalApplication = function (item) { - return KubernetesApplicationHelper.isExternalApplication(item); - }; - - this.$onInit = function () { - this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; - 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/models/application/models/Application.ts b/app/kubernetes/models/application/models/Application.ts index 05095675c..44caf9e91 100644 --- a/app/kubernetes/models/application/models/Application.ts +++ b/app/kubernetes/models/application/models/Application.ts @@ -1,5 +1,10 @@ import { ServiceType } from '@/react/kubernetes/applications/CreateView/application-services/types'; -import { AppType, DeploymentType } from '@/react/kubernetes/applications/types'; +import { + AppType, + DeploymentType, + AppDataAccessPolicy, + AppKind, +} from '@/react/kubernetes/applications/types'; import { ConfigurationVolume } from './ConfigurationVolume'; import { PersistedFolder } from './PersistedFolder'; @@ -13,7 +18,7 @@ export class Application { StackId: string; - ApplicationKind: string; + ApplicationKind?: AppKind; ApplicationOwner: string; @@ -63,7 +68,7 @@ export class Application { DeploymentType?: DeploymentType; - DataAccessPolicy: 'Unknown'; + DataAccessPolicy?: AppDataAccessPolicy; ApplicationType?: AppType; @@ -93,7 +98,6 @@ export class Application { this.Name = ''; this.StackName = ''; this.StackId = ''; - this.ApplicationKind = ''; this.ApplicationOwner = ''; this.ApplicationName = ''; this.ResourcePool = ''; @@ -112,7 +116,6 @@ export class Application { this.Env = []; this.PersistedFolders = []; this.ConfigurationVolumes = []; - this.DataAccessPolicy = 'Unknown'; this.RunningPodsCount = 0; this.TotalPodsCount = 0; this.Yaml = ''; diff --git a/app/kubernetes/models/application/models/ConfigurationVolume.ts b/app/kubernetes/models/application/models/ConfigurationVolume.ts index 3bc421607..5296425f9 100644 --- a/app/kubernetes/models/application/models/ConfigurationVolume.ts +++ b/app/kubernetes/models/application/models/ConfigurationVolume.ts @@ -1,9 +1,9 @@ export class ConfigurationVolume { - fileMountPath: string = ''; + fileMountPath = ''; - rootMountPath: string = ''; + rootMountPath = ''; - configurationKey: string = ''; + configurationKey = ''; - configurationName: string = ''; + configurationName = ''; } diff --git a/app/kubernetes/models/application/models/PersistedFolder.ts b/app/kubernetes/models/application/models/PersistedFolder.ts index bf915d33a..f9cb7824e 100644 --- a/app/kubernetes/models/application/models/PersistedFolder.ts +++ b/app/kubernetes/models/application/models/PersistedFolder.ts @@ -1,7 +1,7 @@ export class PersistedFolder { - MountPath: string = ''; + MountPath = ''; - persistentVolumeClaimName: string = ''; + persistentVolumeClaimName = ''; - HostPath: string = ''; + HostPath = ''; } diff --git a/app/kubernetes/react/components/clusterManagement.ts b/app/kubernetes/react/components/clusterManagement.ts new file mode 100644 index 000000000..72440fc7a --- /dev/null +++ b/app/kubernetes/react/components/clusterManagement.ts @@ -0,0 +1,17 @@ +import angular from 'angular'; + +import { r2a } from '@/react-tools/react2angular'; +import { withUIRouter } from '@/react-tools/withUIRouter'; +import { withCurrentUser } from '@/react-tools/withCurrentUser'; +import { NodeApplicationsDatatable } from '@/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/NodeApplicationsDatatable'; + +export const clusterManagementModule = angular + .module('portainer.kubernetes.react.components.clusterManagement', []) + .component( + 'kubernetesNodeApplicationsDatatable', + r2a(withUIRouter(withCurrentUser(NodeApplicationsDatatable)), [ + 'dataset', + 'isLoading', + 'onRefresh', + ]) + ).name; diff --git a/app/kubernetes/react/components/index.ts b/app/kubernetes/react/components/index.ts index e2a3e0cda..0ced94eda 100644 --- a/app/kubernetes/react/components/index.ts +++ b/app/kubernetes/react/components/index.ts @@ -65,12 +65,14 @@ import { IntegratedAppsDatatable } from '@/react/kubernetes/components/Integrate import { applicationsModule } from './applications'; import { volumesModule } from './volumes'; import { namespacesModule } from './namespaces'; +import { clusterManagementModule } from './clusterManagement'; export const ngModule = angular .module('portainer.kubernetes.react.components', [ applicationsModule, volumesModule, namespacesModule, + clusterManagementModule, ]) .component( 'ingressClassDatatable', diff --git a/app/kubernetes/views/cluster/node/node.html b/app/kubernetes/views/cluster/node/node.html index 9a068fdac..0e7d30b29 100644 --- a/app/kubernetes/views/cluster/node/node.html +++ b/app/kubernetes/views/cluster/node/node.html @@ -256,17 +256,11 @@ -
-
- - -
-
+ + diff --git a/app/react/docker/swarm/NodeView/.keep b/app/react/docker/swarm/NodeView/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/NodeApplicationsDatatable.tsx b/app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/NodeApplicationsDatatable.tsx new file mode 100644 index 000000000..140561bd8 --- /dev/null +++ b/app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/NodeApplicationsDatatable.tsx @@ -0,0 +1,56 @@ +import LaptopCode from '@/assets/ico/laptop-code.svg?c'; + +import { Datatable, TableSettingsMenu } from '@@/datatables'; +import { useRepeater } from '@@/datatables/useRepeater'; +import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh'; +import { useTableStateWithStorage } from '@@/datatables/useTableState'; +import { + BasicTableSettings, + refreshableSettings, + RefreshableTableSettings, +} from '@@/datatables/types'; + +import { useColumns } from './columns'; +import { NodeApplication } from './types'; + +interface TableSettings extends BasicTableSettings, RefreshableTableSettings {} + +export function NodeApplicationsDatatable({ + dataset, + onRefresh, + isLoading, +}: { + dataset: Array; + onRefresh: () => void; + isLoading: boolean; +}) { + const columns = useColumns(true); + const tableState = useTableStateWithStorage( + 'kube-node-apps', + 'Name', + (set) => ({ + ...refreshableSettings(set), + }) + ); + useRepeater(tableState.autoRefreshRate, onRefresh); + + return ( + ( + + + + )} + /> + ); +} diff --git a/app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/columns.helper.tsx b/app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/columns.helper.tsx new file mode 100644 index 000000000..3b0b33b6f --- /dev/null +++ b/app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/columns.helper.tsx @@ -0,0 +1,5 @@ +import { createColumnHelper } from '@tanstack/react-table'; + +import { NodeApplication } from './types'; + +export const helper = createColumnHelper(); diff --git a/app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/columns.name.tsx b/app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/columns.name.tsx new file mode 100644 index 000000000..8e7affa47 --- /dev/null +++ b/app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/columns.name.tsx @@ -0,0 +1,38 @@ +import { CellContext } from '@tanstack/react-table'; + +import { isExternalApplication } from '@/react/kubernetes/applications/utils'; +import { useIsSystemNamespace } from '@/react/kubernetes/namespaces/queries/useIsSystemNamespace'; +import { ExternalBadge } from '@/react/kubernetes/components/ExternalBadge'; +import { SystemBadge } from '@/react/kubernetes/components/SystemBadge'; + +import { Link } from '@@/Link'; + +import { helper } from './columns.helper'; +import { NodeApplication } from './types'; + +export const name = helper.accessor('Name', { + header: 'Name', + cell: Cell, +}); + +function Cell({ + row: { original: item }, +}: CellContext) { + const isSystem = useIsSystemNamespace(item.ResourcePool); + return ( +
+ + {item.Name} + + + {isSystem ? ( + + ) : ( + isExternalApplication({ metadata: item.Metadata }) && + )} +
+ ); +} diff --git a/app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/columns.tsx b/app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/columns.tsx new file mode 100644 index 000000000..807494d75 --- /dev/null +++ b/app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/columns.tsx @@ -0,0 +1,57 @@ +import { useMemo } from 'react'; +import _ from 'lodash'; + +import { humanize, truncate } from '@/portainer/filters/filters'; + +import { Link } from '@@/Link'; + +import { helper } from './columns.helper'; +import { name } from './columns.name'; + +export function useColumns(areStacksVisible: boolean) { + return useMemo( + () => + _.compact([ + name, + areStacksVisible && + helper.accessor('StackName', { + header: 'Stack', + cell: ({ getValue }) => getValue() || '-', + }), + helper.accessor((item) => item.ResourcePool, { + header: 'Namespace', + cell: ({ getValue }) => { + const namespace = getValue(); + return ( + + {namespace} + + ); + }, + }), + helper.accessor('Image', { + header: 'Image', + cell: ({ row: { original: item } }) => ( + <> + {truncate(item.Image, 64)} + {item.Containers?.length > 1 && ( + <>+ {item.Containers.length - 1} + )} + + ), + }), + helper.accessor('CPU', { + header: 'CPU reservation', + cell: ({ getValue }) => _.round(getValue(), 2), + }), + helper.accessor('Memory', { + header: 'Memory reservation', + cell: ({ getValue }) => humanize(getValue()), + }), + ]), + [areStacksVisible] + ); +} diff --git a/app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/types.ts b/app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/types.ts new file mode 100644 index 000000000..7677395fe --- /dev/null +++ b/app/react/kubernetes/cluster/NodeView/NodeApplicationsDatatable/types.ts @@ -0,0 +1,6 @@ +import { KubernetesApplication } from '@/kubernetes/models/application/models'; + +export type NodeApplication = KubernetesApplication & { + CPU: number; + Memory: number; +}; diff --git a/app/react/kubernetes/components/ExternalBadge.tsx b/app/react/kubernetes/components/ExternalBadge.tsx new file mode 100644 index 000000000..cba18f3f1 --- /dev/null +++ b/app/react/kubernetes/components/ExternalBadge.tsx @@ -0,0 +1,5 @@ +import { Badge } from '@@/Badge'; + +export function ExternalBadge() { + return external; +} diff --git a/app/react/kubernetes/components/SystemBadge.tsx b/app/react/kubernetes/components/SystemBadge.tsx new file mode 100644 index 000000000..17552d755 --- /dev/null +++ b/app/react/kubernetes/components/SystemBadge.tsx @@ -0,0 +1,5 @@ +import { Badge } from '@@/Badge'; + +export function SystemBadge() { + return system; +} diff --git a/app/react/kubernetes/namespaces/queries/useIsSystemNamespace.ts b/app/react/kubernetes/namespaces/queries/useIsSystemNamespace.ts new file mode 100644 index 000000000..b076b7aed --- /dev/null +++ b/app/react/kubernetes/namespaces/queries/useIsSystemNamespace.ts @@ -0,0 +1,12 @@ +import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; + +import { useNamespaceQuery } from './useNamespaceQuery'; + +export function useIsSystemNamespace(namespace: string) { + const envId = useEnvironmentId(); + const query = useNamespaceQuery(envId, namespace, { + select: (namespace) => namespace.IsSystem, + }); + + return !!query.data; +} diff --git a/app/react/kubernetes/namespaces/queries/useNamespaceQuery.ts b/app/react/kubernetes/namespaces/queries/useNamespaceQuery.ts index 9e158d870..a0bcebe88 100644 --- a/app/react/kubernetes/namespaces/queries/useNamespaceQuery.ts +++ b/app/react/kubernetes/namespaces/queries/useNamespaceQuery.ts @@ -6,9 +6,14 @@ import { EnvironmentId } from '@/react/portainer/environments/types'; import { DefaultOrSystemNamespace } from '../types'; -export function useNamespaceQuery( +export function useNamespaceQuery( environmentId: EnvironmentId, - namespace: string + namespace: string, + { + select, + }: { + select?(namespace: DefaultOrSystemNamespace): T; + } = {} ) { return useQuery( ['environments', environmentId, 'kubernetes', 'namespaces', namespace], @@ -17,6 +22,7 @@ export function useNamespaceQuery( onError: (err) => { notifyError('Failure', err as Error, 'Unable to get namespace.'); }, + select, } ); }