mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 13:55:21 +02:00
refactor(users): migrate list view to react [EE-2202] (#11914)
This commit is contained in:
parent
33ce841040
commit
3c1441d462
43 changed files with 967 additions and 681 deletions
|
@ -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<
|
||||
|
|
16
app/react/portainer/users/ListView/ListView.tsx
Normal file
16
app/react/portainer/users/ListView/ListView.tsx
Normal file
|
@ -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[];
|
||||
}
|
109
app/react/portainer/users/ListView/NewUserForm/NewUserForm.tsx
Normal file
109
app/react/portainer/users/ListView/NewUserForm/NewUserForm.tsx
Normal file
|
@ -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', '');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
43
app/react/portainer/users/queries/useCreateUserMutation.ts
Normal file
43
app/react/portainer/users/queries/useCreateUserMutation.ts
Normal file
|
@ -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');
|
||||
}
|
||||
}
|
24
app/react/portainer/users/queries/useDeleteUserMutation.ts
Normal file
24
app/react/portainer/users/queries/useDeleteUserMutation.ts
Normal file
|
@ -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;
|
||||
}
|
15
app/react/portainer/users/teams/queries/build-url.ts
Normal file
15
app/react/portainer/users/teams/queries/build-url.ts
Normal file
|
@ -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;
|
||||
}
|
11
app/react/portainer/users/teams/queries/index.ts
Normal file
11
app/react/portainer/users/teams/queries/index.ts
Normal file
|
@ -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';
|
9
app/react/portainer/users/teams/queries/query-keys.ts
Normal file
9
app/react/portainer/users/teams/queries/query-keys.ts
Normal file
|
@ -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');
|
||||
}
|
||||
}
|
27
app/react/portainer/users/teams/queries/useTeam.ts
Normal file
27
app/react/portainer/users/teams/queries/useTeam.ts
Normal file
|
@ -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');
|
||||
}
|
||||
}
|
42
app/react/portainer/users/teams/queries/useTeams.ts
Normal file
42
app/react/portainer/users/teams/queries/useTeams.ts
Normal file
|
@ -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…
Add table
Add a link
Reference in a new issue