refactor(env/groups): migrate list view to react [EE-4703] (#10671)

pull/11505/head
Chaim Lev-Ari 2024-04-04 18:54:57 +03:00 committed by GitHub
parent 521eb5f114
commit d1ba484be1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 202 additions and 188 deletions

View File

@ -273,8 +273,7 @@ angular
url: '/groups',
views: {
'content@': {
templateUrl: './views/groups/groups.html',
controller: 'GroupsController',
component: 'environmentGroupsListView',
},
},
data: {

View File

@ -1,114 +0,0 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle vertical-center">
<div class="widget-icon space-right">
<pr-icon icon="$ctrl.titleIcon"></pr-icon>
</div>
{{ $ctrl.titleText }}
</div>
<div class="searchBar vertical-center">
<pr-icon icon="'search'"></pr-icon>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search for a group..."
auto-focus
ng-model-options="{ debounce: 300 }"
data-cy="endpointGroup-searchInput"
/>
</div>
<div class="actionBar !gap-3">
<button
type="button"
class="btn btn-sm btn-dangerlight h-fit"
ng-disabled="$ctrl.state.selectedItemCount === 0"
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
data-cy="endpointGroup-removeGroupButton"
>
<pr-icon icon="'trash-2'"></pr-icon>
Remove
</button>
<button type="button" class="btn btn-sm btn-primary h-fit" ui-sref="portainer.groups.new" data-cy="endpointGroup-addGroupButton">
<pr-icon icon="'plus'"></pr-icon>Add group
</button>
</div>
</div>
<div class="table-responsive">
<table class="table-hover nowrap-cells table" data-cy="endpointGroup-endpointGroupTable">
<thead>
<tr>
<th>
<div class="vertical-center">
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" data-cy="endpointGroup-selectAllCheckbox" />
<label for="select_all"></label>
</span>
<table-column-header
col-title="'Name'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Name'"
is-sorted-desc="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Name')"
></table-column-header>
</div>
</th>
<th>
<table-column-header col-title="'Actions'" can-sort="false"></table-column-header>
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
ng-class="{ active: item.Checked }"
>
<td>
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" />
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="portainer.groups.group({id: item.Id})">{{ item.Name }}</a>
</td>
<td>
<div class="vertical-center">
<a ui-sref="portainer.groups.group.access({id: item.Id})">
<pr-icon icon="'users'"></pr-icon>
Manage access
</a>
</div>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="3" class="text-muted text-center">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="3" class="text-muted text-center">No group available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline vertical-center">
<span class="limitSelector">
<span style="margin-right: 5px"> Items per page </span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()" data-cy="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@ -1,13 +0,0 @@
angular.module('portainer.app').component('groupsDatatable', {
templateUrl: './groupsDatatable.html',
controller: 'GenericDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
removeAction: '<',
},
});

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/portainer/environments/environment-groups/ListView';
export const environmentGroupModule = angular
.module('portainer.app.react.views.environment-groups', [])
.component(
'environmentGroupsListView',
r2a(withUIRouter(withReactQuery(withCurrentUser(ListView))), [])
).name;

View File

@ -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',

View File

@ -1,7 +0,0 @@
<page-header title="'Environment groups'" breadcrumbs="['Environment group management']" reload="true"> </page-header>
<div class="row">
<div class="col-sm-12">
<groups-datatable title-text="Environment groups" title-icon="dice-4" dataset="groups" table-key="groups" order-by="Name" remove-action="removeAction"></groups-datatable>
</div>
</div>

View File

@ -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();
}

View File

@ -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 (
<Datatable
columns={columns}
isLoading={query.isLoading}
dataset={query.data || []}
settingsManager={tableState}
title="Environment Groups"
titleIcon={Dice4}
renderTableActions={(selectedItems) => (
<TableActions selectedItems={selectedItems} />
)}
/>
);
}

View File

@ -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 (
<>
<DeleteButton
disabled={selectedItems.length === 0}
confirmMessage="Are you sure you want to remove the selected environment group(s)?"
onConfirmed={handleRemove}
/>
<AddButton>Add group</AddButton>
</>
);
function handleRemove() {
const ids = selectedItems.map((item) => item.Id);
deleteMutation.mutate(ids, {
onSuccess() {
notifySuccess('Success', 'Environment Group(s) removed');
},
});
}
}

View File

@ -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<EnvironmentGroup>();
export const columns = [
buildNameColumn<EnvironmentGroup>('Name', '.group'),
columnHelper.display({
header: 'Actions',
cell: ActionsCell,
}),
];
function ActionsCell({
row: { original: item },
}: CellContext<EnvironmentGroup, unknown>) {
return (
<Button
as={Link}
props={{
to: '.group.access',
params: { id: item.Id },
}}
color="link"
icon={Users}
>
Manage access
</Button>
);
}

View File

@ -0,0 +1 @@
export { EnvironmentGroupsDatatable } from './EnvironmentGroupsDatatable';

View File

@ -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<EnvironmentGroup['Id']>
) {
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');
}
}

View File

@ -0,0 +1,17 @@
import { PageHeader } from '@@/PageHeader';
import { EnvironmentGroupsDatatable } from './EnvironmentGroupsDatatable';
export function ListView() {
return (
<>
<PageHeader
title="Environment Groups"
breadcrumbs="Environment group management"
reload
/>
<EnvironmentGroupsDatatable />
</>
);
}

View File

@ -0,0 +1 @@
export { ListView } from './ListView';

View File

@ -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<Array<EnvironmentGroup>>(buildUrl());
return data;
} catch (e) {
throw parseAxiosError(e, 'Unable to get access tokens');
}
}