diff --git a/app/portainer/react/components/settings.ts b/app/portainer/react/components/settings.ts index c40c5af85..ce83d0aa9 100644 --- a/app/portainer/react/components/settings.ts +++ b/app/portainer/react/components/settings.ts @@ -9,6 +9,7 @@ import { withUIRouter } from '@/react-tools/withUIRouter'; import { ApplicationSettingsPanel } from '@/react/portainer/settings/SettingsView/ApplicationSettingsPanel'; import { KubeSettingsPanel } from '@/react/portainer/settings/SettingsView/KubeSettingsPanel'; import { HelmCertPanel } from '@/react/portainer/settings/SettingsView/HelmCertPanel'; +import { HiddenContainersPanel } from '@/react/portainer/settings/SettingsView/HiddenContainersPanel/HiddenContainersPanel'; export const settingsModule = angular .module('portainer.app.react.components.settings', []) @@ -26,6 +27,10 @@ export const settingsModule = angular r2a(withReactQuery(ApplicationSettingsPanel), ['onSuccess']) ) .component('helmCertPanel', r2a(withReactQuery(HelmCertPanel), [])) + .component( + 'hiddenContainersPanel', + r2a(withUIRouter(withReactQuery(HiddenContainersPanel)), []) + ) .component( 'kubeSettingsPanel', r2a(withUIRouter(withReactQuery(KubeSettingsPanel)), []) diff --git a/app/portainer/views/settings/settings.html b/app/portainer/views/settings/settings.html index 100d41599..d1a558ee0 100644 --- a/app/portainer/views/settings/settings.html +++ b/app/portainer/views/settings/settings.html @@ -8,65 +8,7 @@ -
-
- - - -
-
- You can hide containers with specific labels from Portainer UI. You need to specify the label name and value. -
-
- -
- -
- -
- -
-
- -
-
-
-
- - - - - - - - - - - - - - - - - - - - - -
NameValue
{{ label.name }}{{ label.value }} - -
No filter available.
Loading...
-
-
- -
-
-
-
-
+ diff --git a/app/portainer/views/settings/settingsController.js b/app/portainer/views/settings/settingsController.js index 0309640f8..50ca80c51 100644 --- a/app/portainer/views/settings/settingsController.js +++ b/app/portainer/views/settings/settingsController.js @@ -1,78 +1,20 @@ import angular from 'angular'; -angular.module('portainer.app').controller('SettingsController', [ - '$scope', - 'Notifications', - 'SettingsService', - 'StateManager', - function ($scope, Notifications, SettingsService, StateManager) { - $scope.updateSettings = updateSettings; - $scope.handleSuccess = handleSuccess; +angular.module('portainer.app').controller('SettingsController', SettingsController); - $scope.state = { - actionInProgress: false, - showHTTPS: !window.ddExtension, - }; +/* @ngInject */ +function SettingsController($scope, StateManager) { + $scope.handleSuccess = handleSuccess; - $scope.formValues = { - BlackListedLabels: [], - labelName: '', - labelValue: '', - }; + $scope.state = { + showHTTPS: !window.ddExtension, + }; - $scope.removeFilteredContainerLabel = function (index) { - const filteredSettings = $scope.formValues.BlackListedLabels.filter((_, i) => i !== index); - const filteredSettingsPayload = { BlackListedLabels: filteredSettings }; - updateSettings(filteredSettingsPayload, 'Hidden container settings updated'); - }; - - $scope.addFilteredContainerLabel = function () { - var label = { - name: $scope.formValues.labelName, - value: $scope.formValues.labelValue, - }; - - const filteredSettings = [...$scope.formValues.BlackListedLabels, label]; - const filteredSettingsPayload = { BlackListedLabels: filteredSettings }; - updateSettings(filteredSettingsPayload, 'Hidden container settings updated'); - }; - - function updateSettings(settings, successMessage = 'Settings updated') { - return SettingsService.update(settings) - .then(function success(settings) { - Notifications.success('Success', successMessage); - handleSuccess(settings); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to update settings'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); + function handleSuccess(settings) { + if (settings) { + StateManager.updateLogo(settings.LogoURL); + StateManager.updateSnapshotInterval(settings.SnapshotInterval); + StateManager.updateEnableTelemetry(settings.EnableTelemetry); } - - function handleSuccess(settings) { - if (settings) { - StateManager.updateLogo(settings.LogoURL); - StateManager.updateSnapshotInterval(settings.SnapshotInterval); - StateManager.updateEnableTelemetry(settings.EnableTelemetry); - $scope.formValues.BlackListedLabels = settings.BlackListedLabels; - } - } - - function initView() { - SettingsService.settings() - .then(function success(data) { - var settings = data; - $scope.settings = settings; - - $scope.formValues.BlackListedLabels = settings.BlackListedLabels; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve application settings'); - }); - } - - initView(); - }, -]); + } +} diff --git a/app/react/components/DetailsTable/DetailsRow.tsx b/app/react/components/DetailsTable/DetailsRow.tsx index 32331bae2..6633be105 100644 --- a/app/react/components/DetailsTable/DetailsRow.tsx +++ b/app/react/components/DetailsTable/DetailsRow.tsx @@ -2,10 +2,11 @@ import clsx from 'clsx'; import { ReactNode } from 'react'; interface Props { - children?: ReactNode; + children: ReactNode; label: string; colClassName?: string; className?: string; + columns?: Array; } export function DetailsRow({ @@ -13,17 +14,21 @@ export function DetailsRow({ children, colClassName, className, + columns, }: Props) { return ( {label} - {!!children && ( - - {children} + + {children} + + {columns?.map((column, index) => ( + + {column} - )} + ))} ); } diff --git a/app/react/components/DetailsTable/DetailsTable.tsx b/app/react/components/DetailsTable/DetailsTable.tsx index ee7967da6..846d75fc2 100644 --- a/app/react/components/DetailsTable/DetailsTable.tsx +++ b/app/react/components/DetailsTable/DetailsTable.tsx @@ -1,17 +1,22 @@ -import { PropsWithChildren } from 'react'; +import clsx from 'clsx'; +import { Children, PropsWithChildren } from 'react'; type Props = { headers?: string[]; dataCy?: string; + className?: string; + emptyMessage?: string; }; export function DetailsTable({ headers = [], dataCy, + className, + emptyMessage, children, }: PropsWithChildren) { return ( - +
{headers.length > 0 && ( @@ -21,7 +26,17 @@ export function DetailsTable({ )} - {children && {children}} + + {Children.count(children) > 0 ? ( + children + ) : ( + + + + )} +
+ {emptyMessage} +
); } diff --git a/app/react/portainer/settings/SettingsView/HiddenContainersPanel/AddLabelForm.tsx b/app/react/portainer/settings/SettingsView/HiddenContainersPanel/AddLabelForm.tsx new file mode 100644 index 000000000..a568c17c8 --- /dev/null +++ b/app/react/portainer/settings/SettingsView/HiddenContainersPanel/AddLabelForm.tsx @@ -0,0 +1,70 @@ +import { Formik, Form, Field } from 'formik'; +import { Plus } from 'lucide-react'; +import { SchemaOf, object, string } from 'yup'; +import { useReducer } from 'react'; + +import { Button } from '@@/buttons'; +import { FormControl } from '@@/form-components/FormControl'; +import { Input } from '@@/form-components/Input'; + +export function AddLabelForm({ + onSubmit, + isLoading, +}: { + onSubmit: (name: string, value: string) => void; + isLoading: boolean; +}) { + const [formKey, clearForm] = useReducer((state) => state + 1, 0); + + const initialValues = { + name: '', + value: '', + }; + + return ( + + {({ errors, isValid, dirty }) => ( +
+
+ + + + + + + + + +
+
+ )} +
+ ); + + function handleSubmit(values: typeof initialValues) { + clearForm(); + onSubmit(values.name, values.value); + } +} + +function validation(): SchemaOf<{ name: string; value: string }> { + return object({ + name: string().required('Name is required'), + value: string().default(''), + }); +} diff --git a/app/react/portainer/settings/SettingsView/HiddenContainersPanel/HiddenContainersPanel.tsx b/app/react/portainer/settings/SettingsView/HiddenContainersPanel/HiddenContainersPanel.tsx new file mode 100644 index 000000000..84806525c --- /dev/null +++ b/app/react/portainer/settings/SettingsView/HiddenContainersPanel/HiddenContainersPanel.tsx @@ -0,0 +1,68 @@ +import { Box } from 'lucide-react'; + +import { notifySuccess } from '@/portainer/services/notifications'; + +import { TextTip } from '@@/Tip/TextTip'; +import { Widget } from '@@/Widget'; + +import { useSettings, useUpdateSettingsMutation } from '../../queries'; +import { Pair } from '../../types'; + +import { AddLabelForm } from './AddLabelForm'; +import { HiddenContainersTable } from './HiddenContainersTable'; + +export function HiddenContainersPanel() { + const settingsQuery = useSettings((settings) => settings.BlackListedLabels); + const mutation = useUpdateSettingsMutation(); + + if (!settingsQuery.data) { + return null; + } + + const labels = settingsQuery.data; + return ( +
+
+ + + +
+ + You can hide containers with specific labels from Portainer UI. + You need to specify the label name and value. + +
+ + + handleSubmit([...labels, { name, value }]) + } + /> + + + handleSubmit(labels.filter((label) => label.name !== name)) + } + /> +
+
+
+
+ ); + + function handleSubmit(labels: Pair[]) { + mutation.mutate( + { + BlackListedLabels: labels, + }, + { + onSuccess: () => { + notifySuccess('Success', 'Hidden container settings updated'); + }, + } + ); + } +} diff --git a/app/react/portainer/settings/SettingsView/HiddenContainersPanel/HiddenContainersTable.tsx b/app/react/portainer/settings/SettingsView/HiddenContainersPanel/HiddenContainersTable.tsx new file mode 100644 index 000000000..5d3bfe7b4 --- /dev/null +++ b/app/react/portainer/settings/SettingsView/HiddenContainersPanel/HiddenContainersTable.tsx @@ -0,0 +1,44 @@ +import { Trash2 } from 'lucide-react'; + +import { DetailsTable } from '@@/DetailsTable'; +import { Button } from '@@/buttons'; + +import { Pair } from '../../types'; + +export function HiddenContainersTable({ + labels, + isLoading, + onDelete, +}: { + labels: Pair[]; + isLoading: boolean; + onDelete: (name: string) => void; +}) { + return ( + + {labels.map((label, index) => ( + onDelete(label.name)} + disabled={isLoading} + > + Remove + , + ]} + > + {label.value} + + ))} + + ); +}