mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 13:29:41 +02:00
refactor(settings): migrate hidden containers panel to react [EE-5507] (#9119)
This commit is contained in:
parent
eefb4c4287
commit
8b11e1678e
8 changed files with 230 additions and 139 deletions
|
@ -9,6 +9,7 @@ import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||||
import { ApplicationSettingsPanel } from '@/react/portainer/settings/SettingsView/ApplicationSettingsPanel';
|
import { ApplicationSettingsPanel } from '@/react/portainer/settings/SettingsView/ApplicationSettingsPanel';
|
||||||
import { KubeSettingsPanel } from '@/react/portainer/settings/SettingsView/KubeSettingsPanel';
|
import { KubeSettingsPanel } from '@/react/portainer/settings/SettingsView/KubeSettingsPanel';
|
||||||
import { HelmCertPanel } from '@/react/portainer/settings/SettingsView/HelmCertPanel';
|
import { HelmCertPanel } from '@/react/portainer/settings/SettingsView/HelmCertPanel';
|
||||||
|
import { HiddenContainersPanel } from '@/react/portainer/settings/SettingsView/HiddenContainersPanel/HiddenContainersPanel';
|
||||||
|
|
||||||
export const settingsModule = angular
|
export const settingsModule = angular
|
||||||
.module('portainer.app.react.components.settings', [])
|
.module('portainer.app.react.components.settings', [])
|
||||||
|
@ -26,6 +27,10 @@ export const settingsModule = angular
|
||||||
r2a(withReactQuery(ApplicationSettingsPanel), ['onSuccess'])
|
r2a(withReactQuery(ApplicationSettingsPanel), ['onSuccess'])
|
||||||
)
|
)
|
||||||
.component('helmCertPanel', r2a(withReactQuery(HelmCertPanel), []))
|
.component('helmCertPanel', r2a(withReactQuery(HelmCertPanel), []))
|
||||||
|
.component(
|
||||||
|
'hiddenContainersPanel',
|
||||||
|
r2a(withUIRouter(withReactQuery(HiddenContainersPanel)), [])
|
||||||
|
)
|
||||||
.component(
|
.component(
|
||||||
'kubeSettingsPanel',
|
'kubeSettingsPanel',
|
||||||
r2a(withUIRouter(withReactQuery(KubeSettingsPanel)), [])
|
r2a(withUIRouter(withReactQuery(KubeSettingsPanel)), [])
|
||||||
|
|
|
@ -8,65 +8,7 @@
|
||||||
|
|
||||||
<ssl-certificate-settings ng-show="state.showHTTPS"></ssl-certificate-settings>
|
<ssl-certificate-settings ng-show="state.showHTTPS"></ssl-certificate-settings>
|
||||||
|
|
||||||
<div class="row">
|
<hidden-containers-panel></hidden-containers-panel>
|
||||||
<div class="col-sm-12">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-header icon="box" title-text="Hidden containers"></rd-widget-header>
|
|
||||||
<rd-widget-body>
|
|
||||||
<form class="form-horizontal" ng-submit="addFilteredContainerLabel()" name="addTagForm">
|
|
||||||
<div class="form-group">
|
|
||||||
<span class="col-sm-12 text-muted small"> You can hide containers with specific labels from Portainer UI. You need to specify the label name and value. </span>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="header_name" class="col-sm-1 control-label text-left">Name</label>
|
|
||||||
<div class="col-sm-11 col-md-4">
|
|
||||||
<input type="text" required class="form-control" id="header_name" name="label_name" ng-model="formValues.labelName" placeholder="e.g. com.example.foo" />
|
|
||||||
</div>
|
|
||||||
<label for="header_value" class="col-sm-1 control-label text-left">Value</label>
|
|
||||||
<div class="col-sm-11 col-md-4">
|
|
||||||
<input type="text" class="form-control" id="header_value" ng-model="formValues.labelValue" placeholder="e.g. bar" />
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-12 col-md-2">
|
|
||||||
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="!formValues.labelName"><pr-icon icon="'plus'" class-name="'mr-1'"></pr-icon>Add filter</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-12 table-responsive">
|
|
||||||
<table class="table-hover table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Value</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr ng-repeat="label in formValues.BlackListedLabels">
|
|
||||||
<td>{{ label.name }}</td>
|
|
||||||
<td>{{ label.value }}</td>
|
|
||||||
<td>
|
|
||||||
<button type="button" class="btn btn-danger btn-xs" ng-click="removeFilteredContainerLabel($index)">
|
|
||||||
<pr-icon icon="'trash-2'" class-name="'mr-1'"></pr-icon>
|
|
||||||
Remove</button
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-if="formValues.BlackListedLabels.length === 0">
|
|
||||||
<td colspan="3" class="text-muted text-center">No filter available.</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-if="!formValues.BlackListedLabels">
|
|
||||||
<td colspan="3" class="text-muted text-center">Loading...</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !filtered-labels -->
|
|
||||||
</form>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- backup -->
|
<!-- backup -->
|
||||||
<backup-settings-panel></backup-settings-panel>
|
<backup-settings-panel></backup-settings-panel>
|
||||||
|
|
|
@ -1,78 +1,20 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
angular.module('portainer.app').controller('SettingsController', [
|
angular.module('portainer.app').controller('SettingsController', SettingsController);
|
||||||
'$scope',
|
|
||||||
'Notifications',
|
|
||||||
'SettingsService',
|
|
||||||
'StateManager',
|
|
||||||
function ($scope, Notifications, SettingsService, StateManager) {
|
|
||||||
$scope.updateSettings = updateSettings;
|
|
||||||
$scope.handleSuccess = handleSuccess;
|
|
||||||
|
|
||||||
$scope.state = {
|
/* @ngInject */
|
||||||
actionInProgress: false,
|
function SettingsController($scope, StateManager) {
|
||||||
showHTTPS: !window.ddExtension,
|
$scope.handleSuccess = handleSuccess;
|
||||||
};
|
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.state = {
|
||||||
BlackListedLabels: [],
|
showHTTPS: !window.ddExtension,
|
||||||
labelName: '',
|
};
|
||||||
labelValue: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.removeFilteredContainerLabel = function (index) {
|
function handleSuccess(settings) {
|
||||||
const filteredSettings = $scope.formValues.BlackListedLabels.filter((_, i) => i !== index);
|
if (settings) {
|
||||||
const filteredSettingsPayload = { BlackListedLabels: filteredSettings };
|
StateManager.updateLogo(settings.LogoURL);
|
||||||
updateSettings(filteredSettingsPayload, 'Hidden container settings updated');
|
StateManager.updateSnapshotInterval(settings.SnapshotInterval);
|
||||||
};
|
StateManager.updateEnableTelemetry(settings.EnableTelemetry);
|
||||||
|
|
||||||
$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);
|
|
||||||
$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();
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
|
@ -2,10 +2,11 @@ import clsx from 'clsx';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: ReactNode;
|
children: ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
colClassName?: string;
|
colClassName?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
columns?: Array<ReactNode>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DetailsRow({
|
export function DetailsRow({
|
||||||
|
@ -13,17 +14,21 @@ export function DetailsRow({
|
||||||
children,
|
children,
|
||||||
colClassName,
|
colClassName,
|
||||||
className,
|
className,
|
||||||
|
columns,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<tr className={className}>
|
<tr className={className}>
|
||||||
<td className={clsx(colClassName, 'min-w-[150px] !break-normal')}>
|
<td className={clsx(colClassName, 'min-w-[150px] !break-normal')}>
|
||||||
{label}
|
{label}
|
||||||
</td>
|
</td>
|
||||||
{!!children && (
|
<td className={colClassName} data-cy={`detailsTable-${label}Value`}>
|
||||||
<td className={colClassName} data-cy={`detailsTable-${label}Value`}>
|
{children}
|
||||||
{children}
|
</td>
|
||||||
|
{columns?.map((column, index) => (
|
||||||
|
<td key={index} className={colClassName}>
|
||||||
|
{column}
|
||||||
</td>
|
</td>
|
||||||
)}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,22 @@
|
||||||
import { PropsWithChildren } from 'react';
|
import clsx from 'clsx';
|
||||||
|
import { Children, PropsWithChildren } from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
headers?: string[];
|
headers?: string[];
|
||||||
dataCy?: string;
|
dataCy?: string;
|
||||||
|
className?: string;
|
||||||
|
emptyMessage?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DetailsTable({
|
export function DetailsTable({
|
||||||
headers = [],
|
headers = [],
|
||||||
dataCy,
|
dataCy,
|
||||||
|
className,
|
||||||
|
emptyMessage,
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren<Props>) {
|
}: PropsWithChildren<Props>) {
|
||||||
return (
|
return (
|
||||||
<table className="table" data-cy={dataCy}>
|
<table className={clsx('table', className)} data-cy={dataCy}>
|
||||||
{headers.length > 0 && (
|
{headers.length > 0 && (
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -21,7 +26,17 @@ export function DetailsTable({
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
)}
|
)}
|
||||||
{children && <tbody>{children}</tbody>}
|
<tbody>
|
||||||
|
{Children.count(children) > 0 ? (
|
||||||
|
children
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={headers.length} className="text-muted text-center">
|
||||||
|
{emptyMessage}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (
|
||||||
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
validationSchema={validation}
|
||||||
|
key={formKey}
|
||||||
|
>
|
||||||
|
{({ errors, isValid, dirty }) => (
|
||||||
|
<Form className="form-horizontal">
|
||||||
|
<div className="flex w-full items-start gap-4">
|
||||||
|
<FormControl label="Name" errors={errors.name} className="flex-1">
|
||||||
|
<Field
|
||||||
|
as={Input}
|
||||||
|
name="name"
|
||||||
|
placeholder="e.g. com.example.foo"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl label="Value" errors={errors.value} className="flex-1">
|
||||||
|
<Field as={Input} name="value" placeholder="e.g. bar" />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
icon={Plus}
|
||||||
|
disabled={!dirty || !isValid || isLoading}
|
||||||
|
>
|
||||||
|
Add filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(''),
|
||||||
|
});
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<Widget>
|
||||||
|
<Widget.Title icon={Box} title="Hidden containers" />
|
||||||
|
<Widget.Body>
|
||||||
|
<div className="mb-3">
|
||||||
|
<TextTip color="blue">
|
||||||
|
You can hide containers with specific labels from Portainer UI.
|
||||||
|
You need to specify the label name and value.
|
||||||
|
</TextTip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AddLabelForm
|
||||||
|
isLoading={mutation.isLoading}
|
||||||
|
onSubmit={(name, value) =>
|
||||||
|
handleSubmit([...labels, { name, value }])
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<HiddenContainersTable
|
||||||
|
labels={labels}
|
||||||
|
isLoading={mutation.isLoading}
|
||||||
|
onDelete={(name) =>
|
||||||
|
handleSubmit(labels.filter((label) => label.name !== name))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Widget.Body>
|
||||||
|
</Widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleSubmit(labels: Pair[]) {
|
||||||
|
mutation.mutate(
|
||||||
|
{
|
||||||
|
BlackListedLabels: labels,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
notifySuccess('Success', 'Hidden container settings updated');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<DetailsTable
|
||||||
|
headers={['Name', 'Value', '']}
|
||||||
|
className="table-hover"
|
||||||
|
emptyMessage="No filter available."
|
||||||
|
>
|
||||||
|
{labels.map((label, index) => (
|
||||||
|
<DetailsTable.Row
|
||||||
|
key={index}
|
||||||
|
label={label.name}
|
||||||
|
columns={[
|
||||||
|
<Button
|
||||||
|
color="danger"
|
||||||
|
size="xsmall"
|
||||||
|
icon={Trash2}
|
||||||
|
onClick={() => onDelete(label.name)}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{label.value}
|
||||||
|
</DetailsTable.Row>
|
||||||
|
))}
|
||||||
|
</DetailsTable>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue