Skip to content

Commit

Permalink
Add create new tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
tznamena committed May 8, 2024
1 parent 9e3c66f commit f5c394b
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 6 deletions.
45 changes: 45 additions & 0 deletions frontend/awx/access/users/UserPage/UserTokenSecretsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { t } from 'i18next';
import { SetStateAction } from 'react';
import { Modal, ClipboardCopy, ClipboardCopyVariant } from '@patternfly/react-core';
import { PageDetails, PageDetail } from '../../../../../framework';
import { formatDateString } from '../../../../../framework/utils/formatDateString';
import { Token } from '../../../interfaces/Token';

export function UserTokenSecretsModal(props: {
onClose: (value: SetStateAction<Token | undefined>) => void;
newToken: Token;
}) {
const { token, refresh_token } = props.newToken;
return (
<Modal
aria-label={t`Token information`}
isOpen
variant="medium"
position="top"
title={t('Token information')}
onClose={() => {
props.onClose(undefined);
}}
hasNoBodyWrapper
>
<PageDetails
alertPrompts={[t`This is the only time the token will be shown.`]}
numberOfColumns="single"
>
<PageDetail label={t`Token`}>
<ClipboardCopy isReadOnly variant={ClipboardCopyVariant.expansion}>
{token}
</ClipboardCopy>
</PageDetail>
{refresh_token && (
<PageDetail label={t`Refresh Token`}>
<ClipboardCopy isReadOnly variant={ClipboardCopyVariant.expansion}>
{refresh_token}
</ClipboardCopy>
</PageDetail>
)}
<PageDetail label={t`Expires`}>{formatDateString(props.newToken.expires)}</PageDetail>
</PageDetails>
</Modal>
);
}
7 changes: 4 additions & 3 deletions frontend/awx/access/users/UserPage/UserTokens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
IPageAction,
PageActionType,
PageActionSelection,
useGetPageUrl,
} from '../../../../../framework';
import { AwxRoute } from '../../../main/AwxRoutes';
import { useAwxActiveUser } from '../../../common/useAwxActiveUser';
Expand All @@ -22,7 +23,6 @@ import { DetailInfo } from '../../../../../framework/components/DetailInfo';
import { useUserTokensFilters } from '../hooks/useUserTokensFilters';
import { ButtonVariant } from '@patternfly/react-core';
import { PlusCircleIcon, TrashIcon } from '@patternfly/react-icons';
//import { useDeleteTokens } from '../../../administration/applications/hooks/useDeleteTokens';
import { useDeleteUserTokens } from '../hooks/useDeleteUserTokens';

export function UserTokens(props: { infoMessage?: string }) {
Expand All @@ -49,6 +49,7 @@ export function UserTokens(props: { infoMessage?: string }) {
function UserTokensInternal(props: { infoMessage?: string; user: AwxUser }) {
const { user } = props;
const { t } = useTranslation();
const getPageUrl = useGetPageUrl();

const tableColumns = useUserTokensColumns(user);
const toolbarFilters = useUserTokensFilters();
Expand All @@ -69,7 +70,7 @@ function UserTokensInternal(props: { infoMessage?: string; user: AwxUser }) {
isPinned: true,
icon: PlusCircleIcon,
label: t('Create token'),
href: '',
href: getPageUrl(AwxRoute.CreateUserToken, { params: { id: user.id } }),
},
{ type: PageActionType.Seperator },
{
Expand All @@ -81,7 +82,7 @@ function UserTokensInternal(props: { infoMessage?: string; user: AwxUser }) {
onClick: deleteTokens,
},
],
[deleteTokens, t]
[deleteTokens, getPageUrl, t, user.id]
);

const rowActions = useMemo<IPageAction<Token>[]>(
Expand Down
115 changes: 115 additions & 0 deletions frontend/awx/access/users/UserTokenForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router-dom';
import {
LoadingPage,
PageFormSelect,
PageFormSubmitHandler,
PageHeader,
useGetPageUrl,
usePageNavigate,
} from '../../../../framework';
import { useAwxActiveUser } from '../../common/useAwxActiveUser';
import { AwxRoute } from '../../main/AwxRoutes';
import { AwxUser } from '../../interfaces/User';
import { Token } from '../../interfaces/Token';
import { AwxPageForm } from '../../common/AwxPageForm';
import { PageFormTextInput } from '../../../../framework';
import { usePostRequest } from '../../../common/crud/usePostRequest';
import { awxAPI } from '../../common/api/awx-utils';
import { PageFormApplicationSelect } from '../../administration/applications/components/PageFormApplicationSelect';

export function CreateUserToken(props: { onSuccessfulCreate: (newToken: Token) => void }) {
const params = useParams<{ id: string }>();
const { activeAwxUser } = useAwxActiveUser();
const pageNavigate = usePageNavigate();

useEffect(() => {
if (activeAwxUser === undefined || activeAwxUser?.id.toString() !== params.id) {
// redirect to user details for the active/logged-in user
pageNavigate(AwxRoute.UserDetails, { params: { id: activeAwxUser?.id } });
}
}, [activeAwxUser, params.id, pageNavigate]);

if (!activeAwxUser) return <LoadingPage breadcrumbs tabs />;

return activeAwxUser?.id.toString() === params.id ? (
<CreateUserTokenInternal user={activeAwxUser} onCreate={props.onSuccessfulCreate} />
) : (
<></>
);
}

function CreateUserTokenInternal(props: { user: AwxUser; onCreate: (newToken: Token) => void }) {
const { user } = props;
const { t } = useTranslation();
const getPageUrl = useGetPageUrl();
const navigate = useNavigate();
const postRequest = usePostRequest<Token, Token>();
const pageNavigate = usePageNavigate();

const onCancel = () => navigate(-1);
const onSubmit: PageFormSubmitHandler<Token> = async (tokenInput) => {
const newToken = await postRequest(awxAPI`/tokens/`, tokenInput);
props.onCreate(newToken);
pageNavigate(AwxRoute.UserTokenDetails, { params: { id: user.id, tokenid: newToken.id } });
};

return (
<>
<PageHeader
title={t('Create Token')}
breadcrumbs={[
{ label: t('Users'), to: getPageUrl(AwxRoute.Users) },
{
label: user.username,
to: getPageUrl(AwxRoute.UserDetails, { params: { id: user.id } }),
},
{
label: t('Tokens'),
to: getPageUrl(AwxRoute.UserTokens, { params: { id: user.id } }),
},
]}
/>
<AwxPageForm
submitText={t('Create token')}
onSubmit={onSubmit}
cancelText={t('Cancel')}
onCancel={onCancel}
>
<UserTokenFormInputs />
</AwxPageForm>
</>
);
}

function UserTokenFormInputs() {
const { t } = useTranslation();
return (
<>
<PageFormApplicationSelect name="application" isRequired={false} />
<PageFormTextInput<Token>
name="description"
label={t('Description')}
placeholder={t('Enter token description')}
isRequired={false}
/>
<PageFormSelect<Token>
name="scope"
label={t('Scope')}
placeholderText={t('Select a scope')}
options={[
{
label: t('Read'),
value: 'read',
},
{
label: t('Write'),
value: 'write',
},
]}
isRequired
/>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { FieldPath, FieldValues } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { PageFormSingleSelectAwxResource } from '../../../common/PageFormSingleSelectAwxResource';
import { awxAPI } from '../../../common/api/awx-utils';
import { useApplicationsColumns } from '../hooks/useApplicationsColumns';
import { useApplicationsFilters } from '../hooks/useApplicationsFilters';
import { Application } from '../../../interfaces/Application';

export function PageFormApplicationSelect<
TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>(props: { name: TFieldName; isRequired?: boolean; isDisabled?: string; helperText?: string }) {
const { t } = useTranslation();
const applicationColumns = useApplicationsColumns({ disableLinks: true });
const applicationFilters = useApplicationsFilters();
return (
<PageFormSingleSelectAwxResource<Application, TFieldValues, TFieldName>
name={props.name}
id="application"
label={t('Application')}
placeholder={t('Select application')}
queryPlaceholder={t('Loading applications...')}
queryErrorText={t('Error loading applications')}
isRequired={props.isRequired}
isDisabled={props.isDisabled}
helperText={props.helperText}
url={awxAPI`/applications/`}
tableColumns={applicationColumns}
toolbarFilters={applicationFilters}
/>
);
}
5 changes: 5 additions & 0 deletions frontend/awx/interfaces/Token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,9 @@ export interface Token {
application: number;
expires: string;
scope: string;
/** Token */
token?: string;
/** Refresh token */
refresh_token?: string;
/** Application */
}
1 change: 1 addition & 0 deletions frontend/awx/main/AwxRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ export enum AwxRoute {
UserTeams = 'awx-user-teams',
UserRoles = 'awx-user-roles',
UserTokens = 'awx-user-tokens',
CreateUserToken = 'awx-user-token-create',
UserTokenPage = 'awx-user-token-page',
UserTokenDetails = 'awx-user-token-detail',
AddRolesToUser = 'awx-add-roles-to-user',
Expand Down
26 changes: 23 additions & 3 deletions frontend/awx/main/routes/useAwxUsersRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Navigate } from 'react-router-dom';
import { PageNavigationItem } from '../../../../framework';
Expand All @@ -14,9 +14,14 @@ import { Users } from '../../access/users/Users';
import { AddRolesToUser } from '../../access/users/components/AddRolesToUser';
import { AwxRoute } from '../AwxRoutes';
import { UserTokenPage } from '../../access/users/UserPage/UserTokenPage';
import { CreateUserToken } from '../../access/users/UserTokenForm';
import { Token } from '../../interfaces/Token';
import { UserTokenSecretsModal } from '../../access/users/UserPage/UserTokenSecretsModal';

export function useAwxUsersRoutes() {
const { t } = useTranslation();
const [newUserToken, setNewUserToken] = useState<Token>();

const usersRoutes = useMemo<PageNavigationItem>(
() => ({
id: AwxRoute.Users,
Expand Down Expand Up @@ -74,10 +79,25 @@ export function useAwxUsersRoutes() {
path: ':id/roles/add-roles',
element: <AddRolesToUser />,
},
{
id: AwxRoute.CreateUserToken,
path: ':id/tokens/create',
element: <CreateUserToken onSuccessfulCreate={(t: Token) => setNewUserToken(t)} />,
},
{
id: AwxRoute.UserTokenPage,
path: ':id/tokens/:tokenid',
element: <UserTokenPage />,
element: (
<>
<UserTokenPage />
{newUserToken && (
<UserTokenSecretsModal
onClose={setNewUserToken}
newToken={newUserToken}
></UserTokenSecretsModal>
)}
</>
),
children: [
{
id: AwxRoute.UserTokenDetails,
Expand All @@ -96,7 +116,7 @@ export function useAwxUsersRoutes() {
},
],
}),
[t]
[newUserToken, t]
);
return usersRoutes;
}

0 comments on commit f5c394b

Please sign in to comment.