mirror of https://github.com/portainer/portainer
refactor(docker/configs): migrate list view to react [EE-2208]
parent
7549b6cf3f
commit
ae0a527a6d
|
@ -73,9 +73,7 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
|
||||||
url: '/configs',
|
url: '/configs',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
templateUrl: './views/configs/configs.html',
|
component: 'configsListView',
|
||||||
controller: 'ConfigsController',
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
|
|
|
@ -13,7 +13,6 @@ import { InsightsBox } from '@/react/components/InsightsBox';
|
||||||
import { BetaAlert } from '@/react/portainer/environments/update-schedules/common/BetaAlert';
|
import { BetaAlert } from '@/react/portainer/environments/update-schedules/common/BetaAlert';
|
||||||
import { ImagesDatatable } from '@/react/docker/images/ListView/ImagesDatatable/ImagesDatatable';
|
import { ImagesDatatable } from '@/react/docker/images/ListView/ImagesDatatable/ImagesDatatable';
|
||||||
import { EventsDatatable } from '@/react/docker/events/EventsDatatables';
|
import { EventsDatatable } from '@/react/docker/events/EventsDatatables';
|
||||||
import { ConfigsDatatable } from '@/react/docker/configs/ListView/ConfigsDatatable';
|
|
||||||
import { AgentHostBrowser } from '@/react/docker/host/BrowseView/AgentHostBrowser';
|
import { AgentHostBrowser } from '@/react/docker/host/BrowseView/AgentHostBrowser';
|
||||||
import { AgentVolumeBrowser } from '@/react/docker/volumes/BrowseView/AgentVolumeBrowser';
|
import { AgentVolumeBrowser } from '@/react/docker/volumes/BrowseView/AgentVolumeBrowser';
|
||||||
import { ProcessesDatatable } from '@/react/docker/containers/StatsView/ProcessesDatatable';
|
import { ProcessesDatatable } from '@/react/docker/containers/StatsView/ProcessesDatatable';
|
||||||
|
@ -79,14 +78,7 @@ const ngModule = angular
|
||||||
'onRemove',
|
'onRemove',
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
.component(
|
|
||||||
'dockerConfigsDatatable',
|
|
||||||
r2a(withUIRouter(withCurrentUser(ConfigsDatatable)), [
|
|
||||||
'dataset',
|
|
||||||
'onRemoveClick',
|
|
||||||
'onRefresh',
|
|
||||||
])
|
|
||||||
)
|
|
||||||
.component(
|
.component(
|
||||||
'agentHostBrowserReact',
|
'agentHostBrowserReact',
|
||||||
r2a(withUIRouter(withCurrentUser(AgentHostBrowser)), [
|
r2a(withUIRouter(withCurrentUser(AgentHostBrowser)), [
|
||||||
|
|
|
@ -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;
|
|
@ -7,9 +7,10 @@ import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||||
import { DashboardView } from '@/react/docker/DashboardView/DashboardView';
|
import { DashboardView } from '@/react/docker/DashboardView/DashboardView';
|
||||||
|
|
||||||
import { containersModule } from './containers';
|
import { containersModule } from './containers';
|
||||||
|
import { configsModule } from './configs';
|
||||||
|
|
||||||
export const viewsModule = angular
|
export const viewsModule = angular
|
||||||
.module('portainer.docker.react.views', [containersModule])
|
.module('portainer.docker.react.views', [containersModule, configsModule])
|
||||||
.component(
|
.component(
|
||||||
'dockerDashboardView',
|
'dockerDashboardView',
|
||||||
r2a(withUIRouter(withCurrentUser(DashboardView)), [])
|
r2a(withUIRouter(withCurrentUser(DashboardView)), [])
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { getConfigs } from '@/react/docker/configs/queries/useConfigs';
|
||||||
|
|
||||||
import { deleteConfig } from '@/react/docker/configs/queries/useDeleteConfigMutation';
|
import { deleteConfig } from '@/react/docker/configs/queries/useDeleteConfigMutation';
|
||||||
import { createConfig } from '@/react/docker/configs/queries/useCreateConfigMutation';
|
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);
|
angular.module('portainer.docker').factory('ConfigService', ConfigServiceFactory);
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
|
@ -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);
|
|
|
@ -1,38 +1,42 @@
|
||||||
import { Clipboard } from 'lucide-react';
|
import { Clipboard } from 'lucide-react';
|
||||||
|
|
||||||
import { Authorized, useAuthorizations } from '@/react/hooks/useUser';
|
import { Authorized, useAuthorizations } from '@/react/hooks/useUser';
|
||||||
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
|
||||||
import { Datatable, TableSettingsMenu } from '@@/datatables';
|
import { Datatable, TableSettingsMenu } from '@@/datatables';
|
||||||
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
|
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
|
||||||
import { useRepeater } from '@@/datatables/useRepeater';
|
|
||||||
import { AddButton } from '@@/buttons';
|
import { AddButton } from '@@/buttons';
|
||||||
import { useTableState } from '@@/datatables/useTableState';
|
import { useTableState } from '@@/datatables/useTableState';
|
||||||
import { DeleteButton } from '@@/buttons/DeleteButton';
|
|
||||||
|
|
||||||
import { DockerConfig } from '../../types';
|
import { useConfigsList } from '../queries/useConfigsList';
|
||||||
|
|
||||||
import { columns } from './columns';
|
import { columns } from './columns';
|
||||||
import { createStore } from './store';
|
import { createStore } from './store';
|
||||||
|
import { DeleteConfigButton } from './DeleteConfigButton';
|
||||||
interface Props {
|
|
||||||
dataset: Array<DockerConfig>;
|
|
||||||
onRemoveClick: (configs: Array<DockerConfig>) => void;
|
|
||||||
onRefresh: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const storageKey = 'docker_configs';
|
const storageKey = 'docker_configs';
|
||||||
const settingsStore = createStore(storageKey);
|
const settingsStore = createStore(storageKey);
|
||||||
|
|
||||||
export function ConfigsDatatable({ dataset, onRefresh, onRemoveClick }: Props) {
|
export function ConfigsDatatable() {
|
||||||
|
const environmentId = useEnvironmentId();
|
||||||
|
|
||||||
const tableState = useTableState(settingsStore, storageKey);
|
const tableState = useTableState(settingsStore, storageKey);
|
||||||
|
|
||||||
useRepeater(tableState.autoRefreshRate, onRefresh);
|
const configListQuery = useConfigsList(environmentId, {
|
||||||
|
refetchInterval: tableState.autoRefreshRate * 1000,
|
||||||
|
});
|
||||||
|
|
||||||
const hasWriteAccessQuery = useAuthorizations([
|
const hasWriteAccessQuery = useAuthorizations([
|
||||||
'DockerConfigCreate',
|
'DockerConfigCreate',
|
||||||
'DockerConfigDelete',
|
'DockerConfigDelete',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (!configListQuery.data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataset = configListQuery.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Datatable
|
<Datatable
|
||||||
dataset={dataset}
|
dataset={dataset}
|
||||||
|
@ -54,12 +58,7 @@ export function ConfigsDatatable({ dataset, onRefresh, onRemoveClick }: Props) {
|
||||||
hasWriteAccessQuery.authorized && (
|
hasWriteAccessQuery.authorized && (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Authorized authorizations="DockerConfigDelete">
|
<Authorized authorizations="DockerConfigDelete">
|
||||||
<DeleteButton
|
<DeleteConfigButton selectedItems={selectedRows} />
|
||||||
disabled={selectedRows.length === 0}
|
|
||||||
data-cy="remove-docker-configs-button"
|
|
||||||
onConfirmed={() => onRemoveClick(selectedRows)}
|
|
||||||
confirmMessage="Do you want to remove the selected config(s)?"
|
|
||||||
/>
|
|
||||||
</Authorized>
|
</Authorized>
|
||||||
|
|
||||||
<Authorized authorizations="DockerConfigCreate">
|
<Authorized authorizations="DockerConfigCreate">
|
||||||
|
|
|
@ -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)]),
|
||||||
|
});
|
||||||
|
}
|
|
@ -5,12 +5,12 @@ import { createOwnershipColumn } from '@/react/docker/components/datatable/creat
|
||||||
|
|
||||||
import { buildNameColumn } from '@@/datatables/buildNameColumn';
|
import { buildNameColumn } from '@@/datatables/buildNameColumn';
|
||||||
|
|
||||||
import { DockerConfig } from '../../types';
|
import { ConfigViewModel } from '../../model';
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<DockerConfig>();
|
const columnHelper = createColumnHelper<ConfigViewModel>();
|
||||||
|
|
||||||
export const columns = [
|
export const columns = [
|
||||||
buildNameColumn<DockerConfig>(
|
buildNameColumn<ConfigViewModel>(
|
||||||
'Name',
|
'Name',
|
||||||
'docker.configs.config',
|
'docker.configs.config',
|
||||||
'docker-configs-name'
|
'docker-configs-name'
|
||||||
|
@ -22,5 +22,5 @@ export const columns = [
|
||||||
return <time dateTime={date}>{isoDate(date)}</time>;
|
return <time dateTime={date}>{isoDate(date)}</time>;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
createOwnershipColumn<DockerConfig>(),
|
createOwnershipColumn<ConfigViewModel>(),
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { PageHeader } from '@@/PageHeader';
|
||||||
|
|
||||||
|
import { ConfigsDatatable } from './ConfigsDatatable';
|
||||||
|
|
||||||
|
export function ListView() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader title="Configs list" breadcrumbs="Configs" reload />
|
||||||
|
|
||||||
|
<ConfigsDatatable />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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),
|
||||||
|
};
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,14 +4,14 @@ import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
import { buildDockerProxyUrl } from '../../proxy/queries/buildDockerProxyUrl';
|
import { buildDockerProxyUrl } from '../../proxy/queries/buildDockerProxyUrl';
|
||||||
import { DockerConfig } from '../types';
|
import { PortainerResponse } from '../../types';
|
||||||
|
|
||||||
export async function getConfig(
|
export async function getConfig(
|
||||||
environmentId: EnvironmentId,
|
environmentId: EnvironmentId,
|
||||||
configId: DockerConfig['Id']
|
configId: Config['ID']
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get<Config>(
|
const { data } = await axios.get<PortainerResponse<Config>>(
|
||||||
buildDockerProxyUrl(environmentId, 'configs', configId)
|
buildDockerProxyUrl(environmentId, 'configs', configId)
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
|
import { Config } from 'docker-types/generated/1.41';
|
||||||
|
|
||||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
import { DockerConfig } from '../types';
|
|
||||||
import { buildDockerProxyUrl } from '../../proxy/queries/buildDockerProxyUrl';
|
import { buildDockerProxyUrl } from '../../proxy/queries/buildDockerProxyUrl';
|
||||||
|
|
||||||
export async function deleteConfig(
|
export async function deleteConfig(
|
||||||
environmentId: EnvironmentId,
|
environmentId: EnvironmentId,
|
||||||
id: DockerConfig['Id']
|
id: Config['ID']
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
await axios.delete(buildDockerProxyUrl(environmentId, 'configs', id));
|
await axios.delete(buildDockerProxyUrl(environmentId, 'configs', id));
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
|
||||||
|
|
||||||
export type DockerConfig = {
|
|
||||||
Id: string;
|
|
||||||
Name: string;
|
|
||||||
CreatedAt: string;
|
|
||||||
ResourceControl?: ResourceControlViewModel;
|
|
||||||
};
|
|
Loading…
Reference in New Issue