mirror of https://github.com/portainer/portainer
refactor(users): migrate list view to react [EE-2202] (#11914)
parent
33ce841040
commit
3c1441d462
|
@ -85,7 +85,7 @@ func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) er
|
|||
BucketName,
|
||||
&portainer.TeamMembership{},
|
||||
func(obj any) (id int, ok bool) {
|
||||
membership, ok := obj.(portainer.TeamMembership)
|
||||
membership, ok := obj.(*portainer.TeamMembership)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
|
||||
//return fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
|
||||
|
@ -106,7 +106,7 @@ func (service *Service) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) er
|
|||
BucketName,
|
||||
&portainer.TeamMembership{},
|
||||
func(obj any) (id int, ok bool) {
|
||||
membership, ok := obj.(portainer.TeamMembership)
|
||||
membership, ok := obj.(*portainer.TeamMembership)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
|
||||
//return fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
|
||||
|
@ -126,7 +126,7 @@ func (service *Service) DeleteTeamMembershipByTeamIDAndUserID(teamID portainer.T
|
|||
BucketName,
|
||||
&portainer.TeamMembership{},
|
||||
func(obj any) (id int, ok bool) {
|
||||
membership, ok := obj.(portainer.TeamMembership)
|
||||
membership, ok := obj.(*portainer.TeamMembership)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
|
||||
//return fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
|
||||
|
|
|
@ -381,8 +381,7 @@ angular
|
|||
url: '/users',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/users/users.html',
|
||||
controller: 'UsersController',
|
||||
component: 'usersListView',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
|
|
|
@ -2,17 +2,12 @@ import angular from 'angular';
|
|||
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import { UsersDatatable } from '@/react/portainer/users/ListView/UsersDatatable/UsersDatatable';
|
||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||
import { EffectiveAccessViewerDatatable } from '@/react/portainer/users/RolesView/AccessViewer/EffectiveAccessViewerDatatable';
|
||||
import { RbacRolesDatatable } from '@/react/portainer/users/RolesView/RbacRolesDatatable';
|
||||
|
||||
export const usersModule = angular
|
||||
.module('portainer.app.react.components.users', [])
|
||||
.component(
|
||||
'usersDatatable',
|
||||
r2a(withUIRouter(withCurrentUser(UsersDatatable)), ['dataset', 'onRemove'])
|
||||
)
|
||||
.component(
|
||||
'effectiveAccessViewerDatatable',
|
||||
r2a(withUIRouter(withCurrentUser(EffectiveAccessViewerDatatable)), [
|
||||
|
|
|
@ -20,6 +20,7 @@ import { environmentGroupModule } from './env-groups';
|
|||
import { registriesModule } from './registries';
|
||||
import { activityLogsModule } from './activity-logs';
|
||||
import { templatesModule } from './templates';
|
||||
import { usersModule } from './users';
|
||||
|
||||
export const viewsModule = angular
|
||||
.module('portainer.app.react.views', [
|
||||
|
@ -30,6 +31,7 @@ export const viewsModule = angular
|
|||
registriesModule,
|
||||
activityLogsModule,
|
||||
templatesModule,
|
||||
usersModule,
|
||||
])
|
||||
.component(
|
||||
'homeView',
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import { ListView } from '@/react/portainer/users/ListView/ListView';
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||
import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
|
||||
export const usersModule = angular
|
||||
.module('portainer.app.react.views.users', [])
|
||||
|
||||
.component(
|
||||
'usersListView',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(ListView))), [])
|
||||
).name;
|
|
@ -1,13 +1,12 @@
|
|||
import _ from 'lodash-es';
|
||||
|
||||
import { UserTokenModel, UserViewModel } from '@/portainer/models/user';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
import { getUsers } from '@/portainer/users/user.service';
|
||||
import { getUser } from '@/portainer/users/queries/useUser';
|
||||
|
||||
import { TeamMembershipModel } from '../../models/teamMembership';
|
||||
|
||||
/* @ngInject */
|
||||
export function UserService($q, Users, TeamService, TeamMembershipService) {
|
||||
export function UserService($q, Users, TeamService) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
|
@ -23,33 +22,6 @@ export function UserService($q, Users, TeamService, TeamMembershipService) {
|
|||
return new UserViewModel(user);
|
||||
};
|
||||
|
||||
service.createUser = function (username, password, role, teamIds) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
var payload = {
|
||||
username: username,
|
||||
password: password,
|
||||
role: role,
|
||||
};
|
||||
|
||||
Users.create({}, payload)
|
||||
.$promise.then(function success(data) {
|
||||
var userId = data.Id;
|
||||
var teamMembershipQueries = [];
|
||||
angular.forEach(teamIds, function (teamId) {
|
||||
teamMembershipQueries.push(TeamMembershipService.createMembership(userId, teamId, 2));
|
||||
});
|
||||
$q.all(teamMembershipQueries).then(function success() {
|
||||
deferred.resolve();
|
||||
});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to create user', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.deleteUser = function (id) {
|
||||
return Users.remove({ id: id }).$promise;
|
||||
};
|
||||
|
@ -112,27 +84,6 @@ export function UserService($q, Users, TeamService, TeamMembershipService) {
|
|||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.getAccessTokens = function (id) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Users.getAccessTokens({ id: id })
|
||||
.$promise.then(function success(data) {
|
||||
var userTokens = data.map(function (item) {
|
||||
return new UserTokenModel(item);
|
||||
});
|
||||
deferred.resolve(userTokens);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve user tokens', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.deleteAccessToken = function (id, tokenId) {
|
||||
return Users.deleteAccessToken({ id: id, tokenId: tokenId }).$promise;
|
||||
};
|
||||
|
||||
service.initAdministrator = function (username, password) {
|
||||
return Users.initAdminUser({ Username: username, Password: password }).$promise;
|
||||
};
|
||||
|
|
|
@ -1,169 +0,0 @@
|
|||
<page-header title="'Users'" breadcrumbs="['User management']" reload="true"> </page-header>
|
||||
|
||||
<div class="row" ng-if="isAdmin">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="plus" title-text="Add a new user"> </rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form name="form" class="form-horizontal">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="username" class="col-sm-3 col-lg-2 control-label required text-left">
|
||||
Username
|
||||
<portainer-tooltip ng-if="AuthenticationMethod === 2" message="'Username must exactly match username defined in external LDAP source.'"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
data-cy="user-usernameInput"
|
||||
class="form-control"
|
||||
id="username"
|
||||
ng-model="formValues.Username"
|
||||
ng-change="checkUsernameValidity()"
|
||||
placeholder="e.g. jdoe"
|
||||
auto-focus
|
||||
/>
|
||||
<span class="input-group-addon">
|
||||
<pr-icon mode="'success'" icon="'check'" ng-if="state.validUsername"></pr-icon>
|
||||
<pr-icon mode="'danger'" icon="'x'" ng-if="!state.validUsername"></pr-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- new-password-input -->
|
||||
<div class="form-group" ng-if="AuthenticationMethod === 1">
|
||||
<label for="password" class="col-sm-3 col-lg-2 control-label required text-left">Password</label>
|
||||
<div class="col-sm-8">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
ng-model="formValues.Password"
|
||||
id="password"
|
||||
name="password"
|
||||
ng-minlength="requiredPasswordLength"
|
||||
data-cy="user-passwordInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !new-password-input -->
|
||||
<!-- confirm-password-input -->
|
||||
<div class="form-group" ng-if="AuthenticationMethod === 1">
|
||||
<label for="confirm_password" class="col-sm-3 col-lg-2 control-label required text-left">Confirm password</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control form-control--has-icon"
|
||||
ng-model="formValues.ConfirmPassword"
|
||||
id="confirm_password"
|
||||
data-cy="user-passwordConfirmInput"
|
||||
/>
|
||||
<span class="input-group-addon">
|
||||
<pr-icon
|
||||
mode="'success'"
|
||||
icon="'check'"
|
||||
aria-hidden="true"
|
||||
ng-if="form.password.$viewValue !== '' && form.password.$viewValue === formValues.ConfirmPassword"
|
||||
></pr-icon>
|
||||
<pr-icon
|
||||
mode="'danger'"
|
||||
icon="'x'"
|
||||
aria-hidden="true"
|
||||
ng-if="!(form.password.$viewValue !== '' && form.password.$viewValue === formValues.ConfirmPassword)"
|
||||
></pr-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !confirm-password-input -->
|
||||
|
||||
<!-- password-check-hint -->
|
||||
<div class="form-group" ng-if="AuthenticationMethod === 1">
|
||||
<div class="col-sm-3 col-lg-2"></div>
|
||||
<div class="col-sm-8">
|
||||
<password-check-hint password-valid="form.password.$valid && formValues.Password"></password-check-hint>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ! password-check-hint -->
|
||||
|
||||
<!-- admin-checkbox -->
|
||||
<div class="form-group" ng-if="isAdmin">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
label-class="'col-sm-3 col-lg-2 control-label text-left'"
|
||||
checked="formValues.Administrator"
|
||||
label="'Administrator'"
|
||||
tooltip="'Administrators have access to Portainer settings management as well as full control over all defined environments and their resources.'"
|
||||
on-change="(handleAdministratorChange)"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !admin-checkbox -->
|
||||
<!-- teams -->
|
||||
<div class="form-group" ng-if="!formValues.Administrator">
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left" for="teams-selector">Add to team(s)</label>
|
||||
<div class="col-sm-8">
|
||||
<span class="small text-muted" ng-if="teams.length === 0">
|
||||
You don't seem to have any teams to add users into. Head over to the <a ui-sref="portainer.teams">Teams view</a> to create some.
|
||||
</span>
|
||||
<teams-selector
|
||||
ng-if="teams.length > 0"
|
||||
value="formValues.TeamIds"
|
||||
teams="teams"
|
||||
placeholder="'Select one or more teams'"
|
||||
data-cy="'user-teamSelect'"
|
||||
on-change="(onChangeTeamIds)"
|
||||
input-id="'teams-selector'"
|
||||
disabled="teamSync"
|
||||
></teams-selector>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="teamSync">
|
||||
<div class="col-sm-12">
|
||||
<span class="small">
|
||||
<p class="small text-muted vertical-center">
|
||||
<pr-icon icon="'alert-circle'" class-name="'icon-warning =vertical-center'"></pr-icon>
|
||||
The team leader feature is disabled as external authentication is currently enabled with team sync.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !teams -->
|
||||
<div class="form-group" ng-if="isAdmin && !formValues.Administrator && formValues.Teams.length === 0">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted vertical-center">
|
||||
<pr-icon class="vertical-center" icon="'alert-circle'" aria-hidden="true" mode="'primary'" size="'md'"></pr-icon>
|
||||
<span
|
||||
>Note: non-administrator users who aren't in a team don't have access to any environments by default. Head over to the
|
||||
<a ui-sref="portainer.endpoints">Environments view</a> to manage their accesses.</span
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm !ml-0"
|
||||
ng-disabled="state.actionInProgress || !state.validUsername || formValues.Username === '' || (AuthenticationMethod === 1 && (!formValues.Password || form.$invalid || formValues.Password !== formValues.ConfirmPassword))"
|
||||
ng-click="addUser()"
|
||||
button-spinner="state.actionInProgress"
|
||||
data-cy="user-createUserButton"
|
||||
>
|
||||
<span ng-hide="state.actionInProgress" class="vertical-center icon-white"> <pr-icon icon="'plus'" aria-hidden="true" size="'md'"></pr-icon> Create user</span>
|
||||
<span ng-show="state.actionInProgress">Creating user...</span>
|
||||
</button>
|
||||
<span class="text-danger" ng-if="state.userCreationError">
|
||||
<pr-icon icon="'alert-circle'" aria-hidden="true" mode="'primary'" size="'md'"></pr-icon> {{ state.userCreationError }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<users-datatable dataset="users" on-remove="(removeAction)"></users-datatable>
|
|
@ -1,143 +0,0 @@
|
|||
import _ from 'lodash-es';
|
||||
import { AuthenticationMethod } from '@/react/portainer/settings/types';
|
||||
import { processItemsInBatches } from '@/react/common/processItemsInBatches';
|
||||
|
||||
angular.module('portainer.app').controller('UsersController', [
|
||||
'$q',
|
||||
'$scope',
|
||||
'$state',
|
||||
'UserService',
|
||||
'TeamService',
|
||||
'TeamMembershipService',
|
||||
'Notifications',
|
||||
'Authentication',
|
||||
'SettingsService',
|
||||
function ($q, $scope, $state, UserService, TeamService, TeamMembershipService, Notifications, Authentication, SettingsService) {
|
||||
$scope.state = {
|
||||
userCreationError: '',
|
||||
validUsername: false,
|
||||
actionInProgress: false,
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
Username: '',
|
||||
Password: '',
|
||||
ConfirmPassword: '',
|
||||
Administrator: false,
|
||||
TeamIds: [],
|
||||
};
|
||||
|
||||
$scope.handleAdministratorChange = function (checked) {
|
||||
return $scope.$evalAsync(() => {
|
||||
$scope.formValues.Administrator = checked;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onChangeTeamIds = function (teamIds) {
|
||||
return $scope.$evalAsync(() => {
|
||||
$scope.formValues.TeamIds = teamIds;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.checkUsernameValidity = function () {
|
||||
var valid = true;
|
||||
for (var i = 0; i < $scope.users.length; i++) {
|
||||
if ($scope.formValues.Username.toLocaleLowerCase() === $scope.users[i].Username.toLocaleLowerCase()) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$scope.state.validUsername = valid;
|
||||
$scope.state.userCreationError = valid ? '' : 'Username already taken';
|
||||
};
|
||||
|
||||
$scope.addUser = function () {
|
||||
$scope.state.actionInProgress = true;
|
||||
$scope.state.userCreationError = '';
|
||||
var username = $scope.formValues.Username;
|
||||
var password = $scope.formValues.Password;
|
||||
var role = $scope.formValues.Administrator ? 1 : 2;
|
||||
UserService.createUser(username, password, role, $scope.formValues.TeamIds)
|
||||
.then(function success() {
|
||||
Notifications.success('User successfully created', username);
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create user');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
async function deleteSelectedUsers(selectedItems) {
|
||||
async function doRemove(user) {
|
||||
return UserService.deleteUser(user.Id)
|
||||
.then(function success() {
|
||||
Notifications.success('User successfully removed', user.Username);
|
||||
var index = $scope.users.indexOf(user);
|
||||
$scope.users.splice(index, 1);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove user');
|
||||
});
|
||||
}
|
||||
await processItemsInBatches(selectedItems, doRemove);
|
||||
$state.reload();
|
||||
}
|
||||
|
||||
$scope.removeAction = function (selectedItems) {
|
||||
return deleteSelectedUsers(selectedItems);
|
||||
};
|
||||
|
||||
function assignTeamLeaders(users, memberships) {
|
||||
for (var i = 0; i < users.length; i++) {
|
||||
var user = users[i];
|
||||
user.isTeamLeader = false;
|
||||
for (var j = 0; j < memberships.length; j++) {
|
||||
var membership = memberships[j];
|
||||
if (user.Id === membership.UserId && membership.Role === 1) {
|
||||
user.isTeamLeader = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initView() {
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
var isAdmin = Authentication.isAdmin();
|
||||
$scope.isAdmin = isAdmin;
|
||||
$q.all({
|
||||
users: UserService.users(true),
|
||||
teams: isAdmin ? TeamService.teams() : UserService.userLeadingTeams(userDetails.ID),
|
||||
memberships: TeamMembershipService.memberships(),
|
||||
settings: SettingsService.publicSettings(),
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.AuthenticationMethod = data.settings.AuthenticationMethod;
|
||||
var users = data.users;
|
||||
assignTeamLeaders(users, data.memberships);
|
||||
users = assignAuthMethod(users, $scope.AuthenticationMethod);
|
||||
$scope.users = users;
|
||||
$scope.teams = _.orderBy(data.teams, 'Name', 'asc');
|
||||
$scope.requiredPasswordLength = data.settings.RequiredPasswordLength;
|
||||
$scope.teamSync = data.settings.TeamSync;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve users and teams');
|
||||
$scope.users = [];
|
||||
$scope.teams = [];
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
},
|
||||
]);
|
||||
|
||||
function assignAuthMethod(users, authMethod) {
|
||||
return users.map((u) => ({
|
||||
...u,
|
||||
authMethod: AuthenticationMethod[u.Id === 1 ? AuthenticationMethod.Internal : authMethod],
|
||||
}));
|
||||
}
|
|
@ -9,6 +9,10 @@ import {
|
|||
|
||||
import { notifyError } from '@/portainer/services/notifications';
|
||||
|
||||
/**
|
||||
* @deprecated use withGlobalError
|
||||
* `onError` and other callbacks are not supported on react-query v5
|
||||
*/
|
||||
export function withError(fallbackMessage?: string, title = 'Failure') {
|
||||
return {
|
||||
onError(error: unknown) {
|
||||
|
@ -29,7 +33,7 @@ type OptionalReadonly<T> = T | Readonly<T>;
|
|||
|
||||
export function withInvalidate(
|
||||
queryClient: QueryClient,
|
||||
queryKeysToInvalidate: Array<OptionalReadonly<Array<string | number>>>,
|
||||
queryKeysToInvalidate: Array<OptionalReadonly<Array<unknown>>>,
|
||||
// skipRefresh will set the mutation state to success without waiting for the invalidated queries to refresh
|
||||
// see the following for info: https://tkdodo.eu/blog/mastering-mutations-in-react-query#awaited-promises
|
||||
{ skipRefresh }: { skipRefresh?: boolean } = {}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { PortainerSelect } from '@@/form-components/PortainerSelect';
|
|||
interface Props {
|
||||
name?: string;
|
||||
value: TeamId[] | readonly TeamId[];
|
||||
onChange(value: readonly TeamId[]): void;
|
||||
onChange(value: TeamId[]): void;
|
||||
teams: Team[];
|
||||
dataCy: string;
|
||||
inputId?: string;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PropsWithChildren } from 'react';
|
||||
import { ComponentProps, PropsWithChildren } from 'react';
|
||||
|
||||
import { AutomationTestingProps } from '@/types';
|
||||
|
||||
|
@ -12,6 +12,7 @@ interface Props extends AutomationTestingProps {
|
|||
isLoading: boolean;
|
||||
isValid: boolean;
|
||||
errors?: unknown;
|
||||
submitIcon?: ComponentProps<typeof LoadingButton>['icon'];
|
||||
}
|
||||
|
||||
export function FormActions({
|
||||
|
@ -21,6 +22,7 @@ export function FormActions({
|
|||
children,
|
||||
isValid,
|
||||
errors,
|
||||
submitIcon,
|
||||
'data-cy': dataCy,
|
||||
}: PropsWithChildren<Props>) {
|
||||
return (
|
||||
|
@ -34,6 +36,7 @@ export function FormActions({
|
|||
isLoading={isLoading}
|
||||
disabled={!isValid}
|
||||
data-cy={dataCy}
|
||||
icon={submitIcon}
|
||||
>
|
||||
{submitLabel}
|
||||
</LoadingButton>
|
||||
|
|
|
@ -36,7 +36,7 @@ interface SharedProps
|
|||
|
||||
interface MultiProps<TValue> extends SharedProps {
|
||||
value: readonly TValue[];
|
||||
onChange(value: readonly TValue[]): void;
|
||||
onChange(value: TValue[]): void;
|
||||
options: Options<TValue>;
|
||||
isMulti: true;
|
||||
components?: SelectComponentsConfig<
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import { PageHeader } from '@@/PageHeader';
|
||||
|
||||
import { NewUserForm } from './NewUserForm/NewUserForm';
|
||||
import { UsersDatatable } from './UsersDatatable/UsersDatatable';
|
||||
|
||||
export function ListView() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Users" breadcrumbs="User management" reload />
|
||||
|
||||
<NewUserForm />
|
||||
|
||||
<UsersDatatable />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { useField } from 'formik';
|
||||
|
||||
import { SwitchField } from '@@/form-components/SwitchField';
|
||||
|
||||
import { FormValues } from './FormValues';
|
||||
|
||||
export function AdminSwitch() {
|
||||
const [{ name, value }, , { setValue }] =
|
||||
useField<FormValues['isAdmin']>('isAdmin');
|
||||
return (
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<SwitchField
|
||||
data-cy="user-adminSwitch"
|
||||
label="Administrator"
|
||||
tooltip="Administrators have access to Portainer settings management as well as full control over all defined environments and their resources.'"
|
||||
checked={value}
|
||||
onChange={(checked) => setValue(checked)}
|
||||
name={name}
|
||||
labelClass="col-sm-3 col-lg-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { Check, XIcon } from 'lucide-react';
|
||||
import { useField } from 'formik';
|
||||
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { InputGroup } from '@@/form-components/InputGroup';
|
||||
import { Icon } from '@@/Icon';
|
||||
|
||||
import { FormValues } from './FormValues';
|
||||
|
||||
export function ConfirmPasswordField() {
|
||||
const [{ name, onBlur, onChange, value }, { error }] =
|
||||
useField<FormValues['confirmPassword']>('confirmPassword');
|
||||
return (
|
||||
<FormControl
|
||||
inputId="confirm_password"
|
||||
label="Confirm password"
|
||||
required
|
||||
errors={error}
|
||||
>
|
||||
<InputGroup>
|
||||
<InputGroup.Input
|
||||
id="confirm_password"
|
||||
name={name}
|
||||
data-cy="user-passwordConfirmInput"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
required
|
||||
type="password"
|
||||
autoComplete="one-time-code"
|
||||
/>
|
||||
<InputGroup.Addon>
|
||||
{error ? (
|
||||
<Icon mode="danger" icon={XIcon} />
|
||||
) : (
|
||||
<Icon mode="success" icon={Check} />
|
||||
)}
|
||||
</InputGroup.Addon>
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { TeamId } from '../../teams/types';
|
||||
|
||||
export interface FormValues {
|
||||
username: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
isAdmin: boolean;
|
||||
teams: TeamId[];
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
import { PlusIcon } from 'lucide-react';
|
||||
import { Form, Formik } from 'formik';
|
||||
|
||||
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
||||
import { AuthenticationMethod } from '@/react/portainer/settings/types';
|
||||
import { Role } from '@/portainer/users/types';
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
|
||||
import { Widget } from '@@/Widget';
|
||||
import { FormActions } from '@@/form-components/FormActions';
|
||||
import { PasswordCheckHint } from '@@/PasswordCheckHint';
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
|
||||
import { useTeams } from '../../teams/queries';
|
||||
import { useCreateUserMutation } from '../../queries/useCreateUserMutation';
|
||||
|
||||
import { UsernameField } from './UsernameField';
|
||||
import { PasswordField } from './PasswordField';
|
||||
import { ConfirmPasswordField } from './ConfirmPasswordField';
|
||||
import { FormValues } from './FormValues';
|
||||
import { TeamsFieldset } from './TeamsFieldset';
|
||||
import { useValidation } from './useValidation';
|
||||
|
||||
export function NewUserForm() {
|
||||
const { isPureAdmin } = useCurrentUser();
|
||||
const teamsQuery = useTeams(!isPureAdmin);
|
||||
const settingsQuery = usePublicSettings();
|
||||
const createUserMutation = useCreateUserMutation();
|
||||
const validation = useValidation();
|
||||
|
||||
if (!teamsQuery.data || !settingsQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { AuthenticationMethod: authMethod } = settingsQuery.data;
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Widget>
|
||||
<Widget.Title icon={PlusIcon} title="Add a new user" />
|
||||
<Widget.Body>
|
||||
<Formik<FormValues>
|
||||
initialValues={{
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
isAdmin: false,
|
||||
teams: [],
|
||||
}}
|
||||
validationSchema={validation}
|
||||
validateOnMount
|
||||
onSubmit={(values, { resetForm }) => {
|
||||
createUserMutation.mutate(
|
||||
{
|
||||
password: values.password,
|
||||
username: values.username,
|
||||
role: values.isAdmin ? Role.Admin : Role.Standard,
|
||||
teams: values.teams,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
notifySuccess(
|
||||
'User successfully created',
|
||||
values.username
|
||||
);
|
||||
resetForm();
|
||||
},
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
{({ errors, isValid }) => (
|
||||
<Form className="form-horizontal">
|
||||
<UsernameField authMethod={authMethod} />
|
||||
|
||||
{authMethod === AuthenticationMethod.Internal && (
|
||||
<>
|
||||
<PasswordField />
|
||||
|
||||
<ConfirmPasswordField />
|
||||
|
||||
<FormControl label="">
|
||||
<PasswordCheckHint passwordValid={!errors.password} />
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
|
||||
<TeamsFieldset />
|
||||
|
||||
<FormActions
|
||||
data-cy="user-createUserButton"
|
||||
submitLabel="Create user"
|
||||
isLoading={createUserMutation.isLoading}
|
||||
isValid={isValid}
|
||||
loadingText="Creating user..."
|
||||
errors={errors}
|
||||
submitIcon={PlusIcon}
|
||||
/>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { useField } from 'formik';
|
||||
|
||||
import { Input } from '@@/form-components/Input';
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
|
||||
import { FormValues } from './FormValues';
|
||||
|
||||
export function PasswordField() {
|
||||
const [{ name, onBlur, onChange, value }, { error }] =
|
||||
useField<FormValues['password']>('password');
|
||||
return (
|
||||
<FormControl label="Password" required inputId="psw-input" errors={error}>
|
||||
<Input
|
||||
type="password"
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
id="psw-input"
|
||||
data-cy="user-passwordInput"
|
||||
required
|
||||
autoComplete="one-time-code"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import { useField } from 'formik';
|
||||
|
||||
import { TeamsSelector } from '@@/TeamsSelector';
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
|
||||
import { Team } from '../../teams/types';
|
||||
|
||||
import { FormValues } from './FormValues';
|
||||
|
||||
export function TeamsField({
|
||||
teams,
|
||||
disabled,
|
||||
}: {
|
||||
teams: Array<Team>;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const [{ name, value }, { error }, { setValue }] =
|
||||
useField<FormValues['teams']>('teams');
|
||||
|
||||
return (
|
||||
<FormControl label="Add to team(s)" inputId="teams-field" errors={error}>
|
||||
<TeamsSelector
|
||||
dataCy="user-teamSelect"
|
||||
onChange={(value) => setValue(value)}
|
||||
value={value}
|
||||
name={name}
|
||||
teams={teams}
|
||||
inputId="teams-field"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
import { useFormikContext } from 'formik';
|
||||
|
||||
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
||||
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { Link } from '@@/Link';
|
||||
|
||||
import { useTeams } from '../../teams/queries';
|
||||
|
||||
import { AdminSwitch } from './AdminSwitch';
|
||||
import { FormValues } from './FormValues';
|
||||
import { TeamsField } from './TeamsField';
|
||||
|
||||
export function TeamsFieldset() {
|
||||
const { values } = useFormikContext<FormValues>();
|
||||
const { isPureAdmin } = useCurrentUser();
|
||||
const teamsQuery = useTeams(!isPureAdmin);
|
||||
const settingsQuery = usePublicSettings();
|
||||
if (!teamsQuery.data || !settingsQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { TeamSync: teamSync } = settingsQuery.data;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isPureAdmin && <AdminSwitch />}
|
||||
|
||||
{!values.isAdmin && (
|
||||
<TeamsField teams={teamsQuery.data} disabled={teamSync} />
|
||||
)}
|
||||
|
||||
{teamSync && <TeamSyncMessage />}
|
||||
|
||||
{isPureAdmin && !values.isAdmin && values.teams.length === 0 && (
|
||||
<NoTeamSelected />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TeamSyncMessage() {
|
||||
return (
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<TextTip color="orange">
|
||||
The team leader feature is disabled as external authentication is
|
||||
currently enabled with team sync.
|
||||
</TextTip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NoTeamSelected() {
|
||||
return (
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<TextTip color="blue">
|
||||
Note: non-administrator users who aren't in a team don't
|
||||
have access to any environments by default. Head over to the{' '}
|
||||
<Link to="portainer.endpoints" data-cy="env-link">
|
||||
Environments view
|
||||
</Link>{' '}
|
||||
to manage their accesses.
|
||||
</TextTip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import { Check, XIcon } from 'lucide-react';
|
||||
import { useField } from 'formik';
|
||||
|
||||
import { AuthenticationMethod } from '@/react/portainer/settings/types';
|
||||
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { InputGroup } from '@@/form-components/InputGroup';
|
||||
import { Icon } from '@@/Icon';
|
||||
|
||||
import { FormValues } from './FormValues';
|
||||
|
||||
export function UsernameField({
|
||||
authMethod,
|
||||
}: {
|
||||
authMethod: AuthenticationMethod;
|
||||
}) {
|
||||
const [{ name, onBlur, onChange, value }, { error }] =
|
||||
useField<FormValues['username']>('username');
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
inputId="username-field"
|
||||
label="Username"
|
||||
required
|
||||
errors={error}
|
||||
tooltip={
|
||||
authMethod === AuthenticationMethod.LDAP
|
||||
? 'Username must exactly match username defined in external LDAP source.'
|
||||
: null
|
||||
}
|
||||
>
|
||||
<InputGroup>
|
||||
<InputGroup.Input
|
||||
id="username-field"
|
||||
name={name}
|
||||
placeholder="e.g. jdoe"
|
||||
data-cy="user-usernameInput"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
required
|
||||
autoComplete="create-username"
|
||||
/>
|
||||
<InputGroup.Addon>
|
||||
{error ? (
|
||||
<Icon mode="danger" icon={XIcon} />
|
||||
) : (
|
||||
<Icon mode="success" icon={Check} />
|
||||
)}
|
||||
</InputGroup.Addon>
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import { SchemaOf, array, boolean, number, object, ref, string } from 'yup';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
||||
import { AuthenticationMethod } from '@/react/portainer/settings/types';
|
||||
import { useUsers } from '@/portainer/users/queries';
|
||||
|
||||
import { FormValues } from './FormValues';
|
||||
|
||||
export function useValidation(): SchemaOf<FormValues> {
|
||||
const usersQuery = useUsers(true);
|
||||
const settingsQuery = usePublicSettings();
|
||||
|
||||
const authMethod =
|
||||
settingsQuery.data?.AuthenticationMethod ?? AuthenticationMethod.Internal;
|
||||
|
||||
return useMemo(() => {
|
||||
const users = usersQuery.data ?? [];
|
||||
|
||||
const base = object({
|
||||
username: string()
|
||||
.required('Username is required')
|
||||
.test({
|
||||
name: 'unique',
|
||||
message: 'Username is already taken',
|
||||
test: (value) => users.every((u) => u.Username !== value),
|
||||
}),
|
||||
password: string().default(''),
|
||||
confirmPassword: string().default(''),
|
||||
isAdmin: boolean().default(false),
|
||||
teams: array(number().required()).required(),
|
||||
});
|
||||
|
||||
if (authMethod === AuthenticationMethod.Internal) {
|
||||
return base.concat(
|
||||
passwordValidation(settingsQuery.data?.RequiredPasswordLength)
|
||||
);
|
||||
}
|
||||
|
||||
return base;
|
||||
}, [authMethod, settingsQuery.data?.RequiredPasswordLength, usersQuery.data]);
|
||||
}
|
||||
|
||||
function passwordValidation(minLength: number | undefined = 12) {
|
||||
return object({
|
||||
password: string().required('Password is required').min(minLength, ''),
|
||||
confirmPassword: string().oneOf(
|
||||
[ref('password'), null],
|
||||
'Passwords must match'
|
||||
),
|
||||
});
|
||||
}
|
|
@ -1,24 +1,66 @@
|
|||
import { User as UserIcon } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { useUsers } from '@/portainer/users/queries';
|
||||
import { AuthenticationMethod } from '@/react/portainer/settings/types';
|
||||
import { useSettings } from '@/react/portainer/settings/queries';
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
import {
|
||||
mutationOptions,
|
||||
withError,
|
||||
withInvalidate,
|
||||
} from '@/react-tools/react-query';
|
||||
import { processItemsInBatches } from '@/react/common/processItemsInBatches';
|
||||
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { DeleteButton } from '@@/buttons/DeleteButton';
|
||||
|
||||
import { useTeamMemberships } from '../../teams/queries/useTeamMemberships';
|
||||
import { TeamId, TeamRole } from '../../teams/types';
|
||||
import { deleteUser } from '../../queries/useDeleteUserMutation';
|
||||
|
||||
import { columns } from './columns';
|
||||
import { DecoratedUser } from './types';
|
||||
|
||||
const store = createPersistedStore('users');
|
||||
|
||||
export function UsersDatatable({
|
||||
dataset,
|
||||
onRemove,
|
||||
}: {
|
||||
dataset?: Array<DecoratedUser>;
|
||||
onRemove: (selectedItems: Array<DecoratedUser>) => void;
|
||||
}) {
|
||||
export function UsersDatatable() {
|
||||
const { handleRemove } = useRemoveMutation();
|
||||
const { isPureAdmin } = useCurrentUser();
|
||||
const usersQuery = useUsers(isPureAdmin);
|
||||
const membershipsQuery = useTeamMemberships();
|
||||
const settingsQuery = useSettings();
|
||||
const tableState = useTableState(store, 'users');
|
||||
|
||||
const dataset: Array<DecoratedUser> | null = useMemo(() => {
|
||||
if (!usersQuery.data || !membershipsQuery.data || !settingsQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const memberships = membershipsQuery.data;
|
||||
|
||||
return usersQuery.data.map((user) => {
|
||||
const teamMembership = memberships.find(
|
||||
(membership) => membership.UserID === user.Id
|
||||
);
|
||||
|
||||
return {
|
||||
...user,
|
||||
isTeamLeader: teamMembership?.Role === TeamRole.Leader,
|
||||
authMethod:
|
||||
AuthenticationMethod[
|
||||
user.Id === 1
|
||||
? AuthenticationMethod.Internal
|
||||
: settingsQuery.data.AuthenticationMethod
|
||||
],
|
||||
};
|
||||
});
|
||||
}, [membershipsQuery.data, settingsQuery.data, usersQuery.data]);
|
||||
|
||||
return (
|
||||
<Datatable
|
||||
columns={columns}
|
||||
|
@ -32,7 +74,7 @@ export function UsersDatatable({
|
|||
<DeleteButton
|
||||
disabled={selectedItems.length === 0}
|
||||
confirmMessage="Do you want to remove the selected users? They will not be able to login into Portainer anymore."
|
||||
onConfirmed={() => onRemove(selectedItems)}
|
||||
onConfirmed={() => handleRemove(selectedItems.map((i) => i.Id))}
|
||||
data-cy="remove-users-button"
|
||||
/>
|
||||
)}
|
||||
|
@ -40,3 +82,25 @@ export function UsersDatatable({
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function useRemoveMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const deleteMutation = useMutation(
|
||||
async (ids: TeamId[]) => processItemsInBatches(ids, deleteUser),
|
||||
mutationOptions(
|
||||
withError('Unable to remove users'),
|
||||
withInvalidate(queryClient, [['users']])
|
||||
)
|
||||
);
|
||||
|
||||
return { handleRemove };
|
||||
|
||||
async function handleRemove(teams: TeamId[]) {
|
||||
deleteMutation.mutate(teams, {
|
||||
onSuccess: () => {
|
||||
notifySuccess('Teams successfully removed', '');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import { useQueryClient, useMutation } from '@tanstack/react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { withGlobalError, withInvalidate } from '@/react-tools/react-query';
|
||||
import { userQueryKeys } from '@/portainer/users/queries/queryKeys';
|
||||
import { buildUrl } from '@/portainer/users/user.service';
|
||||
import { Role, User } from '@/portainer/users/types';
|
||||
|
||||
import { TeamId, TeamRole } from '../teams/types';
|
||||
import { createTeamMembership } from '../teams/queries';
|
||||
|
||||
interface CreateUserPayload {
|
||||
username: string;
|
||||
password: string;
|
||||
role: Role;
|
||||
teams: Array<TeamId>;
|
||||
}
|
||||
|
||||
export function useCreateUserMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (values: CreateUserPayload) => {
|
||||
const user = await createUser(values);
|
||||
return Promise.all(
|
||||
values.teams.map((id) =>
|
||||
createTeamMembership(user.Id, id, TeamRole.Member)
|
||||
)
|
||||
);
|
||||
},
|
||||
...withInvalidate(queryClient, [userQueryKeys.base()]),
|
||||
...withGlobalError('Unable to create user'),
|
||||
});
|
||||
}
|
||||
|
||||
async function createUser(payload: CreateUserPayload) {
|
||||
try {
|
||||
const { data } = await axios.post<User>(buildUrl(), payload);
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw parseAxiosError(err, 'Unable to create user');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { withGlobalError, withInvalidate } from '@/react-tools/react-query';
|
||||
import { UserId } from '@/portainer/users/types';
|
||||
import { buildUrl } from '@/portainer/users/user.service';
|
||||
import { userQueryKeys } from '@/portainer/users/queries/queryKeys';
|
||||
|
||||
export function useDeleteUserMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: UserId) => deleteUser(id),
|
||||
...withGlobalError('Unable to delete user'),
|
||||
...withInvalidate(queryClient, [userQueryKeys.base()]),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteUser(id: UserId) {
|
||||
try {
|
||||
await axios.delete(buildUrl(id));
|
||||
} catch (error) {
|
||||
throw parseAxiosError(error);
|
||||
}
|
||||
}
|
|
@ -1,19 +1,13 @@
|
|||
import { useRouter } from '@uirouter/react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Users } from 'lucide-react';
|
||||
|
||||
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
||||
import {
|
||||
mutationOptions,
|
||||
withError,
|
||||
withInvalidate,
|
||||
} from '@/react-tools/react-query';
|
||||
|
||||
import { Widget } from '@@/Widget';
|
||||
import { DeleteButton } from '@@/buttons/DeleteButton';
|
||||
|
||||
import { Team, TeamId, TeamMembership, TeamRole } from '../types';
|
||||
import { deleteTeam } from '../teams.service';
|
||||
import { Team, TeamMembership, TeamRole } from '../types';
|
||||
import { useDeleteTeamMutation } from '../queries/useDeleteTeamMutation';
|
||||
|
||||
interface Props {
|
||||
team: Team;
|
||||
|
@ -22,7 +16,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export function Details({ team, memberships, isAdmin }: Props) {
|
||||
const deleteMutation = useDeleteTeam();
|
||||
const deleteMutation = useDeleteTeamMutation();
|
||||
const router = useRouter();
|
||||
const teamSyncQuery = usePublicSettings<boolean>({
|
||||
select: (settings) => settings.TeamSync,
|
||||
|
@ -80,15 +74,3 @@ export function Details({ team, memberships, isAdmin }: Props) {
|
|||
deleteMutation.mutate(team.Id);
|
||||
}
|
||||
}
|
||||
|
||||
function useDeleteTeam() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation(
|
||||
(id: TeamId) => deleteTeam(id),
|
||||
|
||||
mutationOptions(
|
||||
withError('Unable to delete team'),
|
||||
withInvalidate(queryClient, [['teams']])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Formik, Field, Form } from 'formik';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useReducer } from 'react';
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
|
@ -14,8 +13,8 @@ import { UsersSelector } from '@@/UsersSelector';
|
|||
import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
|
||||
import { createTeam } from '../../teams.service';
|
||||
import { Team } from '../../types';
|
||||
import { useAddTeamMutation } from '../../queries/useAddTeamMutation';
|
||||
|
||||
import { FormValues } from './types';
|
||||
import { validationSchema } from './CreateTeamForm.validation';
|
||||
|
@ -146,22 +145,3 @@ export function CreateTeamForm({ users, teams }: Props) {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function useAddTeamMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
(values: FormValues) => createTeam(values.name, values.leaders),
|
||||
{
|
||||
meta: {
|
||||
error: {
|
||||
title: 'Failure',
|
||||
message: 'Failed to create team',
|
||||
},
|
||||
},
|
||||
onSuccess() {
|
||||
return queryClient.invalidateQueries(['teams']);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import { ColumnDef } from '@tanstack/react-table';
|
|||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
import { promiseSequence } from '@/portainer/helpers/promise-utils';
|
||||
import { Team, TeamId } from '@/react/portainer/users/teams/types';
|
||||
import { deleteTeam } from '@/react/portainer/users/teams/teams.service';
|
||||
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { buildNameColumn } from '@@/datatables/buildNameColumn';
|
||||
|
@ -13,6 +12,8 @@ import { createPersistedStore } from '@@/datatables/types';
|
|||
import { useTableState } from '@@/datatables/useTableState';
|
||||
import { DeleteButton } from '@@/buttons/DeleteButton';
|
||||
|
||||
import { deleteTeam } from '../../queries/useDeleteTeamMutation';
|
||||
|
||||
const storageKey = 'teams';
|
||||
|
||||
const columns: ColumnDef<Team>[] = [
|
||||
|
|
|
@ -1,135 +0,0 @@
|
|||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { promiseSequence } from '@/portainer/helpers/promise-utils';
|
||||
import { notifyError } from '@/portainer/services/notifications';
|
||||
import { UserId } from '@/portainer/users/types';
|
||||
|
||||
import {
|
||||
createTeamMembership,
|
||||
deleteTeamMembership,
|
||||
updateTeamMembership,
|
||||
} from './team-membership.service';
|
||||
import { getTeam, getTeamMemberships, getTeams } from './teams.service';
|
||||
import { Team, TeamId, TeamMembership, TeamRole } from './types';
|
||||
|
||||
export function useTeams<T = Team[]>(
|
||||
onlyLedTeams = false,
|
||||
environmentId = 0,
|
||||
{
|
||||
enabled = true,
|
||||
select = (data) => data as unknown as T,
|
||||
}: {
|
||||
enabled?: boolean;
|
||||
select?: (data: Team[]) => T;
|
||||
} = {}
|
||||
) {
|
||||
const teams = useQuery(
|
||||
['teams', { onlyLedTeams, environmentId }],
|
||||
() => getTeams(onlyLedTeams, environmentId),
|
||||
{
|
||||
meta: {
|
||||
error: { title: 'Failure', message: 'Unable to load teams' },
|
||||
},
|
||||
enabled,
|
||||
select,
|
||||
}
|
||||
);
|
||||
|
||||
return teams;
|
||||
}
|
||||
|
||||
export function useTeam(id: TeamId, onError?: (error: unknown) => void) {
|
||||
return useQuery(['teams', id], () => getTeam(id), {
|
||||
meta: {
|
||||
error: { title: 'Failure', message: 'Unable to load team' },
|
||||
},
|
||||
onError,
|
||||
});
|
||||
}
|
||||
|
||||
export function useTeamMemberships(id: TeamId) {
|
||||
return useQuery(['teams', id, 'memberships'], () => getTeamMemberships(id), {
|
||||
meta: {
|
||||
error: { title: 'Failure', message: 'Unable to load team memberships' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useAddMemberMutation(teamId: TeamId) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
(userIds: UserId[]) =>
|
||||
promiseSequence(
|
||||
userIds.map(
|
||||
(userId) => () =>
|
||||
createTeamMembership(userId, teamId, TeamRole.Member)
|
||||
)
|
||||
),
|
||||
{
|
||||
onError(error) {
|
||||
notifyError('Failure', error as Error, 'Failure to add membership');
|
||||
},
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries(['teams', teamId, 'memberships']);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useRemoveMemberMutation(
|
||||
teamId: TeamId,
|
||||
teamMemberships: TeamMembership[] = []
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
(userIds: UserId[]) =>
|
||||
promiseSequence(
|
||||
userIds.map((userId) => () => {
|
||||
const membership = teamMemberships.find(
|
||||
(membership) => membership.UserID === userId
|
||||
);
|
||||
if (!membership) {
|
||||
throw new Error('Membership not found');
|
||||
}
|
||||
return deleteTeamMembership(membership.Id);
|
||||
})
|
||||
),
|
||||
{
|
||||
onError(error) {
|
||||
notifyError('Failure', error as Error, 'Failure to add membership');
|
||||
},
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries(['teams', teamId, 'memberships']);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useUpdateRoleMutation(
|
||||
teamId: TeamId,
|
||||
teamMemberships: TeamMembership[] = []
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
({ userId, role }: { userId: UserId; role: TeamRole }) => {
|
||||
const membership = teamMemberships.find(
|
||||
(membership) => membership.UserID === userId
|
||||
);
|
||||
if (!membership) {
|
||||
throw new Error('Membership not found');
|
||||
}
|
||||
return updateTeamMembership(membership.Id, userId, teamId, role);
|
||||
},
|
||||
{
|
||||
onError(error) {
|
||||
notifyError('Failure', error as Error, 'Failure to update membership');
|
||||
},
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries(['teams', teamId, 'memberships']);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { TeamMembershipId } from '../types';
|
||||
|
||||
export function buildMembershipUrl(id?: TeamMembershipId) {
|
||||
let url = '/team_memberships';
|
||||
|
||||
if (id) {
|
||||
url += `/${id}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { TeamId } from '../types';
|
||||
|
||||
export function buildUrl(id?: TeamId, action?: string) {
|
||||
let url = '/teams';
|
||||
|
||||
if (id) {
|
||||
url += `/${id}`;
|
||||
}
|
||||
|
||||
if (action) {
|
||||
url += `/${action}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
export { useTeams } from './useTeams';
|
||||
export {
|
||||
useAddMemberMutation,
|
||||
createTeamMembership,
|
||||
} from './useAddMemberMutation';
|
||||
export { useAddTeamMutation } from './useAddTeamMutation';
|
||||
export { deleteTeam, useDeleteTeamMutation } from './useDeleteTeamMutation';
|
||||
export { useRemoveMemberMutation } from './useRemoveMemberMutation';
|
||||
export { useTeam } from './useTeam';
|
||||
export { useTeamMemberships } from './useTeamMemberships';
|
||||
export { useUpdateRoleMutation } from './useUpdateRoleMutation';
|
|
@ -0,0 +1,9 @@
|
|||
import { TeamId } from '../types';
|
||||
|
||||
export const queryKeys = {
|
||||
base: () => ['teams'] as const,
|
||||
list: (params: unknown) => [...queryKeys.base(), 'list', params] as const,
|
||||
item: (id: TeamId) => [...queryKeys.base(), id] as const,
|
||||
memberships: (id?: TeamId) =>
|
||||
[...queryKeys.base(), 'memberships', id] as const,
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { promiseSequence } from '@/portainer/helpers/promise-utils';
|
||||
import { UserId } from '@/portainer/users/types';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { withGlobalError, withInvalidate } from '@/react-tools/react-query';
|
||||
|
||||
import { TeamId, TeamRole } from '../types';
|
||||
|
||||
import { buildMembershipUrl } from './build-membership-url';
|
||||
import { queryKeys } from './query-keys';
|
||||
|
||||
export function useAddMemberMutation(teamId: TeamId) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (userIds: UserId[]) =>
|
||||
promiseSequence(
|
||||
userIds.map(
|
||||
(userId) => () =>
|
||||
createTeamMembership(userId, teamId, TeamRole.Member)
|
||||
)
|
||||
),
|
||||
|
||||
...withGlobalError('Failure to add membership'),
|
||||
...withInvalidate(queryClient, [queryKeys.memberships(teamId)]),
|
||||
});
|
||||
}
|
||||
|
||||
export async function createTeamMembership(
|
||||
userId: UserId,
|
||||
teamId: TeamId,
|
||||
role: TeamRole
|
||||
) {
|
||||
try {
|
||||
await axios.post(buildMembershipUrl(), { userId, teamId, role });
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e, 'Unable to create team membership');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { UserId } from '@/portainer/users/types';
|
||||
import { withGlobalError, withInvalidate } from '@/react-tools/react-query';
|
||||
|
||||
import { TeamRole } from '../types';
|
||||
|
||||
import { createTeamMembership } from './useAddMemberMutation';
|
||||
import { buildUrl } from './build-url';
|
||||
import { queryKeys } from './query-keys';
|
||||
|
||||
interface CreatePayload {
|
||||
name: string;
|
||||
leaders: UserId[];
|
||||
}
|
||||
|
||||
export function useAddTeamMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: createTeam,
|
||||
...withGlobalError('Failed to create team'),
|
||||
...withInvalidate(queryClient, [queryKeys.base()]),
|
||||
});
|
||||
}
|
||||
|
||||
async function createTeam({ name, leaders }: CreatePayload) {
|
||||
try {
|
||||
const { data: team } = await axios.post(buildUrl(), { name });
|
||||
await Promise.all(
|
||||
leaders.map((leaderId) =>
|
||||
createTeamMembership(leaderId, team.Id, TeamRole.Leader)
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to create team');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import {
|
||||
mutationOptions,
|
||||
withGlobalError,
|
||||
withInvalidate,
|
||||
} from '@/react-tools/react-query';
|
||||
|
||||
import { TeamId } from '../types';
|
||||
|
||||
import { buildUrl } from './build-url';
|
||||
|
||||
export function useDeleteTeamMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation(
|
||||
(id: TeamId) => deleteTeam(id),
|
||||
|
||||
mutationOptions(
|
||||
withGlobalError('Unable to delete team'),
|
||||
withInvalidate(queryClient, [['teams']])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteTeam(id: TeamId) {
|
||||
try {
|
||||
await axios.delete(buildUrl(id));
|
||||
} catch (error) {
|
||||
throw parseAxiosError(error as Error);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { promiseSequence } from '@/portainer/helpers/promise-utils';
|
||||
import { UserId } from '@/portainer/users/types';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { withGlobalError, withInvalidate } from '@/react-tools/react-query';
|
||||
|
||||
import { TeamId, TeamMembership, TeamMembershipId } from '../types';
|
||||
|
||||
import { buildMembershipUrl } from './build-membership-url';
|
||||
import { queryKeys } from './query-keys';
|
||||
|
||||
export function useRemoveMemberMutation(
|
||||
teamId: TeamId,
|
||||
teamMemberships: TeamMembership[] = []
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (userIds: UserId[]) =>
|
||||
promiseSequence(
|
||||
userIds.map((userId) => () => {
|
||||
const membership = teamMemberships.find(
|
||||
(membership) => membership.UserID === userId
|
||||
);
|
||||
if (!membership) {
|
||||
throw new Error('Membership not found');
|
||||
}
|
||||
return deleteTeamMembership(membership.Id);
|
||||
})
|
||||
),
|
||||
...withGlobalError('Failure to remove membership'),
|
||||
...withInvalidate(queryClient, [queryKeys.memberships(teamId)]),
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteTeamMembership(id: TeamMembershipId) {
|
||||
try {
|
||||
await axios.delete(buildMembershipUrl(id));
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e, 'Unable to delete team membership');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { withGlobalError } from '@/react-tools/react-query';
|
||||
|
||||
import { Team, TeamId } from '../types';
|
||||
|
||||
import { buildUrl } from './build-url';
|
||||
import { queryKeys } from './query-keys';
|
||||
|
||||
export function useTeam(id: TeamId, onError?: (error: unknown) => void) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.item(id),
|
||||
queryFn: () => getTeam(id),
|
||||
...withGlobalError('Unable to load team'),
|
||||
onError,
|
||||
});
|
||||
}
|
||||
|
||||
async function getTeam(id: TeamId) {
|
||||
try {
|
||||
const { data } = await axios.get<Team>(buildUrl(id));
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw parseAxiosError(error as Error);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { withGlobalError } from '@/react-tools/react-query';
|
||||
|
||||
import { TeamId, TeamMembership } from '../types';
|
||||
|
||||
import { buildUrl } from './build-url';
|
||||
import { buildMembershipUrl } from './build-membership-url';
|
||||
import { queryKeys } from './query-keys';
|
||||
|
||||
export function useTeamMemberships(id?: TeamId) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.memberships(id),
|
||||
queryFn: () => (id ? getTeamMemberships(id) : getTeamsMemberships()),
|
||||
...withGlobalError('Unable to load team memberships'),
|
||||
});
|
||||
}
|
||||
|
||||
async function getTeamMemberships(teamId: TeamId) {
|
||||
try {
|
||||
const { data } = await axios.get<TeamMembership[]>(
|
||||
buildUrl(teamId, 'memberships')
|
||||
);
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to get team memberships');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTeamsMemberships() {
|
||||
try {
|
||||
const { data } = await axios.get<TeamMembership[]>(buildMembershipUrl());
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to get team memberships');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { withGlobalError } from '@/react-tools/react-query';
|
||||
|
||||
import { Team } from '../types';
|
||||
|
||||
import { buildUrl } from './build-url';
|
||||
import { queryKeys } from './query-keys';
|
||||
|
||||
export function useTeams<T = Team[]>(
|
||||
onlyLedTeams = false,
|
||||
environmentId = 0,
|
||||
{
|
||||
enabled = true,
|
||||
select = (data) => data as unknown as T,
|
||||
}: {
|
||||
enabled?: boolean;
|
||||
select?: (data: Team[]) => T;
|
||||
} = {}
|
||||
) {
|
||||
const teams = useQuery({
|
||||
queryKey: queryKeys.list({ onlyLedTeams, environmentId }),
|
||||
queryFn: () => getTeams(onlyLedTeams, environmentId),
|
||||
...withGlobalError('Unable to load teams'),
|
||||
enabled,
|
||||
select,
|
||||
});
|
||||
|
||||
return teams;
|
||||
}
|
||||
|
||||
async function getTeams(onlyLedTeams = false, environmentId = 0) {
|
||||
try {
|
||||
const { data } = await axios.get<Team[]>(buildUrl(), {
|
||||
params: { onlyLedTeams, environmentId },
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw parseAxiosError(error as Error);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { useQueryClient, useMutation } from '@tanstack/react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { UserId } from '@/portainer/users/types';
|
||||
import { withGlobalError, withInvalidate } from '@/react-tools/react-query';
|
||||
|
||||
import { TeamId, TeamMembership, TeamRole, TeamMembershipId } from '../types';
|
||||
|
||||
import { buildMembershipUrl } from './build-membership-url';
|
||||
import { queryKeys } from './query-keys';
|
||||
|
||||
export function useUpdateRoleMutation(
|
||||
teamId: TeamId,
|
||||
teamMemberships: TeamMembership[] = []
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ userId, role }: { userId: UserId; role: TeamRole }) => {
|
||||
const membership = teamMemberships.find(
|
||||
(membership) => membership.UserID === userId
|
||||
);
|
||||
if (!membership) {
|
||||
throw new Error('Membership not found');
|
||||
}
|
||||
return updateTeamMembership(membership.Id, userId, teamId, role);
|
||||
},
|
||||
...withGlobalError('Failure to update membership'),
|
||||
...withInvalidate(queryClient, [queryKeys.memberships(teamId)]),
|
||||
});
|
||||
}
|
||||
|
||||
async function updateTeamMembership(
|
||||
id: TeamMembershipId,
|
||||
userId: UserId,
|
||||
teamId: TeamId,
|
||||
role: TeamRole
|
||||
) {
|
||||
try {
|
||||
await axios.put(buildMembershipUrl(id), { userId, teamId, role });
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to update team membership');
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
import { UserId } from '@/portainer/users/types';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
import { TeamId, TeamRole, TeamMembershipId } from './types';
|
||||
|
||||
export async function createTeamMembership(
|
||||
userId: UserId,
|
||||
teamId: TeamId,
|
||||
role: TeamRole
|
||||
) {
|
||||
try {
|
||||
await axios.post(buildUrl(), { userId, teamId, role });
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to create team membership');
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteTeamMembership(id: TeamMembershipId) {
|
||||
try {
|
||||
await axios.delete(buildUrl(id));
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to delete team membership');
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateTeamMembership(
|
||||
id: TeamMembershipId,
|
||||
userId: UserId,
|
||||
teamId: TeamId,
|
||||
role: TeamRole
|
||||
) {
|
||||
try {
|
||||
await axios.put(buildUrl(id), { userId, teamId, role });
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to update team membership');
|
||||
}
|
||||
}
|
||||
|
||||
function buildUrl(id?: TeamMembershipId) {
|
||||
let url = '/team_memberships';
|
||||
|
||||
if (id) {
|
||||
url += `/${id}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { type UserId } from '@/portainer/users/types';
|
||||
|
||||
import { createTeamMembership } from './team-membership.service';
|
||||
import { Team, TeamId, TeamMembership, TeamRole } from './types';
|
||||
|
||||
export async function getTeams(onlyLedTeams = false, environmentId = 0) {
|
||||
try {
|
||||
const { data } = await axios.get<Team[]>(buildUrl(), {
|
||||
params: { onlyLedTeams, environmentId },
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw parseAxiosError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTeam(id: TeamId) {
|
||||
try {
|
||||
const { data } = await axios.get<Team>(buildUrl(id));
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw parseAxiosError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteTeam(id: TeamId) {
|
||||
try {
|
||||
await axios.delete(buildUrl(id));
|
||||
} catch (error) {
|
||||
throw parseAxiosError(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createTeam(name: string, leaders: UserId[]) {
|
||||
try {
|
||||
const { data: team } = await axios.post(buildUrl(), { name });
|
||||
await Promise.all(
|
||||
leaders.map((leaderId) =>
|
||||
createTeamMembership(leaderId, team.Id, TeamRole.Leader)
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to create team');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTeamMemberships(teamId: TeamId) {
|
||||
try {
|
||||
const { data } = await axios.get<TeamMembership[]>(
|
||||
buildUrl(teamId, 'memberships')
|
||||
);
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to get team memberships');
|
||||
}
|
||||
}
|
||||
|
||||
function buildUrl(id?: TeamId, action?: string) {
|
||||
let url = '/teams';
|
||||
|
||||
if (id) {
|
||||
url += `/${id}`;
|
||||
}
|
||||
|
||||
if (action) {
|
||||
url += `/${action}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
Loading…
Reference in New Issue