mirror of https://github.com/portainer/portainer
refactor(rbac): migrate access table to react [EE-4710] (#10823)
parent
6ff4fd3db2
commit
e9ebef15a0
@ -1,125 +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'" class-name="'searchIcon'"></pr-icon>
|
||||
<input
|
||||
type="text"
|
||||
data-cy="accessDatatable-searchInput"
|
||||
class="searchInput"
|
||||
ng-model="$ctrl.state.textFilter"
|
||||
ng-change="$ctrl.onTextFilterChange()"
|
||||
placeholder="Search..."
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
/>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-dangerlight vertical-center"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0"
|
||||
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
|
||||
>
|
||||
<pr-icon icon="'trash-2'"></pr-icon>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="small text-muted mx-4 mb-4">
|
||||
<div class="" ng-if="$ctrl.inheritFrom">
|
||||
Access tagged as <code>inherited</code> are inherited from the group access. They cannot be removed or modified at the environment level but they can be overridden.
|
||||
</div>
|
||||
<div class="" ng-if="$ctrl.inheritFrom"> Access tagged as <code>override</code> are overriding the group access for the related users/teams. </div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table-hover nowrap-cells table">
|
||||
<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="select-all-checkbox" />
|
||||
<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="'Type'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'Type'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'Type' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('Type')"
|
||||
></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)) track by $index"
|
||||
ng-class="{ active: item.Checked }"
|
||||
>
|
||||
<td>
|
||||
<span class="md-checkbox">
|
||||
<input
|
||||
id="select_{{ $index }}"
|
||||
type="checkbox"
|
||||
ng-model="item.Checked"
|
||||
ng-disabled="$ctrl.disableRemove(item)"
|
||||
ng-click="$ctrl.selectItem(item, $event)"
|
||||
data-cy="select-checkbox"
|
||||
/>
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
{{ item.Name }}
|
||||
<span ng-if="$ctrl.inheritFrom && item.Inherited" class="text-muted small" style="margin-left: 2px"><code style="font-size: 85% !important">inherited</code></span>
|
||||
<span ng-if="$ctrl.inheritFrom && item.Override" class="text-muted small" style="margin-left: 2px"><code style="font-size: 85% !important">override</code></span>
|
||||
</td>
|
||||
<td>{{ item.Type }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="4" class="text-muted text-center">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="4" class="text-muted text-center">No authorized users or teams.</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">
|
||||
<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>
|
@ -1,16 +0,0 @@
|
||||
angular.module('portainer.app').component('accessDatatable', {
|
||||
templateUrl: './accessDatatable.html',
|
||||
controller: 'AccessDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
roles: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
removeAction: '<',
|
||||
updateAction: '<',
|
||||
reverseOrder: '<',
|
||||
inheritFrom: '<',
|
||||
},
|
||||
});
|
@ -1,49 +0,0 @@
|
||||
angular.module('portainer.app').controller('AccessDatatableController', [
|
||||
'$scope',
|
||||
'$controller',
|
||||
'DatatableService',
|
||||
function ($scope, $controller, DatatableService) {
|
||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
||||
|
||||
this.disableRemove = function (item) {
|
||||
return item.Inherited && this.inheritFrom;
|
||||
};
|
||||
|
||||
this.allowSelection = function (item) {
|
||||
return !this.disableRemove(item);
|
||||
};
|
||||
|
||||
this.$onInit = function () {
|
||||
this.setDefaults();
|
||||
this.prepareTableFromDataset();
|
||||
|
||||
this.state.orderBy = this.orderBy;
|
||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||
if (storedOrder !== null) {
|
||||
this.state.reverseOrder = storedOrder.reverse;
|
||||
this.state.orderBy = storedOrder.orderBy;
|
||||
}
|
||||
|
||||
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||
if (textFilter !== null) {
|
||||
this.state.textFilter = textFilter;
|
||||
this.onTextFilterChange();
|
||||
}
|
||||
|
||||
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
||||
if (storedFilters !== null) {
|
||||
this.filters = storedFilters;
|
||||
}
|
||||
if (this.filters && this.filters.state) {
|
||||
this.filters.state.open = false;
|
||||
}
|
||||
|
||||
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
|
||||
if (storedSettings !== null) {
|
||||
this.settings = storedSettings;
|
||||
this.settings.open = false;
|
||||
}
|
||||
this.onSettingsRepeaterChange();
|
||||
};
|
||||
},
|
||||
]);
|
@ -0,0 +1,22 @@
|
||||
import angular from 'angular';
|
||||
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import { AccessDatatable } from '@/react/portainer/access-control/AccessManagement/AccessDatatable/AccessDatatable';
|
||||
|
||||
export const rbacModule = angular
|
||||
.module('portainer.app.react.components.rbac', [])
|
||||
.component(
|
||||
'accessDatatable',
|
||||
r2a(withUIRouter(withReactQuery(AccessDatatable)), [
|
||||
'dataset',
|
||||
'inheritFrom',
|
||||
'isUpdateEnabled',
|
||||
'onRemove',
|
||||
'onUpdate',
|
||||
'showRoles',
|
||||
'showWarning',
|
||||
'tableKey',
|
||||
])
|
||||
).name;
|
@ -0,0 +1,39 @@
|
||||
import { UserX } from 'lucide-react';
|
||||
|
||||
import { name } from '@/react/portainer/access-control/AccessManagement/AccessDatatable/columns/name';
|
||||
import { type } from '@/react/portainer/access-control/AccessManagement/AccessDatatable/columns/type';
|
||||
import { Access } from '@/react/portainer/access-control/AccessManagement/AccessDatatable/types';
|
||||
import { RemoveAccessButton } from '@/react/portainer/access-control/AccessManagement/AccessDatatable/RemoveAccessButton';
|
||||
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
import { Datatable } from '@@/datatables';
|
||||
|
||||
const tableKey = 'kubernetes_resourcepool_access';
|
||||
const columns = [name, type];
|
||||
const store = createPersistedStore(tableKey);
|
||||
|
||||
export function NamespaceAccessDatatable({
|
||||
dataset,
|
||||
onRemove,
|
||||
}: {
|
||||
dataset?: Array<Access>;
|
||||
onRemove(items: Array<Access>): void;
|
||||
}) {
|
||||
const tableState = useTableState(store, tableKey);
|
||||
|
||||
return (
|
||||
<Datatable
|
||||
data-cy="kube-namespace-access-datatable"
|
||||
title="Namespace Access"
|
||||
titleIcon={UserX}
|
||||
dataset={dataset || []}
|
||||
isLoading={!dataset}
|
||||
columns={columns}
|
||||
settingsManager={tableState}
|
||||
renderTableActions={(selectedItems) => (
|
||||
<RemoveAccessButton items={selectedItems} onClick={onRemove} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,197 @@
|
||||
import { Check, UserX } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import {
|
||||
TeamAccessViewModel,
|
||||
UserAccessViewModel,
|
||||
} from '@/portainer/models/access';
|
||||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
import { withMeta } from '@@/datatables/extend-options/withMeta';
|
||||
import { Button } from '@@/buttons';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
|
||||
import { useColumns } from './columns/useColumns';
|
||||
import { Access } from './types';
|
||||
import { RemoveAccessButton } from './RemoveAccessButton';
|
||||
|
||||
export function AccessDatatable({
|
||||
dataset,
|
||||
tableKey,
|
||||
onRemove,
|
||||
onUpdate,
|
||||
showWarning = false,
|
||||
isUpdateEnabled = false,
|
||||
showRoles = false,
|
||||
inheritFrom = false,
|
||||
}: {
|
||||
tableKey: string;
|
||||
dataset?: Array<Access>;
|
||||
onRemove(items: Array<Access>): void;
|
||||
onUpdate(
|
||||
users: Array<UserAccessViewModel>,
|
||||
teams: Array<TeamAccessViewModel>
|
||||
): void;
|
||||
showWarning?: boolean;
|
||||
isUpdateEnabled?: boolean;
|
||||
showRoles?: boolean;
|
||||
inheritFrom?: boolean;
|
||||
}) {
|
||||
const columns = useColumns({ showRoles, inheritFrom });
|
||||
const [store] = useState(() => createPersistedStore(tableKey));
|
||||
const tableState = useTableState(store, tableKey);
|
||||
const rolesState = useRolesState();
|
||||
|
||||
return (
|
||||
<Datatable
|
||||
data-cy="access-datatable"
|
||||
title="Access"
|
||||
titleIcon={UserX}
|
||||
dataset={dataset || []}
|
||||
isLoading={!dataset}
|
||||
columns={columns}
|
||||
settingsManager={tableState}
|
||||
extendTableOptions={withMeta({
|
||||
table: 'access-table',
|
||||
roles: rolesState,
|
||||
})}
|
||||
isRowSelectable={({ original: item }) => !inheritFrom || !item.Inherited}
|
||||
emptyContentLabel="No authorized users or teams."
|
||||
renderTableActions={(selectedItems) => (
|
||||
<>
|
||||
<RemoveAccessButton items={selectedItems} onClick={onRemove} />
|
||||
|
||||
{isBE && isUpdateEnabled && (
|
||||
<Button
|
||||
data-cy="update-access-button"
|
||||
icon={Check}
|
||||
disabled={rolesState.count === 0}
|
||||
onClick={handleUpdate}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
description={
|
||||
<div className="small text-muted mx-4 mb-4">
|
||||
{inheritFrom && (
|
||||
<>
|
||||
<div>
|
||||
Access tagged as <code>inherited</code> are inherited from the
|
||||
group access. They cannot be removed or modified at the
|
||||
environment level but they can be overridden.
|
||||
</div>
|
||||
<div>
|
||||
Access tagged as <code>override</code> are overriding the group
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isBE && showWarning && isUpdateEnabled && (
|
||||
<TextTip>
|
||||
<div className="text-warning-9 th-highcontrast:text-warning-1 th-dark:text-warning-7">
|
||||
Updating user access will require the affected user(s) to logout
|
||||
and login for the changes to be taken into account.
|
||||
</div>
|
||||
</TextTip>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
function handleUpdate() {
|
||||
const update = rolesState.getUpdate();
|
||||
const teamsAccess = getAccess(update.teams, 'team');
|
||||
const usersAccess = getAccess(update.users, 'user');
|
||||
|
||||
onUpdate(usersAccess, teamsAccess);
|
||||
|
||||
function getAccess(
|
||||
accesses: Record<number, number | undefined>,
|
||||
type: 'team' | 'user'
|
||||
) {
|
||||
return _.compact(
|
||||
Object.entries(accesses).map(([strId, role]) => {
|
||||
if (!strId || !role) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const id = parseInt(strId, 10);
|
||||
const entity = dataset?.find(
|
||||
(item) => item.Type === type && item.Id === id
|
||||
);
|
||||
if (!entity) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...entity,
|
||||
Role: {
|
||||
Id: role,
|
||||
Name: '',
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function useRolesState() {
|
||||
const [teamRoles, setTeamRoles] = useState<
|
||||
Record<number, number | undefined>
|
||||
>({});
|
||||
const [userRoles, setUserRoles] = useState<
|
||||
Record<number, number | undefined>
|
||||
>({});
|
||||
|
||||
const count = useMemo(
|
||||
() => Object.keys(teamRoles).length + Object.keys(userRoles).length,
|
||||
[teamRoles, userRoles]
|
||||
);
|
||||
|
||||
return { getRoleValue, setRolesValue, getUpdate, count };
|
||||
|
||||
function getRoleValue(id: number, entity: 'user' | 'team') {
|
||||
if (entity === 'team') {
|
||||
return teamRoles[id];
|
||||
}
|
||||
return userRoles[id];
|
||||
}
|
||||
|
||||
function setRolesValue(
|
||||
id: number,
|
||||
entity: 'user' | 'team',
|
||||
value: number | undefined
|
||||
) {
|
||||
if (entity === 'team') {
|
||||
setTeamRoles(updater);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setUserRoles(updater);
|
||||
|
||||
function updater(roles: Record<number, number | undefined>) {
|
||||
const newRoles = { ...roles };
|
||||
if (typeof value === 'undefined') {
|
||||
delete newRoles[id];
|
||||
} else {
|
||||
newRoles[id] = value;
|
||||
}
|
||||
return newRoles;
|
||||
}
|
||||
}
|
||||
|
||||
function getUpdate() {
|
||||
return {
|
||||
users: userRoles,
|
||||
teams: teamRoles,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import { DeleteButton } from '@@/buttons/DeleteButton';
|
||||
|
||||
import { Access } from './types';
|
||||
|
||||
export function RemoveAccessButton({
|
||||
onClick,
|
||||
items,
|
||||
}: {
|
||||
onClick(items: Array<Access>): void;
|
||||
items: Array<Access>;
|
||||
}) {
|
||||
return (
|
||||
<DeleteButton
|
||||
confirmMessage="Are you sure you want to unauthorized the selected users or teams?"
|
||||
onConfirmed={() => onClick(items)}
|
||||
disabled={items.length === 0}
|
||||
data-cy="remove-access-button"
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
|
||||
import { Access } from '../types';
|
||||
|
||||
export const helper = createColumnHelper<Access>();
|
@ -0,0 +1,22 @@
|
||||
import { helper } from './helper';
|
||||
|
||||
export const inheritedName = helper.accessor('Name', {
|
||||
cell({ row: { original: item }, getValue }) {
|
||||
const name = getValue();
|
||||
return (
|
||||
<>
|
||||
{name}
|
||||
{item.Inherited && (
|
||||
<span className="text-muted small">
|
||||
<code className="text-sm">inherited</code>
|
||||
</span>
|
||||
)}
|
||||
{item.Override && (
|
||||
<span className="text-muted small">
|
||||
<code className="text-sm">override</code>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
@ -0,0 +1,3 @@
|
||||
import { helper } from './helper';
|
||||
|
||||
export const name = helper.accessor('Name', {});
|
@ -0,0 +1,89 @@
|
||||
import { CellContext } from '@tanstack/react-table';
|
||||
import { Edit, X } from 'lucide-react';
|
||||
|
||||
import { useRbacRoles } from '@/react/portainer/users/RolesView/useRbacRoles';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
import { Select } from '@@/form-components/Input';
|
||||
|
||||
import { Access, getTableMeta } from '../types';
|
||||
|
||||
import { helper } from './helper';
|
||||
|
||||
export const role = helper.accessor('Role.Name', {
|
||||
cell: RoleCell,
|
||||
meta: {
|
||||
width: 320,
|
||||
},
|
||||
});
|
||||
|
||||
function RoleCell({
|
||||
row: { original: item, getCanSelect },
|
||||
table,
|
||||
getValue,
|
||||
}: CellContext<Access, string>) {
|
||||
const meta = getTableMeta(table.options.meta);
|
||||
const type = item.Type as 'team' | 'user';
|
||||
const updateValue = meta.roles.getRoleValue(item.Id, type);
|
||||
const role = getValue();
|
||||
|
||||
if (!getCanSelect()) {
|
||||
return <>{role}</>;
|
||||
}
|
||||
|
||||
if (typeof updateValue === 'undefined') {
|
||||
return (
|
||||
<>
|
||||
{role}
|
||||
<Button
|
||||
color="none"
|
||||
icon={Edit}
|
||||
onClick={() => meta.roles.setRolesValue(item.Id, type, item.Role.Id)}
|
||||
data-cy="edit-role-button"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<RollEdit
|
||||
value={updateValue}
|
||||
onChange={(value) => meta.roles.setRolesValue(item.Id, type, value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RollEdit({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: number;
|
||||
onChange(value?: number): void;
|
||||
}) {
|
||||
const rolesQuery = useRbacRoles({
|
||||
select: (roles) => roles.map((r) => ({ label: r.Name, value: r.Id })),
|
||||
});
|
||||
|
||||
if (!rolesQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 max-w-xs">
|
||||
<Select
|
||||
aria-label="Role"
|
||||
data-cy="role-select"
|
||||
value={value}
|
||||
options={rolesQuery.data}
|
||||
onChange={(e) => onChange(parseInt(e.target.value, 10))}
|
||||
/>
|
||||
<Button
|
||||
color="none"
|
||||
icon={X}
|
||||
onClick={() => onChange()}
|
||||
data-cy="cancel-role-button"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
import { helper } from './helper';
|
||||
|
||||
export const type = helper.accessor('Type', {});
|
@ -0,0 +1,27 @@
|
||||
import _ from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
|
||||
import { inheritedName } from './inheritedName';
|
||||
import { name } from './name';
|
||||
import { type } from './type';
|
||||
import { role } from './role';
|
||||
|
||||
export function useColumns({
|
||||
showRoles,
|
||||
inheritFrom,
|
||||
}: {
|
||||
showRoles: boolean;
|
||||
inheritFrom: boolean;
|
||||
}) {
|
||||
return useMemo(
|
||||
() =>
|
||||
_.compact([
|
||||
inheritFrom ? inheritedName : name,
|
||||
type,
|
||||
isBE && showRoles && role,
|
||||
]),
|
||||
[inheritFrom, showRoles]
|
||||
);
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import {
|
||||
TeamAccessViewModel,
|
||||
UserAccessViewModel,
|
||||
} from '@/portainer/models/access';
|
||||
|
||||
export type Access = UserAccessViewModel | TeamAccessViewModel;
|
||||
|
||||
export interface TableMeta {
|
||||
table: 'access-table';
|
||||
roles: {
|
||||
getRoleValue(id: number, entity: 'user' | 'team'): number | undefined;
|
||||
setRolesValue(
|
||||
id: number,
|
||||
entity: 'user' | 'team',
|
||||
value: number | undefined
|
||||
): void;
|
||||
};
|
||||
}
|
||||
|
||||
function isTableMeta(meta?: unknown): meta is TableMeta {
|
||||
return (
|
||||
!!meta &&
|
||||
typeof meta === 'object' &&
|
||||
'table' in meta &&
|
||||
meta.table === 'access-table'
|
||||
);
|
||||
}
|
||||
|
||||
export function getTableMeta(meta: unknown) {
|
||||
if (!isTableMeta(meta)) {
|
||||
throw new Error('missing table meta');
|
||||
}
|
||||
return meta;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
import { RbacRole } from './types';
|
||||
|
||||
export function useRbacRoles<T = Array<RbacRole>>({
|
||||
select,
|
||||
}: {
|
||||
select: (roles: Array<RbacRole>) => T;
|
||||
}) {
|
||||
return useQuery({
|
||||
select,
|
||||
queryKey: ['roles'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const { data } = await axios.get<Array<RbacRole>>('/roles');
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e, 'Failed to fetch roles');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
Loading…
Reference in new issue