Chaim Lev-Ari 2025-04-17 14:12:46 +00:00 committed by GitHub
commit e25d480db9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 171 additions and 110 deletions

View File

@ -75,9 +75,7 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
url: '/configs',
views: {
'content@': {
templateUrl: './views/configs/configs.html',
controller: 'ConfigsController',
controllerAs: 'ctrl',
component: 'configsListView',
},
},
data: {

View File

@ -13,7 +13,6 @@ import { InsightsBox } from '@/react/components/InsightsBox';
import { BetaAlert } from '@/react/portainer/environments/update-schedules/common/BetaAlert';
import { ImagesDatatable } from '@/react/docker/images/ListView/ImagesDatatable/ImagesDatatable';
import { EventsDatatable } from '@/react/docker/events/EventsDatatables';
import { ConfigsDatatable } from '@/react/docker/configs/ListView/ConfigsDatatable';
import { AgentHostBrowser } from '@/react/docker/host/BrowseView/AgentHostBrowser';
import { AgentVolumeBrowser } from '@/react/docker/volumes/BrowseView/AgentVolumeBrowser';
import { ProcessesDatatable } from '@/react/docker/containers/StatsView/ProcessesDatatable';
@ -79,14 +78,7 @@ const ngModule = angular
'onRemove',
])
)
.component(
'dockerConfigsDatatable',
r2a(withUIRouter(withCurrentUser(ConfigsDatatable)), [
'dataset',
'onRemoveClick',
'onRefresh',
])
)
.component(
'agentHostBrowserReact',
r2a(withUIRouter(withCurrentUser(AgentHostBrowser)), [

View File

@ -0,0 +1,14 @@
import angular from 'angular';
import { r2a } from '@/react-tools/react2angular';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { withReactQuery } from '@/react-tools/withReactQuery';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { ListView } from '@/react/docker/configs/ListView/ListView';
export const configsModule = angular
.module('portainer.docker.react.views.configs', [])
.component(
'configsListView',
r2a(withUIRouter(withReactQuery(withCurrentUser(ListView))), [])
).name;

View File

@ -8,9 +8,10 @@ import { DashboardView } from '@/react/docker/DashboardView/DashboardView';
import { ListView } from '@/react/docker/events/ListView';
import { containersModule } from './containers';
import { configsModule } from './configs';
export const viewsModule = angular
.module('portainer.docker.react.views', [containersModule])
.module('portainer.docker.react.views', [containersModule, configsModule])
.component(
'dockerDashboardView',
r2a(withUIRouter(withCurrentUser(DashboardView)), [])

View File

@ -3,7 +3,7 @@ import { getConfigs } from '@/react/docker/configs/queries/useConfigs';
import { deleteConfig } from '@/react/docker/configs/queries/useDeleteConfigMutation';
import { createConfig } from '@/react/docker/configs/queries/useCreateConfigMutation';
import { ConfigViewModel } from '../models/config';
import { ConfigViewModel } from '../../react/docker/configs/model';
angular.module('portainer.docker').factory('ConfigService', ConfigServiceFactory);

View File

@ -1,3 +0,0 @@
<page-header title="'Configs list'" breadcrumbs="['Configs']" reload="true"> </page-header>
<docker-configs-datatable dataset="ctrl.configs" on-remove-click="(ctrl.removeAction)" on-refresh="(ctrl.getConfigs)"></docker-configs-datatable>

View File

@ -1,59 +0,0 @@
import angular from 'angular';
class ConfigsController {
/* @ngInject */
constructor($state, ConfigService, Notifications, $async, endpoint) {
this.$state = $state;
this.ConfigService = ConfigService;
this.Notifications = Notifications;
this.$async = $async;
this.endpoint = endpoint;
this.removeAction = this.removeAction.bind(this);
this.removeActionAsync = this.removeActionAsync.bind(this);
this.getConfigs = this.getConfigs.bind(this);
this.getConfigsAsync = this.getConfigsAsync.bind(this);
}
getConfigs() {
return this.$async(this.getConfigsAsync);
}
async getConfigsAsync() {
try {
this.configs = await this.ConfigService.configs(this.endpoint.Id);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve configs');
}
}
async $onInit() {
this.configs = [];
this.getConfigs();
}
async removeAction(selectedItems) {
return this.$async(this.removeActionAsync, selectedItems);
}
async removeActionAsync(selectedItems) {
let actionCount = selectedItems.length;
for (const config of selectedItems) {
try {
await this.ConfigService.remove(this.endpoint.Id, config.Id);
this.Notifications.success('Config successfully removed', config.Name);
const index = this.configs.indexOf(config);
this.configs.splice(index, 1);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to remove config');
} finally {
--actionCount;
if (actionCount === 0) {
this.$state.reload();
}
}
}
}
}
export default ConfigsController;
angular.module('portainer.docker').controller('ConfigsController', ConfigsController);

View File

@ -1,38 +1,42 @@
import { Clipboard } from 'lucide-react';
import { Authorized, useAuthorizations } from '@/react/hooks/useUser';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { Datatable, TableSettingsMenu } from '@@/datatables';
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
import { useRepeater } from '@@/datatables/useRepeater';
import { AddButton } from '@@/buttons';
import { useTableState } from '@@/datatables/useTableState';
import { DeleteButton } from '@@/buttons/DeleteButton';
import { DockerConfig } from '../../types';
import { useConfigsList } from '../queries/useConfigsList';
import { columns } from './columns';
import { createStore } from './store';
interface Props {
dataset: Array<DockerConfig>;
onRemoveClick: (configs: Array<DockerConfig>) => void;
onRefresh: () => void;
}
import { DeleteConfigButton } from './DeleteConfigButton';
const storageKey = 'docker_configs';
const settingsStore = createStore(storageKey);
export function ConfigsDatatable({ dataset, onRefresh, onRemoveClick }: Props) {
export function ConfigsDatatable() {
const environmentId = useEnvironmentId();
const tableState = useTableState(settingsStore, storageKey);
useRepeater(tableState.autoRefreshRate, onRefresh);
const configListQuery = useConfigsList(environmentId, {
refetchInterval: tableState.autoRefreshRate * 1000,
});
const hasWriteAccessQuery = useAuthorizations([
'DockerConfigCreate',
'DockerConfigDelete',
]);
if (!configListQuery.data) {
return null;
}
const dataset = configListQuery.data;
return (
<Datatable
dataset={dataset}
@ -54,12 +58,7 @@ export function ConfigsDatatable({ dataset, onRefresh, onRemoveClick }: Props) {
hasWriteAccessQuery.authorized && (
<div className="flex items-center gap-3">
<Authorized authorizations="DockerConfigDelete">
<DeleteButton
disabled={selectedRows.length === 0}
data-cy="remove-docker-configs-button"
onConfirmed={() => onRemoveClick(selectedRows)}
confirmMessage="Do you want to remove the selected config(s)?"
/>
<DeleteConfigButton selectedItems={selectedRows} />
</Authorized>
<Authorized authorizations="DockerConfigCreate">

View File

@ -0,0 +1,44 @@
import { useQueryClient, useMutation } from '@tanstack/react-query';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { promiseSequence } from '@/portainer/helpers/promise-utils';
import { withError, withInvalidate } from '@/react-tools/react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { DeleteButton } from '@@/buttons/DeleteButton';
import { ConfigViewModel } from '../../model';
import { queryKeys } from '../queries/queryKeys';
import { deleteConfig } from '../queries/useDeleteConfigMutation';
export function DeleteConfigButton({
selectedItems,
}: {
selectedItems: Array<ConfigViewModel>;
}) {
const environmentId = useEnvironmentId();
const mutation = useDeleteConfigListMutation(environmentId);
return (
<DeleteButton
data-cy="remove-docker-configs-button"
onConfirmed={() => {
mutation.mutate(selectedItems.map((item) => item.Id));
}}
confirmMessage="Do you want to remove the selected config(s)?"
disabled={selectedItems.length === 0}
/>
);
}
function useDeleteConfigListMutation(environmentId: EnvironmentId) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (ids: Array<string>) =>
promiseSequence(
ids.map((configId) => () => deleteConfig({ environmentId, configId }))
),
...withError('Unable to remove configs'),
...withInvalidate(queryClient, [queryKeys.base(environmentId)]),
});
}

View File

@ -5,12 +5,12 @@ import { createOwnershipColumn } from '@/react/docker/components/datatable/creat
import { buildNameColumn } from '@@/datatables/buildNameColumn';
import { DockerConfig } from '../../types';
import { ConfigViewModel } from '../../model';
const columnHelper = createColumnHelper<DockerConfig>();
const columnHelper = createColumnHelper<ConfigViewModel>();
export const columns = [
buildNameColumn<DockerConfig>(
buildNameColumn<ConfigViewModel>(
'Name',
'docker.configs.config',
'docker-configs-name'
@ -22,5 +22,5 @@ export const columns = [
return <time dateTime={date}>{isoDate(date)}</time>;
},
}),
createOwnershipColumn<DockerConfig>(),
createOwnershipColumn<ConfigViewModel>(),
];

View File

@ -0,0 +1,13 @@
import { PageHeader } from '@@/PageHeader';
import { ConfigsDatatable } from './ConfigsDatatable';
export function ListView() {
return (
<>
<PageHeader title="Configs list" breadcrumbs="Configs" reload />
<ConfigsDatatable />
</>
);
}

View File

@ -0,0 +1,6 @@
import { EnvironmentId } from '@/react/portainer/environments/types';
import { buildDockerProxyUrl } from '@/react/docker/proxy/queries/buildDockerProxyUrl';
export function buildUrl(environmentId: EnvironmentId, id = '', action = '') {
return buildDockerProxyUrl(environmentId, 'configs', id, action);
}

View File

@ -0,0 +1,8 @@
import { queryKeys as proxyQueryKeys } from '@/react/docker/proxy/queries/query-keys';
import { EnvironmentId } from '@/react/portainer/environments/types';
export const queryKeys = {
base: (environmentId: EnvironmentId) =>
[...proxyQueryKeys.base(environmentId), 'configs'] as const,
list: (environmentId: EnvironmentId) => queryKeys.base(environmentId),
};

View File

@ -0,0 +1,29 @@
import { useQuery } from '@tanstack/react-query';
import { Config } from 'docker-types/generated/1.41';
import { EnvironmentId } from '@/react/portainer/environments/types';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { ConfigViewModel } from '@/react/docker/configs/model';
import { queryKeys } from './queryKeys';
import { buildUrl } from './build-url';
export function useConfigsList(
environmentId: EnvironmentId,
{ refetchInterval }: { refetchInterval?: number } = {}
) {
return useQuery({
queryKey: queryKeys.list(environmentId),
queryFn: () => getConfigsList(environmentId),
refetchInterval,
});
}
async function getConfigsList(environmentId: EnvironmentId) {
try {
const { data } = await axios.get<Array<Config>>(buildUrl(environmentId));
return data.map((c) => new ConfigViewModel(c));
} catch (err) {
throw parseAxiosError(err as Error, 'Unable to retrieve configs');
}
}

View File

@ -0,0 +1,26 @@
import { useMutation } from '@tanstack/react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { buildUrl } from './build-url';
export function useDeleteConfigMutation() {
return useMutation({
mutationFn: deleteConfig,
});
}
export async function deleteConfig({
environmentId,
configId,
}: {
environmentId: EnvironmentId;
configId: string;
}) {
try {
await axios.delete(buildUrl(environmentId, configId));
} catch (err) {
throw parseAxiosError(err as Error, 'Unable to delete config');
}
}

View File

@ -4,14 +4,14 @@ import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { buildDockerProxyUrl } from '../../proxy/queries/buildDockerProxyUrl';
import { DockerConfig } from '../types';
import { PortainerResponse } from '../../types';
export async function getConfig(
environmentId: EnvironmentId,
configId: DockerConfig['Id']
configId: Config['ID']
) {
try {
const { data } = await axios.get<Config>(
const { data } = await axios.get<PortainerResponse<Config>>(
buildDockerProxyUrl(environmentId, 'configs', configId)
);
return data;

View File

@ -1,12 +1,13 @@
import { Config } from 'docker-types/generated/1.41';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { DockerConfig } from '../types';
import { buildDockerProxyUrl } from '../../proxy/queries/buildDockerProxyUrl';
export async function deleteConfig(
environmentId: EnvironmentId,
id: DockerConfig['Id']
id: Config['ID']
) {
try {
await axios.delete(buildDockerProxyUrl(environmentId, 'configs', id));

View File

@ -1,8 +0,0 @@
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
export type DockerConfig = {
Id: string;
Name: string;
CreatedAt: string;
ResourceControl?: ResourceControlViewModel;
};