diff --git a/app/portainer/__module.js b/app/portainer/__module.js index 8887f2545..b07c557e5 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -273,8 +273,7 @@ angular url: '/groups', views: { 'content@': { - templateUrl: './views/groups/groups.html', - controller: 'GroupsController', + component: 'environmentGroupsListView', }, }, data: { diff --git a/app/portainer/components/datatables/groups-datatable/groupsDatatable.html b/app/portainer/components/datatables/groups-datatable/groupsDatatable.html deleted file mode 100644 index 841e58f12..000000000 --- a/app/portainer/components/datatables/groups-datatable/groupsDatatable.html +++ /dev/null @@ -1,114 +0,0 @@ -
- - -
-
-
- -
- {{ $ctrl.titleText }} -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - - -
-
- - - - - -
-
- -
- - - - - {{ item.Name }} - - -
Loading...
No group available.
-
- -
-
-
diff --git a/app/portainer/components/datatables/groups-datatable/groupsDatatable.js b/app/portainer/components/datatables/groups-datatable/groupsDatatable.js deleted file mode 100644 index f8e69b55a..000000000 --- a/app/portainer/components/datatables/groups-datatable/groupsDatatable.js +++ /dev/null @@ -1,13 +0,0 @@ -angular.module('portainer.app').component('groupsDatatable', { - templateUrl: './groupsDatatable.html', - controller: 'GenericDatatableController', - bindings: { - titleText: '@', - titleIcon: '@', - dataset: '<', - tableKey: '@', - orderBy: '@', - reverseOrder: '<', - removeAction: '<', - }, -}); diff --git a/app/portainer/react/views/env-groups.ts b/app/portainer/react/views/env-groups.ts new file mode 100644 index 000000000..fcdeccd52 --- /dev/null +++ b/app/portainer/react/views/env-groups.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/portainer/environments/environment-groups/ListView'; + +export const environmentGroupModule = angular + .module('portainer.app.react.views.environment-groups', []) + .component( + 'environmentGroupsListView', + r2a(withUIRouter(withReactQuery(withCurrentUser(ListView))), []) + ).name; diff --git a/app/portainer/react/views/index.ts b/app/portainer/react/views/index.ts index 1f134debf..6e067d8b4 100644 --- a/app/portainer/react/views/index.ts +++ b/app/portainer/react/views/index.ts @@ -16,12 +16,14 @@ import { CreateHelmRepositoriesView } from '@/react/portainer/account/helm-repos import { wizardModule } from './wizard'; import { teamsModule } from './teams'; import { updateSchedulesModule } from './update-schedules'; +import { environmentGroupModule } from './env-groups'; export const viewsModule = angular .module('portainer.app.react.views', [ wizardModule, teamsModule, updateSchedulesModule, + environmentGroupModule, ]) .component( 'homeView', diff --git a/app/portainer/views/groups/groups.html b/app/portainer/views/groups/groups.html deleted file mode 100644 index b60ccfe2a..000000000 --- a/app/portainer/views/groups/groups.html +++ /dev/null @@ -1,7 +0,0 @@ - - -
-
- -
-
diff --git a/app/portainer/views/groups/groupsController.js b/app/portainer/views/groups/groupsController.js deleted file mode 100644 index 3b77985af..000000000 --- a/app/portainer/views/groups/groupsController.js +++ /dev/null @@ -1,52 +0,0 @@ -import angular from 'angular'; -import _ from 'lodash-es'; -import { confirmDestructive } from '@@/modals/confirm'; -import { buildConfirmButton } from '@@/modals/utils'; - -angular.module('portainer.app').controller('GroupsController', GroupsController); - -function GroupsController($scope, $state, $async, GroupService, Notifications) { - $scope.removeAction = removeAction; - - function removeAction(selectedItems) { - return $async(removeActionAsync, selectedItems); - } - - async function removeActionAsync(selectedItems) { - const confirmed = await confirmDestructive({ - title: 'Are you sure?', - message: 'Are you sure you want to remove the selected environment group(s)?', - confirmButton: buildConfirmButton('Remove', 'danger'), - }); - - if (!confirmed) { - return; - } - - for (let group of selectedItems) { - try { - await GroupService.deleteGroup(group.Id); - - Notifications.success('Environment group successfully removed', group.Name); - _.remove($scope.groups, group); - } catch (err) { - Notifications.error('Failure', err, 'Unable to remove group'); - } - } - - $state.reload(); - } - - function initView() { - GroupService.groups() - .then(function success(data) { - $scope.groups = data; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve environment groups'); - $scope.groups = []; - }); - } - - initView(); -} diff --git a/app/react/portainer/environments/environment-groups/ListView/.keep b/app/react/portainer/environments/environment-groups/ListView/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/react/portainer/environments/environment-groups/ListView/EnvironmentGroupsDatatable/EnvironmentGroupsDatatable.tsx b/app/react/portainer/environments/environment-groups/ListView/EnvironmentGroupsDatatable/EnvironmentGroupsDatatable.tsx new file mode 100644 index 000000000..66504966b --- /dev/null +++ b/app/react/portainer/environments/environment-groups/ListView/EnvironmentGroupsDatatable/EnvironmentGroupsDatatable.tsx @@ -0,0 +1,32 @@ +import { Dice4 } from 'lucide-react'; + +import { Datatable } from '@@/datatables'; +import { createPersistedStore } from '@@/datatables/types'; +import { useTableState } from '@@/datatables/useTableState'; + +import { useEnvironmentGroups } from '../../queries/useEnvironmentGroups'; + +import { columns } from './columns'; +import { TableActions } from './TableActions'; + +const tableKey = 'environment-groups'; +const store = createPersistedStore(tableKey); + +export function EnvironmentGroupsDatatable() { + const query = useEnvironmentGroups(); + const tableState = useTableState(store, tableKey); + + return ( + ( + + )} + /> + ); +} diff --git a/app/react/portainer/environments/environment-groups/ListView/EnvironmentGroupsDatatable/TableActions.tsx b/app/react/portainer/environments/environment-groups/ListView/EnvironmentGroupsDatatable/TableActions.tsx new file mode 100644 index 000000000..cce18dcb8 --- /dev/null +++ b/app/react/portainer/environments/environment-groups/ListView/EnvironmentGroupsDatatable/TableActions.tsx @@ -0,0 +1,37 @@ +import { notifySuccess } from '@/portainer/services/notifications'; + +import { DeleteButton } from '@@/buttons/DeleteButton'; +import { AddButton } from '@@/buttons'; + +import { EnvironmentGroup } from '../../types'; + +import { useDeleteEnvironmentGroupsMutation } from './useDeleteEnvironmentGroupsMutation'; + +export function TableActions({ + selectedItems, +}: { + selectedItems: EnvironmentGroup[]; +}) { + const deleteMutation = useDeleteEnvironmentGroupsMutation(); + + return ( + <> + + + Add group + + ); + + function handleRemove() { + const ids = selectedItems.map((item) => item.Id); + deleteMutation.mutate(ids, { + onSuccess() { + notifySuccess('Success', 'Environment Group(s) removed'); + }, + }); + } +} diff --git a/app/react/portainer/environments/environment-groups/ListView/EnvironmentGroupsDatatable/columns.tsx b/app/react/portainer/environments/environment-groups/ListView/EnvironmentGroupsDatatable/columns.tsx new file mode 100644 index 000000000..44ff3e8ff --- /dev/null +++ b/app/react/portainer/environments/environment-groups/ListView/EnvironmentGroupsDatatable/columns.tsx @@ -0,0 +1,36 @@ +import { CellContext, createColumnHelper } from '@tanstack/react-table'; +import { Users } from 'lucide-react'; + +import { buildNameColumn } from '@@/datatables/buildNameColumn'; +import { Button } from '@@/buttons'; +import { Link } from '@@/Link'; + +import { EnvironmentGroup } from '../../types'; + +const columnHelper = createColumnHelper(); + +export const columns = [ + buildNameColumn('Name', '.group'), + columnHelper.display({ + header: 'Actions', + cell: ActionsCell, + }), +]; + +function ActionsCell({ + row: { original: item }, +}: CellContext) { + return ( + + ); +} diff --git a/app/react/portainer/environments/environment-groups/ListView/EnvironmentGroupsDatatable/index.ts b/app/react/portainer/environments/environment-groups/ListView/EnvironmentGroupsDatatable/index.ts new file mode 100644 index 000000000..9d5fbf0df --- /dev/null +++ b/app/react/portainer/environments/environment-groups/ListView/EnvironmentGroupsDatatable/index.ts @@ -0,0 +1 @@ +export { EnvironmentGroupsDatatable } from './EnvironmentGroupsDatatable'; diff --git a/app/react/portainer/environments/environment-groups/ListView/EnvironmentGroupsDatatable/useDeleteEnvironmentGroupsMutation.ts b/app/react/portainer/environments/environment-groups/ListView/EnvironmentGroupsDatatable/useDeleteEnvironmentGroupsMutation.ts new file mode 100644 index 000000000..4542f7c83 --- /dev/null +++ b/app/react/portainer/environments/environment-groups/ListView/EnvironmentGroupsDatatable/useDeleteEnvironmentGroupsMutation.ts @@ -0,0 +1,37 @@ +import { useMutation, useQueryClient } from 'react-query'; + +import { withError, withInvalidate } from '@/react-tools/react-query'; +import { promiseSequence } from '@/portainer/helpers/promise-utils'; +import axios, { parseAxiosError } from '@/portainer/services/axios'; + +import { EnvironmentGroup } from '../../types'; +import { buildUrl } from '../../queries/build-url'; +import { queryKeys } from '../../queries/query-keys'; + +export function useDeleteEnvironmentGroupsMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: deleteEnvironmentGroups, + ...withError('Failed to delete environment groups'), + ...withInvalidate(queryClient, [queryKeys.base()]), + }); +} + +async function deleteEnvironmentGroups( + environmentGroupIds: Array +) { + return promiseSequence( + environmentGroupIds.map( + (environmentGroupId) => () => deleteEnvironmentGroup(environmentGroupId) + ) + ); +} + +async function deleteEnvironmentGroup(id: EnvironmentGroup['Id']) { + try { + await axios.delete(buildUrl(id)); + } catch (e) { + throw parseAxiosError(e, 'Unable to delete environment group'); + } +} diff --git a/app/react/portainer/environments/environment-groups/ListView/ListView.tsx b/app/react/portainer/environments/environment-groups/ListView/ListView.tsx new file mode 100644 index 000000000..9413cd238 --- /dev/null +++ b/app/react/portainer/environments/environment-groups/ListView/ListView.tsx @@ -0,0 +1,17 @@ +import { PageHeader } from '@@/PageHeader'; + +import { EnvironmentGroupsDatatable } from './EnvironmentGroupsDatatable'; + +export function ListView() { + return ( + <> + + + + + ); +} diff --git a/app/react/portainer/environments/environment-groups/ListView/index.ts b/app/react/portainer/environments/environment-groups/ListView/index.ts new file mode 100644 index 000000000..dd06dfd19 --- /dev/null +++ b/app/react/portainer/environments/environment-groups/ListView/index.ts @@ -0,0 +1 @@ +export { ListView } from './ListView'; diff --git a/app/react/portainer/environments/environment-groups/queries/useEnvironmentGroups.ts b/app/react/portainer/environments/environment-groups/queries/useEnvironmentGroups.ts new file mode 100644 index 000000000..4fde407b4 --- /dev/null +++ b/app/react/portainer/environments/environment-groups/queries/useEnvironmentGroups.ts @@ -0,0 +1,24 @@ +import { useQuery } from 'react-query'; + +import axios, { parseAxiosError } from '@/portainer/services/axios'; + +import { EnvironmentGroup } from '../types'; + +import { queryKeys } from './query-keys'; +import { buildUrl } from './build-url'; + +export function useEnvironmentGroups() { + return useQuery({ + queryKey: queryKeys.base(), + queryFn: () => getEnvironmentGroups(), + }); +} + +async function getEnvironmentGroups() { + try { + const { data } = await axios.get>(buildUrl()); + return data; + } catch (e) { + throw parseAxiosError(e, 'Unable to get access tokens'); + } +}