From ae0a527a6d422dcb339081e161bf5c2300c4c104 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Sun, 10 Mar 2024 13:04:05 +0200 Subject: [PATCH] refactor(docker/configs): migrate list view to react [EE-2208] --- app/docker/__module.js | 4 +- app/docker/react/components/index.ts | 10 +--- app/docker/react/views/configs.ts | 14 +++++ app/docker/react/views/index.ts | 3 +- app/docker/services/configService.js | 2 +- app/docker/views/configs/configs.html | 3 - app/docker/views/configs/configsController.js | 59 ------------------- app/react/docker/configs/.keep | 0 app/react/docker/configs/ListView/.keep | 0 .../ConfigsDatatable/ConfigsDatatable.tsx | 33 +++++------ .../ConfigsDatatable/DeleteConfigButton.tsx | 44 ++++++++++++++ .../ListView/ConfigsDatatable/columns.tsx | 8 +-- .../docker/configs/ListView/ListView.tsx | 13 ++++ .../configs/ListView/queries/build-url.ts | 6 ++ .../configs/ListView/queries/queryKeys.ts | 8 +++ .../ListView/queries/useConfigsList.ts | 29 +++++++++ .../queries/useDeleteConfigMutation.ts | 26 ++++++++ .../docker/configs/model.ts} | 0 app/react/docker/configs/queries/useConfig.ts | 6 +- .../queries/useDeleteConfigMutation.ts | 5 +- app/react/docker/configs/types.ts | 8 --- 21 files changed, 171 insertions(+), 110 deletions(-) create mode 100644 app/docker/react/views/configs.ts delete mode 100644 app/docker/views/configs/configs.html delete mode 100644 app/docker/views/configs/configsController.js delete mode 100644 app/react/docker/configs/.keep delete mode 100644 app/react/docker/configs/ListView/.keep create mode 100644 app/react/docker/configs/ListView/ConfigsDatatable/DeleteConfigButton.tsx create mode 100644 app/react/docker/configs/ListView/ListView.tsx create mode 100644 app/react/docker/configs/ListView/queries/build-url.ts create mode 100644 app/react/docker/configs/ListView/queries/queryKeys.ts create mode 100644 app/react/docker/configs/ListView/queries/useConfigsList.ts create mode 100644 app/react/docker/configs/ListView/queries/useDeleteConfigMutation.ts rename app/{docker/models/config.ts => react/docker/configs/model.ts} (100%) delete mode 100644 app/react/docker/configs/types.ts diff --git a/app/docker/__module.js b/app/docker/__module.js index 12a66d44e..cd29f9288 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -73,9 +73,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: { diff --git a/app/docker/react/components/index.ts b/app/docker/react/components/index.ts index d1116bda3..359c7f001 100644 --- a/app/docker/react/components/index.ts +++ b/app/docker/react/components/index.ts @@ -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)), [ diff --git a/app/docker/react/views/configs.ts b/app/docker/react/views/configs.ts new file mode 100644 index 000000000..17ef0c0ee --- /dev/null +++ b/app/docker/react/views/configs.ts @@ -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; diff --git a/app/docker/react/views/index.ts b/app/docker/react/views/index.ts index 6205f0b22..fc043d754 100644 --- a/app/docker/react/views/index.ts +++ b/app/docker/react/views/index.ts @@ -7,9 +7,10 @@ import { withUIRouter } from '@/react-tools/withUIRouter'; import { DashboardView } from '@/react/docker/DashboardView/DashboardView'; 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)), []) diff --git a/app/docker/services/configService.js b/app/docker/services/configService.js index 05a63a076..00ddf7b64 100644 --- a/app/docker/services/configService.js +++ b/app/docker/services/configService.js @@ -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); diff --git a/app/docker/views/configs/configs.html b/app/docker/views/configs/configs.html deleted file mode 100644 index ee4727bbe..000000000 --- a/app/docker/views/configs/configs.html +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/docker/views/configs/configsController.js b/app/docker/views/configs/configsController.js deleted file mode 100644 index 2d6168172..000000000 --- a/app/docker/views/configs/configsController.js +++ /dev/null @@ -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); diff --git a/app/react/docker/configs/.keep b/app/react/docker/configs/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/react/docker/configs/ListView/.keep b/app/react/docker/configs/ListView/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/react/docker/configs/ListView/ConfigsDatatable/ConfigsDatatable.tsx b/app/react/docker/configs/ListView/ConfigsDatatable/ConfigsDatatable.tsx index dc7917234..684aa11f0 100644 --- a/app/react/docker/configs/ListView/ConfigsDatatable/ConfigsDatatable.tsx +++ b/app/react/docker/configs/ListView/ConfigsDatatable/ConfigsDatatable.tsx @@ -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; - onRemoveClick: (configs: Array) => 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 ( - onRemoveClick(selectedRows)} - confirmMessage="Do you want to remove the selected config(s)?" - /> + diff --git a/app/react/docker/configs/ListView/ConfigsDatatable/DeleteConfigButton.tsx b/app/react/docker/configs/ListView/ConfigsDatatable/DeleteConfigButton.tsx new file mode 100644 index 000000000..75e49e333 --- /dev/null +++ b/app/react/docker/configs/ListView/ConfigsDatatable/DeleteConfigButton.tsx @@ -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; +}) { + const environmentId = useEnvironmentId(); + const mutation = useDeleteConfigListMutation(environmentId); + + return ( + { + 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) => + promiseSequence( + ids.map((configId) => () => deleteConfig({ environmentId, configId })) + ), + ...withError('Unable to remove configs'), + ...withInvalidate(queryClient, [queryKeys.base(environmentId)]), + }); +} diff --git a/app/react/docker/configs/ListView/ConfigsDatatable/columns.tsx b/app/react/docker/configs/ListView/ConfigsDatatable/columns.tsx index 0249e91ca..5904b0d97 100644 --- a/app/react/docker/configs/ListView/ConfigsDatatable/columns.tsx +++ b/app/react/docker/configs/ListView/ConfigsDatatable/columns.tsx @@ -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(); +const columnHelper = createColumnHelper(); export const columns = [ - buildNameColumn( + buildNameColumn( 'Name', 'docker.configs.config', 'docker-configs-name' @@ -22,5 +22,5 @@ export const columns = [ return ; }, }), - createOwnershipColumn(), + createOwnershipColumn(), ]; diff --git a/app/react/docker/configs/ListView/ListView.tsx b/app/react/docker/configs/ListView/ListView.tsx new file mode 100644 index 000000000..28ddd1fbb --- /dev/null +++ b/app/react/docker/configs/ListView/ListView.tsx @@ -0,0 +1,13 @@ +import { PageHeader } from '@@/PageHeader'; + +import { ConfigsDatatable } from './ConfigsDatatable'; + +export function ListView() { + return ( + <> + + + + + ); +} diff --git a/app/react/docker/configs/ListView/queries/build-url.ts b/app/react/docker/configs/ListView/queries/build-url.ts new file mode 100644 index 000000000..5a14b51d7 --- /dev/null +++ b/app/react/docker/configs/ListView/queries/build-url.ts @@ -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); +} diff --git a/app/react/docker/configs/ListView/queries/queryKeys.ts b/app/react/docker/configs/ListView/queries/queryKeys.ts new file mode 100644 index 000000000..395ff8027 --- /dev/null +++ b/app/react/docker/configs/ListView/queries/queryKeys.ts @@ -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), +}; diff --git a/app/react/docker/configs/ListView/queries/useConfigsList.ts b/app/react/docker/configs/ListView/queries/useConfigsList.ts new file mode 100644 index 000000000..75f4f4d8b --- /dev/null +++ b/app/react/docker/configs/ListView/queries/useConfigsList.ts @@ -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>(buildUrl(environmentId)); + return data.map((c) => new ConfigViewModel(c)); + } catch (err) { + throw parseAxiosError(err as Error, 'Unable to retrieve configs'); + } +} diff --git a/app/react/docker/configs/ListView/queries/useDeleteConfigMutation.ts b/app/react/docker/configs/ListView/queries/useDeleteConfigMutation.ts new file mode 100644 index 000000000..9a911d20d --- /dev/null +++ b/app/react/docker/configs/ListView/queries/useDeleteConfigMutation.ts @@ -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'); + } +} diff --git a/app/docker/models/config.ts b/app/react/docker/configs/model.ts similarity index 100% rename from app/docker/models/config.ts rename to app/react/docker/configs/model.ts diff --git a/app/react/docker/configs/queries/useConfig.ts b/app/react/docker/configs/queries/useConfig.ts index fc80798de..8fc1b57b9 100644 --- a/app/react/docker/configs/queries/useConfig.ts +++ b/app/react/docker/configs/queries/useConfig.ts @@ -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( + const { data } = await axios.get>( buildDockerProxyUrl(environmentId, 'configs', configId) ); return data; diff --git a/app/react/docker/configs/queries/useDeleteConfigMutation.ts b/app/react/docker/configs/queries/useDeleteConfigMutation.ts index ef03aa44c..2911ce6ae 100644 --- a/app/react/docker/configs/queries/useDeleteConfigMutation.ts +++ b/app/react/docker/configs/queries/useDeleteConfigMutation.ts @@ -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)); diff --git a/app/react/docker/configs/types.ts b/app/react/docker/configs/types.ts deleted file mode 100644 index 489fd3b4f..000000000 --- a/app/react/docker/configs/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel'; - -export type DockerConfig = { - Id: string; - Name: string; - CreatedAt: string; - ResourceControl?: ResourceControlViewModel; -};