From 37ece734f0a436b160ad49054107b48bd6c9ec4b Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Sat, 29 Jul 2023 17:08:41 +0200 Subject: [PATCH] refactor(kube/apps): convert placement table to react [EE-4662] (#8938) --- app/kubernetes/filters/application.ts | 33 +++ app/kubernetes/filters/applicationFilters.js | 19 +- .../models/{affinities.js => affinities.ts} | 16 +- app/kubernetes/react/components/index.ts | 5 + .../views/applications/edit/application.html | 10 +- .../placements-datatable/controller.js | 71 ------- .../components/placements-datatable/index.js | 15 -- .../placements-datatable/template.html | 191 ------------------ .../PlacementsDatatable.tsx | 66 ++++++ .../PlacementsDatatableSubRow.tsx | 189 +++++++++++++++++ .../PlacementsDatatable/columns/helper.ts | 5 + .../PlacementsDatatable/columns/index.tsx | 15 ++ .../PlacementsDatatable/columns/status.tsx | 23 +++ .../ItemView/PlacementsDatatable/index.ts | 1 + .../kubernetes/applications/ItemView/types.ts | 29 +++ 15 files changed, 379 insertions(+), 309 deletions(-) create mode 100644 app/kubernetes/filters/application.ts rename app/kubernetes/pod/models/{affinities.js => affinities.ts} (62%) delete mode 100644 app/kubernetes/views/applications/edit/components/placements-datatable/controller.js delete mode 100644 app/kubernetes/views/applications/edit/components/placements-datatable/index.js delete mode 100644 app/kubernetes/views/applications/edit/components/placements-datatable/template.html create mode 100644 app/react/kubernetes/applications/ItemView/PlacementsDatatable/PlacementsDatatable.tsx create mode 100644 app/react/kubernetes/applications/ItemView/PlacementsDatatable/PlacementsDatatableSubRow.tsx create mode 100644 app/react/kubernetes/applications/ItemView/PlacementsDatatable/columns/helper.ts create mode 100644 app/react/kubernetes/applications/ItemView/PlacementsDatatable/columns/index.tsx create mode 100644 app/react/kubernetes/applications/ItemView/PlacementsDatatable/columns/status.tsx create mode 100644 app/react/kubernetes/applications/ItemView/PlacementsDatatable/index.ts create mode 100644 app/react/kubernetes/applications/ItemView/types.ts diff --git a/app/kubernetes/filters/application.ts b/app/kubernetes/filters/application.ts new file mode 100644 index 000000000..1bb7033bf --- /dev/null +++ b/app/kubernetes/filters/application.ts @@ -0,0 +1,33 @@ +import { KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from '../pod/models'; + +export function nodeAffinityValues( + values: string | string[], + operator: KubernetesPodNodeAffinityNodeSelectorRequirementOperators +) { + if ( + operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.IN || + operator === + KubernetesPodNodeAffinityNodeSelectorRequirementOperators.NOT_IN + ) { + return values; + } + + if ( + operator === + KubernetesPodNodeAffinityNodeSelectorRequirementOperators.EXISTS || + operator === + KubernetesPodNodeAffinityNodeSelectorRequirementOperators.DOES_NOT_EXIST + ) { + return ''; + } + + if ( + operator === + KubernetesPodNodeAffinityNodeSelectorRequirementOperators.GREATER_THAN || + operator === + KubernetesPodNodeAffinityNodeSelectorRequirementOperators.LOWER_THAN + ) { + return values[0]; + } + return ''; +} diff --git a/app/kubernetes/filters/applicationFilters.js b/app/kubernetes/filters/applicationFilters.js index 69f9c2979..8b4fe62c0 100644 --- a/app/kubernetes/filters/applicationFilters.js +++ b/app/kubernetes/filters/applicationFilters.js @@ -1,7 +1,7 @@ import _ from 'lodash-es'; import { KubernetesApplicationDataAccessPolicies } from 'Kubernetes/models/application/models'; import { KubernetesApplicationTypes, KubernetesApplicationTypeStrings } from 'Kubernetes/models/application/models'; -import { KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from 'Kubernetes/pod/models'; +import { nodeAffinityValues } from './application'; angular .module('portainer.kubernetes') @@ -65,22 +65,7 @@ angular }) .filter('kubernetesApplicationConstraintNodeAffinityValue', function () { 'use strict'; - return function (values, operator) { - if (operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.IN || operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.NOT_IN) { - return values; - } else if ( - operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.EXISTS || - operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.DOES_NOT_EXIST - ) { - return ''; - } else if ( - operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.GREATER_THAN || - operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.LOWER_THAN - ) { - return values[0]; - } - return ''; - }; + return nodeAffinityValues; }) .filter('kubernetesNodeLabelHumanReadbleText', function () { 'use strict'; diff --git a/app/kubernetes/pod/models/affinities.js b/app/kubernetes/pod/models/affinities.ts similarity index 62% rename from app/kubernetes/pod/models/affinities.js rename to app/kubernetes/pod/models/affinities.ts index 08b38f9d0..d2e2e607f 100644 --- a/app/kubernetes/pod/models/affinities.js +++ b/app/kubernetes/pod/models/affinities.ts @@ -1,11 +1,11 @@ -export const KubernetesPodNodeAffinityNodeSelectorRequirementOperators = Object.freeze({ - IN: 'In', - NOT_IN: 'NotIn', - EXISTS: 'Exists', - DOES_NOT_EXIST: 'DoesNotExist', - GREATER_THAN: 'Gt', - LOWER_THAN: 'Lt', -}); +export enum KubernetesPodNodeAffinityNodeSelectorRequirementOperators { + IN = 'In', + NOT_IN = 'NotIn', + EXISTS = 'Exists', + DOES_NOT_EXIST = 'DoesNotExist', + GREATER_THAN = 'Gt', + LOWER_THAN = 'Lt', +} /** * KubernetesPodAffinity Model diff --git a/app/kubernetes/react/components/index.ts b/app/kubernetes/react/components/index.ts index c45e66eb1..212a9b676 100644 --- a/app/kubernetes/react/components/index.ts +++ b/app/kubernetes/react/components/index.ts @@ -19,6 +19,7 @@ import { import { withUserProvider } from '@/react/test-utils/withUserProvider'; import { withFormValidation } from '@/react-tools/withFormValidation'; import { withCurrentUser } from '@/react-tools/withCurrentUser'; +import { PlacementsDatatable } from '@/react/kubernetes/applications/ItemView/PlacementsDatatable'; export const ngModule = angular .module('portainer.kubernetes.react.components', []) @@ -107,6 +108,10 @@ export const ngModule = angular withUIRouter(withReactQuery(withUserProvider(ApplicationDetailsWidget))), [] ) + ) + .component( + 'kubernetesApplicationPlacementsDatatable', + r2a(withCurrentUser(PlacementsDatatable), ['dataset', 'onRefresh']) ); export const componentsModule = ngModule.name; diff --git a/app/kubernetes/views/applications/edit/application.html b/app/kubernetes/views/applications/edit/application.html index 32bd244c3..c6996653d 100644 --- a/app/kubernetes/views/applications/edit/application.html +++ b/app/kubernetes/views/applications/edit/application.html @@ -40,15 +40,11 @@ The placement component helps you understand whether or not this application can be deployed on a specific node. + diff --git a/app/kubernetes/views/applications/edit/components/placements-datatable/controller.js b/app/kubernetes/views/applications/edit/components/placements-datatable/controller.js deleted file mode 100644 index ea550309a..000000000 --- a/app/kubernetes/views/applications/edit/components/placements-datatable/controller.js +++ /dev/null @@ -1,71 +0,0 @@ -import _ from 'lodash-es'; - -angular.module('portainer.docker').controller('KubernetesApplicationPlacementsDatatableController', function ($scope, $controller, DatatableService, Authentication) { - angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); - this.state = Object.assign(this.state, { - expandedItems: [], - expandAll: false, - }); - - this.expandItem = function (item, expanded) { - if (!this.itemCanExpand(item)) { - return; - } - - item.Expanded = expanded; - if (!expanded) { - item.Highlighted = false; - } - }; - - this.itemCanExpand = function (item) { - return !item.AcceptsApplication; - }; - - this.hasExpandableItems = function () { - return _.filter(this.state.filteredDataSet, (item) => this.itemCanExpand(item)).length; - }; - - this.expandAll = function () { - this.state.expandAll = !this.state.expandAll; - _.forEach(this.state.filteredDataSet, (item) => { - if (this.itemCanExpand(item)) { - this.expandItem(item, this.state.expandAll); - } - }); - }; - - 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/views/applications/edit/components/placements-datatable/index.js b/app/kubernetes/views/applications/edit/components/placements-datatable/index.js deleted file mode 100644 index 21a02d6f3..000000000 --- a/app/kubernetes/views/applications/edit/components/placements-datatable/index.js +++ /dev/null @@ -1,15 +0,0 @@ -angular.module('portainer.kubernetes').component('kubernetesApplicationPlacementsDatatable', { - templateUrl: './template.html', - controller: 'KubernetesApplicationPlacementsDatatableController', - bindings: { - titleText: '@', - titleIcon: '@', - dataset: '<', - tableKey: '@', - orderBy: '@', - reverseOrder: '<', - refreshCallback: '<', - loading: '<', - removeAction: '<', - }, -}); diff --git a/app/kubernetes/views/applications/edit/components/placements-datatable/template.html b/app/kubernetes/views/applications/edit/components/placements-datatable/template.html deleted file mode 100644 index 3ef0d33ef..000000000 --- a/app/kubernetes/views/applications/edit/components/placements-datatable/template.html +++ /dev/null @@ -1,191 +0,0 @@ -
-
-
-
- -
- - {{ $ctrl.titleText }} - -
- -
- - - - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - -
- - - - - - - - {{ item.Name }} -
- This application is missing a toleration for the taint {{ taint.Key }}{{ taint.Value ? '=' + taint.Value : '' }}:{{ taint.Effect }} -
Placement constraint not respected for that node.
- This application can only be scheduled on a node where the label {{ label.key }} is set to {{ label.value }} -
Placement label not respected for that node.
This application can only be scheduled on nodes respecting one of the following labels combination:
- - {{ term.key }} {{ term.operator }} {{ term.values | kubernetesApplicationConstraintNodeAffinityValue : term.operator }} - - {{ $last ? '' : ' + ' }} -
Loading...
No node available.
-
- -
diff --git a/app/react/kubernetes/applications/ItemView/PlacementsDatatable/PlacementsDatatable.tsx b/app/react/kubernetes/applications/ItemView/PlacementsDatatable/PlacementsDatatable.tsx new file mode 100644 index 000000000..e9bd8e493 --- /dev/null +++ b/app/react/kubernetes/applications/ItemView/PlacementsDatatable/PlacementsDatatable.tsx @@ -0,0 +1,66 @@ +import { Minimize2 } from 'lucide-react'; + +import { + BasicTableSettings, + createPersistedStore, + refreshableSettings, + RefreshableTableSettings, +} from '@@/datatables/types'; +import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable'; +import { useRepeater } from '@@/datatables/useRepeater'; +import { TableSettingsMenu } from '@@/datatables'; +import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh'; +import { useTableState } from '@@/datatables/useTableState'; + +import { Node } from '../types'; + +import { SubRow } from './PlacementsDatatableSubRow'; +import { columns } from './columns'; + +interface TableSettings extends BasicTableSettings, RefreshableTableSettings {} + +function createStore(storageKey: string) { + return createPersistedStore(storageKey, 'node', (set) => ({ + ...refreshableSettings(set), + })); +} + +const storageKey = 'kubernetes.application.placements'; +const settingsStore = createStore(storageKey); + +export function PlacementsDatatable({ + dataset, + onRefresh, +}: { + dataset: Node[]; + onRefresh: () => Promise; +}) { + const tableState = useTableState(settingsStore, storageKey); + + useRepeater(tableState.autoRefreshRate, onRefresh); + + return ( + !row.original.AcceptsApplication} + title="Placement constraints/preferences" + titleIcon={Minimize2} + dataset={dataset} + settingsManager={tableState} + columns={columns} + disableSelect + noWidget + renderTableSettings={() => ( + + + + )} + emptyContentLabel="No node available." + renderSubRow={(row) => ( + + )} + /> + ); +} diff --git a/app/react/kubernetes/applications/ItemView/PlacementsDatatable/PlacementsDatatableSubRow.tsx b/app/react/kubernetes/applications/ItemView/PlacementsDatatable/PlacementsDatatableSubRow.tsx new file mode 100644 index 000000000..8e86563e2 --- /dev/null +++ b/app/react/kubernetes/applications/ItemView/PlacementsDatatable/PlacementsDatatableSubRow.tsx @@ -0,0 +1,189 @@ +import clsx from 'clsx'; +import { Fragment } from 'react'; + +import { nodeAffinityValues } from '@/kubernetes/filters/application'; +import { useAuthorizations } from '@/react/hooks/useUser'; + +import { Affinity, Label, Node, Taint } from '../types'; + +interface SubRowProps { + node: Node; + cellCount: number; +} + +export function SubRow({ node, cellCount }: SubRowProps) { + const authorized = useAuthorizations( + 'K8sApplicationErrorDetailsR', + undefined, + true + ); + + if (!authorized) { + <> + {isDefined(node.UnmetTaints) && ( + + + Placement constraint not respected for that node. + + + )} + + {(isDefined(node.UnmatchedNodeSelectorLabels) || + isDefined(node.UnmatchedNodeAffinities)) && ( + + + Placement label not respected for that node. + + + )} + ; + } + + return ( + <> + {isDefined(node.UnmetTaints) && ( + + )} + {isDefined(node.UnmatchedNodeSelectorLabels) && ( + + )} + {isDefined(node.UnmatchedNodeAffinities) && ( + + )} + + ); +} + +function isDefined(arr?: Array): arr is Array { + return !!arr && arr.length > 0; +} + +function UnmetTaintsInfo({ + taints, + isHighlighted, + cellCount, +}: { + taints: Array; + isHighlighted: boolean; + cellCount: number; +}) { + return ( + <> + {taints.map((taint) => ( + + + This application is missing a toleration for the taint + + {taint.Key} + {taint.Value ? `=${taint.Value}` : ''}:{taint.Effect} + + + + ))} + + ); +} + +function UnmatchedLabelsInfo({ + labels, + isHighlighted, + cellCount, +}: { + labels: Array