diff --git a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html index 42c163087..13bec8dc9 100644 --- a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html +++ b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html @@ -1,7 +1,7 @@
-
-
+
+
@@ -109,12 +109,17 @@ data-cy="k8sApp-searchApplicationsInput" />
- -
+
+
+
+
+
- - Namespace +
+ + Namespace +
- -
-
- - -
-
- - - - - -
-
-
-
- Close -
-
-
- -
-
- - -
-
- - - Namespace - - -
-
-
-
- -
- - - System resources are hidden, this can be changed in the table settings. - -
-
-
- -
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - -
- - -
-
-
- - - - - - - -
-
- - - - -
- - -
-
-
- {{ item.Name }} - - {{ item.ResourcePool }} - system - {{ item.Applications.length }} - - - Logs - -
- {{ app.Name }} - external -
Loading...
No stack available.
-
- - diff --git a/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.js b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.js deleted file mode 100644 index f016de251..000000000 --- a/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.js +++ /dev/null @@ -1,20 +0,0 @@ -angular.module('portainer.kubernetes').component('kubernetesApplicationsStacksDatatable', { - templateUrl: './applicationsStacksDatatable.html', - controller: 'KubernetesApplicationsStacksDatatableController', - bindings: { - titleText: '@', - titleIcon: '@', - dataset: '<', - tableKey: '@', - orderBy: '@', - reverseOrder: '<', - refreshCallback: '<', - removeAction: '<', - namespaces: '<', - namespace: '<', - onChangeNamespaceDropdown: '<', - isAppsLoading: '<', - isSystemResources: '<', - setSystemResources: '<', - }, -}); diff --git a/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatableController.js b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatableController.js deleted file mode 100644 index 27bef3600..000000000 --- a/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatableController.js +++ /dev/null @@ -1,165 +0,0 @@ -import _ from 'lodash-es'; -import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models'; -import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; -import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper'; - -angular.module('portainer.kubernetes').controller('KubernetesApplicationsStacksDatatableController', [ - '$scope', - '$controller', - 'DatatableService', - 'Authentication', - function ($scope, $controller, DatatableService, Authentication) { - angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); - this.state = Object.assign(this.state, { - expandedItems: [], - expandAll: false, - namespace: '', - namespaces: [], - }); - - var ctrl = this; - - this.settings = Object.assign(this.settings, { - showSystem: false, - }); - - this.onSettingsShowSystemChange = function () { - this.updateNamespace(); - this.setSystemResources(this.settings.showSystem); - DatatableService.setDataTableSettings(this.tableKey, this.settings); - }; - - this.isExternalApplication = function (item) { - return KubernetesApplicationHelper.isExternalApplication(item); - }; - - /** - * Do not allow applications in system namespaces to be selected - */ - this.allowSelection = function (item) { - return !this.isSystemNamespace(item.ResourcePool); - }; - - /** - * @param {String} namespace Namespace (string name) - * @returns Boolean - */ - this.isSystemNamespace = function (namespace) { - return KubernetesNamespaceHelper.isSystemNamespace(namespace); - }; - - this.isDisplayed = function (item) { - return !ctrl.isSystemNamespace(item.ResourcePool) || ctrl.settings.showSystem; - }; - - this.expandItem = function (item, expanded) { - if (!this.itemCanExpand(item)) { - return; - } - - item.Expanded = expanded; - if (!expanded) { - item.Highlighted = false; - } - }; - - this.itemCanExpand = function (item) { - return item.Applications.length > 0; - }; - - 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.onChangeNamespace = function () { - this.onChangeNamespaceDropdown(this.state.namespace); - }; - - this.updateNamespace = function () { - if (this.namespaces) { - const namespaces = [{ Name: 'All namespaces', Value: '', IsSystem: false }]; - this.namespaces.find((ns) => { - if (!this.settings.showSystem && ns.IsSystem) { - return false; - } - namespaces.push({ Name: ns.Name, Value: ns.Name, IsSystem: ns.IsSystem }); - }); - this.state.namespaces = namespaces; - - if (this.state.namespace && !this.state.namespaces.find((ns) => ns.Name === this.state.namespace)) { - if (this.state.namespaces.length > 1) { - let defaultNS = this.state.namespaces.find((ns) => ns.Name === 'default'); - defaultNS = defaultNS || this.state.namespaces[1]; - this.state.namespace = defaultNS.Value; - } else { - this.state.namespace = this.state.namespaces[0].Value; - } - this.onChangeNamespaceDropdown(this.state.namespace); - } - } - }; - - this.$onChanges = function () { - if (typeof this.isSystemResources !== 'undefined') { - this.settings.showSystem = this.isSystemResources; - DatatableService.setDataTableSettings(this.settingsKey, this.settings); - } - this.state.namespace = this.namespace; - this.updateNamespace(); - this.prepareTableFromDataset(); - }; - - this.$onInit = function () { - this.isAdmin = Authentication.isAdmin(); - 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.settingsKey); - if (storedSettings !== null) { - this.settings = storedSettings; - this.settings.open = false; - - this.setSystemResources && this.setSystemResources(this.settings.showSystem); - } - - // Set the default selected namespace - if (!this.state.namespace) { - this.state.namespace = this.namespace; - } - - this.updateNamespace(); - this.onSettingsRepeaterChange(); - }; - }, -]); diff --git a/app/kubernetes/react/components/index.ts b/app/kubernetes/react/components/index.ts index 9cf2af1fd..140a254b8 100644 --- a/app/kubernetes/react/components/index.ts +++ b/app/kubernetes/react/components/index.ts @@ -21,6 +21,7 @@ import { ApplicationContainersDatatable } from '@/react/kubernetes/applications/ import { withFormValidation } from '@/react-tools/withFormValidation'; import { withCurrentUser } from '@/react-tools/withCurrentUser'; import { YAMLInspector } from '@/react/kubernetes/components/YAMLInspector'; +import { ApplicationsStacksDatatable } from '@/react/kubernetes/applications/ListView/ApplicationsStacksDatatable'; export const ngModule = angular .module('portainer.kubernetes.react.components', []) @@ -134,6 +135,18 @@ export const ngModule = angular withUIRouter(withReactQuery(withCurrentUser(ApplicationEventsDatatable))), [] ) + ) + .component( + 'kubernetesApplicationsStacksDatatable', + r2a(withUIRouter(withCurrentUser(ApplicationsStacksDatatable)), [ + 'dataset', + 'onRefresh', + 'onRemove', + 'namespace', + 'namespaces', + 'onNamespaceChange', + 'isLoading', + ]) ); export const componentsModule = ngModule.name; diff --git a/app/kubernetes/views/applications/applications.html b/app/kubernetes/views/applications/applications.html index a6aa1e02b..67805ed38 100644 --- a/app/kubernetes/views/applications/applications.html +++ b/app/kubernetes/views/applications/applications.html @@ -30,18 +30,15 @@ Stacks + diff --git a/app/kubernetes/views/applications/applicationsController.js b/app/kubernetes/views/applications/applicationsController.js index 8b3aea018..9010118a3 100644 --- a/app/kubernetes/views/applications/applicationsController.js +++ b/app/kubernetes/views/applications/applicationsController.js @@ -144,10 +144,12 @@ class KubernetesApplicationsController { } onChangeNamespaceDropdown(namespaceName) { - this.state.namespaceName = namespaceName; - // save the selected namespaceName in local storage with the key 'kubernetes_namespace_filter_${environmentId}_${userID}' - this.LocalStorage.storeNamespaceFilter(this.endpoint.Id, this.user.ID, namespaceName); - return this.$async(this.getApplicationsAsync); + return this.$async(async () => { + this.state.namespaceName = namespaceName; + // save the selected namespaceName in local storage with the key 'kubernetes_namespace_filter_${environmentId}_${userID}' + this.LocalStorage.storeNamespaceFilter(this.endpoint.Id, this.user.ID, namespaceName); + return this.getApplicationsAsync(); + }); } async getApplicationsAsync() { diff --git a/app/react/components/datatables/DatatableHeader.tsx b/app/react/components/datatables/DatatableHeader.tsx index a3a6f1265..117ad58e4 100644 --- a/app/react/components/datatables/DatatableHeader.tsx +++ b/app/react/components/datatables/DatatableHeader.tsx @@ -28,15 +28,19 @@ export function DatatableHeader({ return null; } + const searchBar = ; + const tableActions = !!renderTableActions && ( + {renderTableActions()} + ); + const tableTitleSettings = !!renderTableSettings && ( + {renderTableSettings()} + ); + return ( - - {renderTableActions && ( - {renderTableActions()} - )} - - {!!renderTableSettings && renderTableSettings()} - + {searchBar} + {tableActions} + {tableTitleSettings} ); } diff --git a/app/react/components/datatables/ExpandableDatatableRow.tsx b/app/react/components/datatables/ExpandableDatatableRow.tsx index bcf9da237..5a9e85353 100644 --- a/app/react/components/datatables/ExpandableDatatableRow.tsx +++ b/app/react/components/datatables/ExpandableDatatableRow.tsx @@ -6,14 +6,12 @@ import { DefaultType } from './types'; interface Props { row: Row; - disableSelect?: boolean; renderSubRow(row: Row): ReactNode; expandOnClick?: boolean; } export function ExpandableDatatableTableRow({ row, - disableSelect, renderSubRow, expandOnClick, }: Props) { @@ -25,14 +23,7 @@ export function ExpandableDatatableTableRow({ cells={cells} onClick={expandOnClick ? () => row.toggleExpanded() : undefined} /> - {row.getIsExpanded() && row.getCanExpand() && ( - - {!disableSelect && } - - {renderSubRow(row)} - - - )} + {row.getIsExpanded() && renderSubRow(row)} ); } diff --git a/app/react/components/form-components/ReactSelect.css b/app/react/components/form-components/ReactSelect.css index bd7a9bb41..e0ea54b70 100644 --- a/app/react/components/form-components/ReactSelect.css +++ b/app/react/components/form-components/ReactSelect.css @@ -89,6 +89,16 @@ min-height: 34px; } +.input-group .portainer-selector-root .portainer-selector__control:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.input-group .portainer-selector-root .portainer-selector__control:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + .portainer-selector-root .portainer-selector__multi-value { background-color: var(--multi-value-tag-bg); } diff --git a/app/react/kubernetes/applications/ListView/.keep b/app/react/kubernetes/applications/ListView/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/ApplicationsStacksDatatable.tsx b/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/ApplicationsStacksDatatable.tsx new file mode 100644 index 000000000..730abfdd2 --- /dev/null +++ b/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/ApplicationsStacksDatatable.tsx @@ -0,0 +1,105 @@ +import { List } from 'lucide-react'; + +import { useAuthorizations } from '@/react/hooks/useUser'; +import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription'; +import { systemResourcesSettings } from '@/react/kubernetes/datatables/SystemResourcesSettings'; + +import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable'; +import { createPersistedStore, refreshableSettings } from '@@/datatables/types'; +import { useRepeater } from '@@/datatables/useRepeater'; +import { useTableState } from '@@/datatables/useTableState'; +import { InsightsBox } from '@@/InsightsBox'; + +import { KubernetesStack } from '../../types'; + +import { columns } from './columns'; +import { SubRows } from './SubRows'; +import { Namespace, TableSettings } from './types'; +import { StacksSettingsMenu } from './StacksSettingsMenu'; +import { NamespaceFilter } from './NamespaceFilter'; +import { TableActions } from './TableActions'; + +const storageKey = 'kubernetes.applications.stacks'; + +const settingsStore = createPersistedStore( + storageKey, + 'name', + (set) => ({ + ...systemResourcesSettings(set), + ...refreshableSettings(set), + }) +); + +interface Props { + dataset: Array; + onRemove(selectedItems: Array): void; + onRefresh(): Promise; + namespace?: string; + namespaces: Array; + onNamespaceChange(namespace: string): void; + isLoading?: boolean; +} + +export function ApplicationsStacksDatatable({ + dataset, + onRemove, + onRefresh, + namespace = '', + namespaces, + onNamespaceChange, + isLoading, +}: Props) { + const tableState = useTableState(settingsStore, storageKey); + + const authorized = useAuthorizations('K8sApplicationsW'); + useRepeater(tableState.autoRefreshRate, onRefresh); + + return ( + row.original.Applications.length > 0} + title="Stacks" + titleIcon={List} + dataset={dataset} + isLoading={isLoading} + columns={columns} + settingsManager={tableState} + disableSelect={!authorized} + renderSubRow={(row) => ( + + )} + noWidget + emptyContentLabel="No stack available." + description={ +
+
+ +
+ +
+ + +
+ +
+
+
+ } + renderTableActions={(selectedItems) => ( + + )} + renderTableSettings={() => } + getRowId={(row) => `${row.Name}-${row.ResourcePool}`} + /> + ); +} diff --git a/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/NamespaceFilter.tsx b/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/NamespaceFilter.tsx new file mode 100644 index 000000000..ffa9b67f9 --- /dev/null +++ b/app/react/kubernetes/applications/ListView/ApplicationsStacksDatatable/NamespaceFilter.tsx @@ -0,0 +1,61 @@ +import { Filter } from 'lucide-react'; +import { useEffect } from 'react'; + +import { Icon } from '@@/Icon'; +import { Select } from '@@/form-components/Input'; +import { InputGroup } from '@@/form-components/InputGroup'; + +import { Namespace } from './types'; + +function transformNamespaces(namespaces: Namespace[], showSystem: boolean) { + return namespaces + .filter((ns) => showSystem || !ns.IsSystem) + .map(({ Name, IsSystem }) => ({ + label: IsSystem ? `${Name} - system` : Name, + value: Name, + })); +} + +export function NamespaceFilter({ + namespaces, + value, + onChange, + showSystem, +}: { + namespaces: Namespace[]; + value: string; + onChange: (value: string) => void; + showSystem: boolean; +}) { + const transformedNamespaces = transformNamespaces(namespaces, showSystem); + + // sync value with displayed namespaces + useEffect(() => { + const names = transformedNamespaces.map((ns) => ns.value); + if (value && !names.find((ns) => ns === value)) { + onChange( + names.length > 0 ? names.find((ns) => ns === 'default') || names[0] : '' + ); + } + }, [value, onChange, transformedNamespaces]); + + return ( + + +
+ + Namespace +
+
+