From 99b39da03d958328390318f8e1a9c4da671e2559 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Tue, 14 Nov 2023 12:57:27 +0200 Subject: [PATCH] refactor(edge/groups): migrate view to react [EE-4683] (#10592) --- .../groups-datatable/groupsDatatable.html | 126 ------------------ .../groupsDatatableController.js | 17 --- app/edge/components/groups-datatable/index.js | 17 --- app/edge/react/views/index.ts | 9 +- .../edgeGroupsView/edgeGroupsView.html | 7 - .../edgeGroupsViewController.js | 47 ------- .../views/edge-groups/edgeGroupsView/index.js | 8 -- app/react/edge/edge-groups/ListView/.keep | 0 .../ListView/EdgeGroupsDatatable.tsx | 37 +++++ .../edge/edge-groups/ListView/ListView.tsx | 12 ++ .../edge-groups/ListView/TableActions.tsx | 37 +++++ .../edge-groups/ListView/columns/helper.ts | 5 + .../edge-groups/ListView/columns/index.ts | 13 ++ .../edge-groups/ListView/columns/name.tsx | 34 +++++ app/react/edge/edge-groups/ListView/index.ts | 1 + .../ListView/useDeleteEdgeGroupMutation.ts | 35 +++++ .../edge/edge-groups/queries/build-url.ts | 11 +- .../edge/edge-groups/queries/useEdgeGroups.ts | 11 +- 18 files changed, 199 insertions(+), 228 deletions(-) delete mode 100644 app/edge/components/groups-datatable/groupsDatatable.html delete mode 100644 app/edge/components/groups-datatable/groupsDatatableController.js delete mode 100644 app/edge/components/groups-datatable/index.js delete mode 100644 app/edge/views/edge-groups/edgeGroupsView/edgeGroupsView.html delete mode 100644 app/edge/views/edge-groups/edgeGroupsView/edgeGroupsViewController.js delete mode 100644 app/edge/views/edge-groups/edgeGroupsView/index.js delete mode 100644 app/react/edge/edge-groups/ListView/.keep create mode 100644 app/react/edge/edge-groups/ListView/EdgeGroupsDatatable.tsx create mode 100644 app/react/edge/edge-groups/ListView/ListView.tsx create mode 100644 app/react/edge/edge-groups/ListView/TableActions.tsx create mode 100644 app/react/edge/edge-groups/ListView/columns/helper.ts create mode 100644 app/react/edge/edge-groups/ListView/columns/index.ts create mode 100644 app/react/edge/edge-groups/ListView/columns/name.tsx create mode 100644 app/react/edge/edge-groups/ListView/index.ts create mode 100644 app/react/edge/edge-groups/ListView/useDeleteEdgeGroupMutation.ts diff --git a/app/edge/components/groups-datatable/groupsDatatable.html b/app/edge/components/groups-datatable/groupsDatatable.html deleted file mode 100644 index 271c37a12..000000000 --- a/app/edge/components/groups-datatable/groupsDatatable.html +++ /dev/null @@ -1,126 +0,0 @@ -
- - -
-
-
- -
- Edge Groups -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -
- - - - - {{ item.Name }} - in use - {{ item.TrustedEndpoints.length }}{{ item.Dynamic ? 'Dynamic' : 'Static' }}
Loading...
No Edge group available.
-
- -
-
-
diff --git a/app/edge/components/groups-datatable/groupsDatatableController.js b/app/edge/components/groups-datatable/groupsDatatableController.js deleted file mode 100644 index 3f7cdf774..000000000 --- a/app/edge/components/groups-datatable/groupsDatatableController.js +++ /dev/null @@ -1,17 +0,0 @@ -import angular from 'angular'; - -export class EdgeGroupsDatatableController { - /* @ngInject */ - constructor($scope, $controller) { - const allowSelection = this.allowSelection; - angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); - this.allowSelection = allowSelection.bind(this); - } - - /** - * Override this method to allow/deny selection - */ - allowSelection(item) { - return !item.HasEdgeStack; - } -} diff --git a/app/edge/components/groups-datatable/index.js b/app/edge/components/groups-datatable/index.js deleted file mode 100644 index 409551a94..000000000 --- a/app/edge/components/groups-datatable/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import angular from 'angular'; - -import { EdgeGroupsDatatableController } from './groupsDatatableController'; - -angular.module('portainer.edge').component('edgeGroupsDatatable', { - templateUrl: './groupsDatatable.html', - controller: EdgeGroupsDatatableController, - bindings: { - dataset: '<', - titleIcon: '@', - tableKey: '@', - orderBy: '@', - removeAction: '<', - updateAction: '<', - reverseOrder: '<', - }, -}); diff --git a/app/edge/react/views/index.ts b/app/edge/react/views/index.ts index 29730c057..28a8bf1b4 100644 --- a/app/edge/react/views/index.ts +++ b/app/edge/react/views/index.ts @@ -5,7 +5,8 @@ import { withCurrentUser } from '@/react-tools/withCurrentUser'; import { withReactQuery } from '@/react-tools/withReactQuery'; import { withUIRouter } from '@/react-tools/withUIRouter'; import { WaitingRoomView } from '@/react/edge/edge-devices/WaitingRoomView'; -import { ListView } from '@/react/edge/edge-stacks/ListView'; +import { ListView as EdgeStacksListView } from '@/react/edge/edge-stacks/ListView'; +import { ListView as EdgeGroupsListView } from '@/react/edge/edge-groups/ListView'; export const viewsModule = angular .module('portainer.edge.react.views', []) @@ -15,5 +16,9 @@ export const viewsModule = angular ) .component( 'edgeStacksView', - r2a(withUIRouter(withCurrentUser(ListView)), []) + r2a(withUIRouter(withCurrentUser(EdgeStacksListView)), []) + ) + .component( + 'edgeGroupsView', + r2a(withUIRouter(withCurrentUser(EdgeGroupsListView)), []) ).name; diff --git a/app/edge/views/edge-groups/edgeGroupsView/edgeGroupsView.html b/app/edge/views/edge-groups/edgeGroupsView/edgeGroupsView.html deleted file mode 100644 index 747e097ad..000000000 --- a/app/edge/views/edge-groups/edgeGroupsView/edgeGroupsView.html +++ /dev/null @@ -1,7 +0,0 @@ - - -
-
- -
-
diff --git a/app/edge/views/edge-groups/edgeGroupsView/edgeGroupsViewController.js b/app/edge/views/edge-groups/edgeGroupsView/edgeGroupsViewController.js deleted file mode 100644 index 989cf3c49..000000000 --- a/app/edge/views/edge-groups/edgeGroupsView/edgeGroupsViewController.js +++ /dev/null @@ -1,47 +0,0 @@ -import _ from 'lodash-es'; -import { confirmDelete } from '@@/modals/confirm'; - -export class EdgeGroupsController { - /* @ngInject */ - constructor($async, $state, EdgeGroupService, Notifications) { - this.$async = $async; - this.$state = $state; - this.EdgeGroupService = EdgeGroupService; - this.Notifications = Notifications; - - this.removeAction = this.removeAction.bind(this); - this.removeActionAsync = this.removeActionAsync.bind(this); - } - - async $onInit() { - try { - this.items = await this.EdgeGroupService.groups(); - } catch (err) { - this.items = []; - this.Notifications.error('Failure', err, 'Unable to retrieve Edge groups'); - } - } - - removeAction(selectedItems) { - return this.$async(this.removeActionAsync, selectedItems); - } - - async removeActionAsync(selectedItems) { - if (!(await confirmDelete('Do you want to remove the selected Edge Group(s)?'))) { - return; - } - - for (let item of selectedItems) { - try { - await this.EdgeGroupService.remove(item.Id); - - this.Notifications.success('Edge Group successfully removed', item.Name); - _.remove(this.items, item); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to remove Edge Group'); - } - } - - this.$state.reload(); - } -} diff --git a/app/edge/views/edge-groups/edgeGroupsView/index.js b/app/edge/views/edge-groups/edgeGroupsView/index.js deleted file mode 100644 index 2c511421b..000000000 --- a/app/edge/views/edge-groups/edgeGroupsView/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import angular from 'angular'; - -import { EdgeGroupsController } from './edgeGroupsViewController'; - -angular.module('portainer.edge').component('edgeGroupsView', { - templateUrl: './edgeGroupsView.html', - controller: EdgeGroupsController, -}); diff --git a/app/react/edge/edge-groups/ListView/.keep b/app/react/edge/edge-groups/ListView/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/react/edge/edge-groups/ListView/EdgeGroupsDatatable.tsx b/app/react/edge/edge-groups/ListView/EdgeGroupsDatatable.tsx new file mode 100644 index 000000000..8a7d98761 --- /dev/null +++ b/app/react/edge/edge-groups/ListView/EdgeGroupsDatatable.tsx @@ -0,0 +1,37 @@ +import { LayoutGrid } from 'lucide-react'; + +import { Datatable } from '@@/datatables'; +import { useTableState } from '@@/datatables/useTableState'; +import { createPersistedStore } from '@@/datatables/types'; + +import { useEdgeGroups } from '../queries/useEdgeGroups'; + +import { columns } from './columns'; +import { TableActions } from './TableActions'; + +const tableKey = 'edge-groups'; + +const settingsStore = createPersistedStore(tableKey); + +export function EdgeGroupsDatatable() { + const tableState = useTableState(settingsStore, tableKey); + const edgeGroupsQuery = useEdgeGroups(); + + return ( + ( + + )} + isRowSelectable={({ original: item }) => + !(item.HasEdgeStack || item.HasEdgeJob || item.HasEdgeConfig) + } + /> + ); +} diff --git a/app/react/edge/edge-groups/ListView/ListView.tsx b/app/react/edge/edge-groups/ListView/ListView.tsx new file mode 100644 index 000000000..97aed7b52 --- /dev/null +++ b/app/react/edge/edge-groups/ListView/ListView.tsx @@ -0,0 +1,12 @@ +import { PageHeader } from '@@/PageHeader'; + +import { EdgeGroupsDatatable } from './EdgeGroupsDatatable'; + +export function ListView() { + return ( + <> + + + + ); +} diff --git a/app/react/edge/edge-groups/ListView/TableActions.tsx b/app/react/edge/edge-groups/ListView/TableActions.tsx new file mode 100644 index 000000000..5d38e45b3 --- /dev/null +++ b/app/react/edge/edge-groups/ListView/TableActions.tsx @@ -0,0 +1,37 @@ +import { notifySuccess } from '@/portainer/services/notifications'; + +import { AddButton } from '@@/buttons'; +import { DeleteButton } from '@@/buttons/DeleteButton'; + +import { EdgeGroup } from '../types'; + +import { useDeleteEdgeGroupsMutation } from './useDeleteEdgeGroupMutation'; + +export function TableActions({ + selectedItems, +}: { + selectedItems: Array; +}) { + const removeMutation = useDeleteEdgeGroupsMutation(); + + return ( +
+ handleRemove(selectedItems)} + /> + + Add Edge group +
+ ); + + async function handleRemove(selectedItems: Array) { + const ids = selectedItems.map((item) => item.Id); + removeMutation.mutate(ids, { + onSuccess: () => { + notifySuccess('Success', 'Edge Group(s) removed'); + }, + }); + } +} diff --git a/app/react/edge/edge-groups/ListView/columns/helper.ts b/app/react/edge/edge-groups/ListView/columns/helper.ts new file mode 100644 index 000000000..d4e142ba9 --- /dev/null +++ b/app/react/edge/edge-groups/ListView/columns/helper.ts @@ -0,0 +1,5 @@ +import { createColumnHelper } from '@tanstack/react-table'; + +import { EdgeGroupListItemResponse } from '../../queries/useEdgeGroups'; + +export const columnHelper = createColumnHelper(); diff --git a/app/react/edge/edge-groups/ListView/columns/index.ts b/app/react/edge/edge-groups/ListView/columns/index.ts new file mode 100644 index 000000000..7b1d5e469 --- /dev/null +++ b/app/react/edge/edge-groups/ListView/columns/index.ts @@ -0,0 +1,13 @@ +import { columnHelper } from './helper'; +import { name } from './name'; + +export const columns = [ + name, + columnHelper.accessor((group) => group.TrustedEndpoints.length, { + header: 'Environments Count', + }), + columnHelper.accessor('Dynamic', { + header: 'Group Type', + cell: ({ getValue }) => (getValue() ? 'Dynamic' : 'Static'), + }), +]; diff --git a/app/react/edge/edge-groups/ListView/columns/name.tsx b/app/react/edge/edge-groups/ListView/columns/name.tsx new file mode 100644 index 000000000..a92fb9b08 --- /dev/null +++ b/app/react/edge/edge-groups/ListView/columns/name.tsx @@ -0,0 +1,34 @@ +import { CellContext } from '@tanstack/react-table'; + +import { Link } from '@@/Link'; + +import { EdgeGroupListItemResponse } from '../../queries/useEdgeGroups'; + +import { columnHelper } from './helper'; + +export const name = columnHelper.accessor('Name', { + header: 'Name', + cell: NameCell, +}); + +function NameCell({ + renderValue, + row: { original: item }, +}: CellContext) { + const name = renderValue() || ''; + + if (typeof name !== 'string') { + return null; + } + + return ( + <> + + {name} + + {(item.HasEdgeJob || item.HasEdgeStack) && ( + in use + )} + + ); +} diff --git a/app/react/edge/edge-groups/ListView/index.ts b/app/react/edge/edge-groups/ListView/index.ts new file mode 100644 index 000000000..dd06dfd19 --- /dev/null +++ b/app/react/edge/edge-groups/ListView/index.ts @@ -0,0 +1 @@ +export { ListView } from './ListView'; diff --git a/app/react/edge/edge-groups/ListView/useDeleteEdgeGroupMutation.ts b/app/react/edge/edge-groups/ListView/useDeleteEdgeGroupMutation.ts new file mode 100644 index 000000000..8cb262a3b --- /dev/null +++ b/app/react/edge/edge-groups/ListView/useDeleteEdgeGroupMutation.ts @@ -0,0 +1,35 @@ +import { useMutation, useQueryClient } from 'react-query'; + +import { promiseSequence } from '@/portainer/helpers/promise-utils'; +import axios, { parseAxiosError } from '@/portainer/services/axios'; +import { + mutationOptions, + withError, + withInvalidate, +} from '@/react-tools/react-query'; + +import { EdgeGroup } from '../types'; +import { buildUrl } from '../queries/build-url'; +import { queryKeys } from '../queries/query-keys'; + +export function useDeleteEdgeGroupsMutation() { + const queryClient = useQueryClient(); + return useMutation( + (edgeGroupIds: Array) => + promiseSequence( + edgeGroupIds.map((edgeGroupId) => () => deleteEdgeGroup(edgeGroupId)) + ), + mutationOptions( + withError('Unable to delete Edge Group(s)'), + withInvalidate(queryClient, [queryKeys.base()]) + ) + ); +} + +async function deleteEdgeGroup(id: EdgeGroup['Id']) { + try { + await axios.delete(buildUrl({ id })); + } catch (e) { + throw parseAxiosError(e, 'Unable to delete edge Group'); + } +} diff --git a/app/react/edge/edge-groups/queries/build-url.ts b/app/react/edge/edge-groups/queries/build-url.ts index 720780dd0..dd4ae1d68 100644 --- a/app/react/edge/edge-groups/queries/build-url.ts +++ b/app/react/edge/edge-groups/queries/build-url.ts @@ -1,3 +1,10 @@ -export function buildUrl() { - return '/edge_groups'; +import { EdgeGroup } from '../types'; + +export function buildUrl({ + action, + id, +}: { id?: EdgeGroup['Id']; action?: string } = {}) { + const baseUrl = '/edge_groups'; + const url = id ? `${baseUrl}/${id}` : baseUrl; + return action ? `${url}/${action}` : url; } diff --git a/app/react/edge/edge-groups/queries/useEdgeGroups.ts b/app/react/edge/edge-groups/queries/useEdgeGroups.ts index 9adea5fb3..5c6d3952d 100644 --- a/app/react/edge/edge-groups/queries/useEdgeGroups.ts +++ b/app/react/edge/edge-groups/queries/useEdgeGroups.ts @@ -1,15 +1,22 @@ import { useQuery } from 'react-query'; import axios, { parseAxiosError } from '@/portainer/services/axios'; -import { EnvironmentType } from '@/react/portainer/environments/types'; +import { + EnvironmentId, + EnvironmentType, +} from '@/react/portainer/environments/types'; import { EdgeGroup } from '../types'; import { queryKeys } from './query-keys'; import { buildUrl } from './build-url'; -interface EdgeGroupListItemResponse extends EdgeGroup { +export interface EdgeGroupListItemResponse extends EdgeGroup { EndpointTypes: Array; + HasEdgeStack?: boolean; + HasEdgeJob?: boolean; + HasEdgeConfig?: boolean; + TrustedEndpoints: Array; } async function getEdgeGroups() {