Fix #4559 UI: Add support to get JWT token for bot users (#4727)

This commit is contained in:
Sachin Chaurasiya 2022-05-09 23:49:16 +05:30 committed by GitHub
parent 92913c6eaf
commit 642735dfb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 835 additions and 6 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -123,3 +123,20 @@ export const getUserCounts = () => {
export const deleteUser = (id: string) => {
return APIClient.delete(`/users/${id}`);
};
export const getUserToken: Function = (id: string): Promise<AxiosResponse> => {
return APIClient.get(`/users/token/${id}`);
};
export const generateUserToken: Function = (
id: string,
data: Record<string, string>
): Promise<AxiosResponse> => {
return APIClient.put(`/users/generateToken/${id}`, data);
};
export const revokeUserToken: Function = (
id: string
): Promise<AxiosResponse> => {
return APIClient.put(`/users/revokeToken/${id}`);
};

View File

@ -0,0 +1,437 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { AxiosError, AxiosResponse } from 'axios';
import classNames from 'classnames';
import { isNil } from 'lodash';
import moment from 'moment';
import React, {
FC,
Fragment,
HTMLAttributes,
useEffect,
useState,
} from 'react';
import Select, { SingleValue } from 'react-select';
import { generateUserToken, getUserToken } from '../../axiosAPIs/userAPI';
import { ROUTES } from '../../constants/constants';
import { JWTTokenExpiry, User } from '../../generated/entity/teams/user';
import { EntityReference } from '../../generated/type/entityReference';
import { getEntityName, requiredField } from '../../utils/CommonUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import { Button } from '../buttons/Button/Button';
import CopyToClipboardButton from '../buttons/CopyToClipboardButton/CopyToClipboardButton';
import Description from '../common/description/Description';
import { reactSingleSelectCustomStyle } from '../common/react-select-component/reactSelectCustomStyle';
import TitleBreadcrumb from '../common/title-breadcrumb/title-breadcrumb.component';
import PageContainerV1 from '../containers/PageContainerV1';
import PageLayout from '../containers/PageLayout';
import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
import { UserDetails } from '../Users/Users.interface';
interface BotsDetailProp extends HTMLAttributes<HTMLDivElement> {
botsData: User;
updateBotsDetails: (data: UserDetails) => void;
revokeTokenHandler: () => void;
}
interface Option {
value: string;
label: string;
}
const BotsDetail: FC<BotsDetailProp> = ({
botsData,
updateBotsDetails,
revokeTokenHandler,
}) => {
const [displayName, setDisplayName] = useState(botsData.displayName);
const [isDisplayNameEdit, setIsDisplayNameEdit] = useState(false);
const [isDescriptionEdit, setIsDescriptionEdit] = useState(false);
const [botsToken, setBotsToken] = useState<string>('');
const [isRevokingToken, setIsRevokingToken] = useState<boolean>(false);
const [isRegeneratingToken, setIsRegeneratingToken] =
useState<boolean>(false);
const [generateToken, setGenerateToken] = useState<boolean>(false);
const [selectedExpiry, setSelectedExpiry] = useState('7');
const getJWTTokenExpiryOptions = () => {
return Object.keys(JWTTokenExpiry).map((expiry) => {
const expiryValue = JWTTokenExpiry[expiry as keyof typeof JWTTokenExpiry];
return { label: `${expiryValue} days`, value: expiryValue };
});
};
const getExpiryDateText = () => {
if (selectedExpiry === JWTTokenExpiry.Unlimited) {
return <p className="tw-mt-2">The token will never expire!</p>;
} else {
return (
<p className="tw-mt-2">
The token will expire on{' '}
{moment().add(selectedExpiry, 'days').format('ddd Do MMMM, YYYY')}
</p>
);
}
};
const handleOnChange = (
value: SingleValue<unknown>,
{ action }: { action: string }
) => {
if (isNil(value) || action === 'clear') {
setSelectedExpiry('');
} else {
const selectedValue = value as Option;
setSelectedExpiry(selectedValue.value);
}
};
const fetchBotsToken = () => {
getUserToken(botsData.id)
.then((res: AxiosResponse) => {
const { JWTToken } = res.data;
setBotsToken(JWTToken);
})
.catch((err: AxiosError) => {
showErrorToast(err);
});
};
const generateBotsToken = (data: Record<string, string>) => {
generateUserToken(botsData.id, data)
.then((res: AxiosResponse) => {
const { JWTToken } = res.data;
setBotsToken(JWTToken);
})
.catch((err: AxiosError) => {
showErrorToast(err);
})
.finally(() => {
setGenerateToken(false);
});
};
const handleTokenGeneration = () => {
if (botsToken) {
setIsRegeneratingToken(true);
} else {
setGenerateToken(true);
}
};
const handleGenerate = () => {
const data = {
JWTToken: 'string',
JWTTokenExpiry: selectedExpiry,
};
generateBotsToken(data);
};
const onDisplayNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setDisplayName(e.target.value);
};
const handleDisplayNameChange = () => {
if (displayName !== botsData.displayName) {
updateBotsDetails({ displayName: displayName || '' });
}
setIsDisplayNameEdit(false);
};
const handleDescriptionChange = (description: string) => {
if (description !== botsData.description) {
updateBotsDetails({ description });
}
setIsDescriptionEdit(false);
};
const getDisplayNameComponent = () => {
return (
<div className="tw-mt-4 tw-w-full">
{isDisplayNameEdit ? (
<div className="tw-flex tw-items-center tw-gap-1">
<input
className="tw-form-inputs tw-px-3 tw-py-0.5 tw-w-64"
data-testid="displayName"
id="displayName"
name="displayName"
placeholder="displayName"
type="text"
value={displayName}
onChange={onDisplayNameChange}
/>
<div className="tw-flex tw-justify-end" data-testid="buttons">
<Button
className="tw-px-1 tw-py-1 tw-rounded tw-text-sm tw-mr-1"
data-testid="cancel-displayName"
size="custom"
theme="primary"
variant="contained"
onMouseDown={() => setIsDisplayNameEdit(false)}>
<FontAwesomeIcon className="tw-w-3.5 tw-h-3.5" icon="times" />
</Button>
<Button
className="tw-px-1 tw-py-1 tw-rounded tw-text-sm"
data-testid="save-displayName"
size="custom"
theme="primary"
variant="contained"
onClick={handleDisplayNameChange}>
<FontAwesomeIcon className="tw-w-3.5 tw-h-3.5" icon="check" />
</Button>
</div>
</div>
) : (
<Fragment>
{displayName ? (
<span className="tw-text-base tw-font-medium tw-mr-2">
{displayName}
</span>
) : (
<span className="tw-no-description tw-text-sm">
Add display name
</span>
)}
<button
className="tw-ml-2 focus:tw-outline-none"
data-testid="edit-displayName"
onClick={() => setIsDisplayNameEdit(true)}>
<SVGIcons alt="edit" icon="icon-edit" title="Edit" width="12px" />
</button>
</Fragment>
)}
</div>
);
};
const getDescriptionComponent = () => {
return (
<div className="tw--ml-5">
<Description
hasEditAccess
description={botsData.description || ''}
entityName={getEntityName(botsData as unknown as EntityReference)}
isEdit={isDescriptionEdit}
onCancel={() => setIsDescriptionEdit(false)}
onDescriptionEdit={() => setIsDescriptionEdit(true)}
onDescriptionUpdate={handleDescriptionChange}
/>
</div>
);
};
const fetchLeftPanel = () => {
return (
<div data-testid="left-panel">
<div className="tw-pb-4 tw-mb-4 tw-border-b tw-flex tw-flex-col">
<div className="tw-h-28 tw-w-28">
<SVGIcons
alt="bot-profile"
icon={Icons.BOT_PROFILE}
width="112px"
/>
</div>
{getDisplayNameComponent()}
{getDescriptionComponent()}
</div>
</div>
);
};
const fetchRightPanel = () => {
return (
<div data-testid="right-panel">
<div className="tw-pb-4 tw-mb-4 tw-border-b tw-flex tw-flex-col">
<h6 className="tw-mb-2 tw-text-lg">Token Security</h6>
<p className="tw-mb-2">
Anyone who has your JWT Token will be able to send REST API requests
to the OpenMetadata Server. Do not expose the JWT Token in your
application code. Do not share it on GitHub or anywhere else online.
</p>
</div>
</div>
);
};
const getCopyComponent = () => {
if (botsToken) {
return <CopyToClipboardButton copyText={botsToken} />;
} else {
return null;
}
};
const centerLayout = () => {
if (generateToken) {
return (
<div className="tw-mt-4">
<div data-testid="filter-dropdown">
<label htmlFor="expiration">{requiredField('Expiration')}</label>
<Select
defaultValue={{ label: '7 days', value: '7' }}
id="expiration"
isSearchable={false}
options={getJWTTokenExpiryOptions()}
styles={reactSingleSelectCustomStyle}
onChange={handleOnChange}
/>
{getExpiryDateText()}
</div>
<div className="tw-flex tw-justify-end">
<Button
className={classNames('tw-mr-2')}
data-testid="discard-button"
size="regular"
theme="primary"
variant="text"
onClick={() => setGenerateToken(false)}>
Cancel
</Button>
<Button
data-testid="confirm-button"
size="regular"
theme="primary"
type="submit"
variant="contained"
onClick={handleGenerate}>
Generate
</Button>
</div>
</div>
);
} else {
if (botsToken) {
return (
<Fragment>
<div className="tw-flex tw-justify-between tw-items-center tw-mt-4">
<input
disabled
className="tw-form-inputs tw-p-1.5"
placeholder="Generate new token..."
type="password"
value={botsToken}
/>
{getCopyComponent()}
</div>
</Fragment>
);
} else {
return (
<div className="tw-no-description tw-text-sm tw-mt-4">
No token available
</div>
);
}
}
};
const getCenterLayout = () => {
return (
<div className="tw-w-full tw-bg-white tw-shadow tw-rounded tw-p-4">
<div className="tw-flex tw-justify-between tw-items-center">
<h6 className="tw-mb-2 tw-self-center">
{generateToken ? 'Generate JWT token' : 'JWT Token'}
</h6>
{!generateToken ? (
<div className="tw-flex">
<Button
size="small"
theme="primary"
variant="outlined"
onClick={() => handleTokenGeneration()}>
{botsToken ? 'Re-generate token' : 'Generate new token'}
</Button>
{botsToken ? (
<Button
className="tw-px-2 tw-py-0.5 tw-font-medium tw-ml-2 tw-rounded-md tw-border-error hover:tw-border-error tw-text-error hover:tw-text-error focus:tw-outline-none"
data-testid="delete-button"
size="custom"
variant="outlined"
onClick={() => setIsRevokingToken(true)}>
Revoke token
</Button>
) : null}
</div>
) : null}
</div>
<hr className="tw-mt-2" />
<p className="tw-mt-4">
Token you have generated that can be used to access the OpenMetadata
API.
</p>
{centerLayout()}
</div>
);
};
useEffect(() => {
if (botsData.id) {
fetchBotsToken();
}
}, [botsData]);
return (
<PageContainerV1 className="tw-py-4">
<TitleBreadcrumb
className="tw-px-6"
titleLinks={[
{
name: 'Bots',
url: ROUTES.BOTS,
},
{ name: botsData.name || '', url: '', activeTitle: true },
]}
/>
<PageLayout
classes="tw-h-full tw-px-4"
leftPanel={fetchLeftPanel()}
rightPanel={fetchRightPanel()}>
{getCenterLayout()}
</PageLayout>
{isRevokingToken ? (
<ConfirmationModal
bodyText="Are you sure you want to revoke access for JWT token?"
cancelText="Cancel"
confirmText="Confirm"
header="Are you sure?"
onCancel={() => setIsRevokingToken(false)}
onConfirm={() => {
revokeTokenHandler();
setIsRevokingToken(false);
}}
/>
) : null}
{isRegeneratingToken ? (
<ConfirmationModal
bodyText="Generating a new token will revoke the existing JWT token. Are you sure you want to proceed?"
cancelText="Cancel"
confirmText="Confirm"
header="Are you sure?"
onCancel={() => setIsRegeneratingToken(false)}
onConfirm={() => {
setIsRegeneratingToken(false);
setGenerateToken(true);
}}
/>
) : null}
</PageContainerV1>
);
};
export default BotsDetail;

View File

@ -0,0 +1,95 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC, Fragment, HTMLAttributes } from 'react';
import { useHistory } from 'react-router-dom';
import { getBotsPath } from '../../constants/constants';
import { User } from '../../generated/entity/teams/user';
import { EntityReference } from '../../generated/type/entityReference';
import { getEntityName } from '../../utils/CommonUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
import ErrorPlaceHolder from '../common/error-with-placeholder/ErrorPlaceHolder';
import RichTextEditorPreviewer from '../common/rich-text-editor/RichTextEditorPreviewer';
import TitleBreadcrumb from '../common/title-breadcrumb/title-breadcrumb.component';
import PageLayout from '../containers/PageLayout';
interface BotsListProp extends HTMLAttributes<HTMLDivElement> {
bots: Array<User>;
}
const BotsList: FC<BotsListProp> = ({ bots }) => {
const history = useHistory();
const handleTitleClick = (botsName: string) => {
const botsPath = getBotsPath(botsName);
history.push(botsPath);
};
const BotCard = ({ bot }: { bot: User }) => {
return (
<div className="tw-bg-white tw-shadow tw-border tw-border-main tw-rounded tw-p-3">
<div className="tw-flex">
<SVGIcons alt="bot-profile" icon={Icons.BOT_PROFILE} />
<span
className="tw-ml-2 tw-self-center tw-cursor-pointer hover:tw-underline"
data-testid="bot-displayname"
onClick={() => handleTitleClick(bot.name || '')}>
{getEntityName(bot as unknown as EntityReference)}
</span>
</div>
<div className="tw-mt-2">
{bot.description ? (
<RichTextEditorPreviewer markdown={bot.description || ''} />
) : (
<span className="tw-no-description tw-p-2 tw--ml-1.5">
No description{' '}
</span>
)}
</div>
</div>
);
};
const getListComponent = () => {
if (!bots.length) {
return <ErrorPlaceHolder>No bots are available</ErrorPlaceHolder>;
} else {
return (
<Fragment>
<TitleBreadcrumb
className="tw-mb-2"
titleLinks={[
{
name: 'Bots',
url: '',
activeTitle: true,
},
]}
/>
<div className="tw-grid xxl:tw-grid-cols-4 lg:tw-grid-cols-3 md:tw-grid-cols-2 tw-gap-4">
{bots.map((bot, key) => (
<BotCard bot={bot} key={key} />
))}
</div>
</Fragment>
);
}
};
return (
<PageLayout classes="tw-h-full tw-p-4">{getListComponent()}</PageLayout>
);
};
export default BotsList;

View File

@ -31,7 +31,6 @@ const TeamsAndUsers = ({
users,
isUsersLoading,
admins,
bots,
activeUserTab,
userSearchTerm,
selectedUserList,
@ -78,10 +77,6 @@ const TeamsAndUsers = ({
name: UserType.ADMINS,
data: admins,
},
{
name: UserType.BOTS,
data: bots,
},
];
/**

View File

@ -67,6 +67,7 @@ const PLACEHOLDER_WEBHOOK_NAME = ':webhookName';
const PLACEHOLDER_GLOSSARY_NAME = ':glossaryName';
const PLACEHOLDER_GLOSSARY_TERMS_FQN = ':glossaryTermsFQN';
const PLACEHOLDER_USER_NAME = ':username';
const PLACEHOLDER_BOTS_NAME = ':botsName';
export const pagingObject = { after: '', before: '', total: 0 };
@ -204,6 +205,8 @@ export const ROUTES = {
ADD_GLOSSARY_TERMS: `/glossary/${PLACEHOLDER_GLOSSARY_NAME}/add-term`,
GLOSSARY_TERMS: `/glossary/${PLACEHOLDER_GLOSSARY_NAME}/term/${PLACEHOLDER_GLOSSARY_TERMS_FQN}`,
ADD_GLOSSARY_TERMS_CHILD: `/glossary/${PLACEHOLDER_GLOSSARY_NAME}/term/${PLACEHOLDER_GLOSSARY_TERMS_FQN}/add-term`,
BOTS: `/bots`,
BOTS_PROFILE: `/bots/${PLACEHOLDER_BOTS_NAME}`,
};
export const IN_PAGE_SEARCH_ROUTES: Record<string, Array<string>> = {
@ -400,6 +403,13 @@ export const getAddGlossaryTermsPath = (
return path;
};
export const getBotsPath = (botsName: string) => {
let path = ROUTES.BOTS_PROFILE;
path = path.replace(PLACEHOLDER_BOTS_NAME, botsName);
return path;
};
export const TIMEOUT = {
USER_LIST: 60000, // 60 seconds for user retrieval
TOAST_DELAY: 5000, // 5 seconds timeout for toaster autohide delay
@ -412,6 +422,7 @@ export const navLinkDevelop = [
];
export const navLinkSettings = [
{ name: 'Bots', to: '/bots', disabled: false },
{ name: 'Glossaries', to: '/glossary', disabled: false },
{ name: 'Roles', to: '/roles', disabled: false, isAdminOnly: true },
{ name: 'Services', to: '/services', disabled: false },

View File

@ -0,0 +1,58 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AxiosError, AxiosResponse } from 'axios';
import React, { Fragment, useEffect, useState } from 'react';
import { getUsers } from '../../axiosAPIs/userAPI';
import BotsList from '../../components/BotsList/BotsList';
import Loader from '../../components/Loader/Loader';
import { User } from '../../generated/entity/teams/user';
import jsonData from '../../jsons/en';
import { showErrorToast } from '../../utils/ToastUtils';
const BotsListPage = () => {
const [bots, setBots] = useState<Array<User>>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const fetchBots = () => {
setIsLoading(true);
getUsers('', 1000)
.then((res: AxiosResponse) => {
if (res.data) {
const { data } = res.data;
const botsUser = data.filter((user: User) => user?.isBot);
setBots(botsUser);
} else {
throw jsonData['api-error-messages']['unexpected-server-response'];
}
})
.catch((err: AxiosError) => {
showErrorToast(err);
})
.finally(() => {
setIsLoading(false);
});
};
useEffect(() => {
fetchBots();
}, []);
return (
<Fragment>
{isLoading ? <Loader /> : <BotsList bots={bots || []} />}
</Fragment>
);
};
export default BotsListPage;

View File

@ -0,0 +1,121 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AxiosError, AxiosResponse } from 'axios';
import { compare } from 'fast-json-patch';
import React, { Fragment, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import {
getUserByName,
revokeUserToken,
updateUserDetail,
} from '../../axiosAPIs/userAPI';
import BotsDetail from '../../components/BotsDetail/BotsDetail.component';
import Loader from '../../components/Loader/Loader';
import { UserDetails } from '../../components/Users/Users.interface';
import { User } from '../../generated/entity/teams/user';
import jsonData from '../../jsons/en';
import { showErrorToast } from '../../utils/ToastUtils';
const BotsPage = () => {
const { botsName } = useParams<{ [key: string]: string }>();
const [botsData, setBotsData] = useState<User>({} as User);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const fetchBotsData = () => {
setIsLoading(true);
getUserByName(botsName)
.then((res: AxiosResponse) => {
if (res.data) {
setBotsData(res.data);
} else {
throw jsonData['api-error-messages']['unexpected-server-response'];
}
})
.catch((err: AxiosError) => {
showErrorToast(
err,
jsonData['api-error-messages']['fetch-user-details-error']
);
setIsError(true);
})
.finally(() => setIsLoading(false));
};
const updateBotsDetails = (data: UserDetails) => {
const updatedDetails = { ...botsData, ...data };
const jsonPatch = compare(botsData, updatedDetails);
updateUserDetail(botsData.id, jsonPatch)
.then((res: AxiosResponse) => {
if (res.data) {
setBotsData((prevData) => ({ ...prevData, ...data }));
} else {
throw jsonData['api-error-messages']['unexpected-error'];
}
})
.catch((err: AxiosError) => {
showErrorToast(err);
});
};
const revokeBotsToken = () => {
revokeUserToken(botsData.id)
.then((res: AxiosResponse) => {
const data = res.data;
setBotsData(data);
})
.catch((err: AxiosError) => {
showErrorToast(err);
});
};
const ErrorPlaceholder = () => {
return (
<div
className="tw-flex tw-flex-col tw-items-center tw-place-content-center tw-mt-40 tw-gap-1"
data-testid="error">
<p className="tw-text-base" data-testid="error-message">
No bots available with name{' '}
<span className="tw-font-medium" data-testid="username">
{botsName}
</span>{' '}
</p>
</div>
);
};
const getBotsDetailComponent = () => {
if (isError) {
return <ErrorPlaceholder />;
} else {
return (
<BotsDetail
botsData={botsData}
revokeTokenHandler={revokeBotsToken}
updateBotsDetails={updateBotsDetails}
/>
);
}
};
useEffect(() => {
fetchBotsData();
}, [botsName]);
return (
<Fragment>{isLoading ? <Loader /> : getBotsDetailComponent()}</Fragment>
);
};
export default BotsPage;

View File

@ -21,6 +21,8 @@ import AddGlossaryTermPage from '../pages/AddGlossaryTermPage/AddGlossaryTermPag
import AddIngestionPage from '../pages/AddIngestionPage/AddIngestionPage.component';
import AddServicePage from '../pages/AddServicePage/AddServicePage.component';
import AddWebhookPage from '../pages/AddWebhookPage/AddWebhookPage.component';
import BotsListPage from '../pages/BotsListpage/BotsListpage.component';
import BotsPage from '../pages/BotsPage/BotsPage.component';
import CreateUserPage from '../pages/CreateUserPage/CreateUserPage.component';
import DashboardDetailsPage from '../pages/DashboardDetailsPage/DashboardDetailsPage.component';
import DatabaseDetails from '../pages/database-details/index';
@ -164,6 +166,13 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
component={CreateUserPage}
path={ROUTES.CREATE_USER}
/>
<AdminProtectedRoute exact component={BotsListPage} path={ROUTES.BOTS} />
<AdminProtectedRoute
exact
component={BotsPage}
path={ROUTES.BOTS_PROFILE}
/>
<Redirect to={ROUTES.NOT_FOUND} />
</Switch>
);

View File

@ -24,6 +24,7 @@ import IconAnnouncement from '../assets/svg/announcements.svg';
import IconAPI from '../assets/svg/api.svg';
import IconArrowDownPrimary from '../assets/svg/arrow-down-primary.svg';
import IconArrowRightPrimary from '../assets/svg/arrow-right-primary.svg';
import IconBotProfile from '../assets/svg/bot-profile.svg';
import IconSuccess from '../assets/svg/check.svg';
import IconCheckboxPrimary from '../assets/svg/checkbox-primary.svg';
import IconCircleCheckbox from '../assets/svg/circle-checkbox.svg';
@ -277,6 +278,7 @@ export const Icons = {
SUCCESS_BADGE: 'success-badge',
FAIL_BADGE: 'fail-badge',
PENDING_BADGE: 'pending-badge',
BOT_PROFILE: 'bot-profile',
CREATE_INGESTION: 'create-ingestion',
DEPLOY_INGESTION: 'deploy-ingestion',
};
@ -802,6 +804,10 @@ const SVGIcons: FunctionComponent<Props> = ({
case Icons.PENDING_BADGE:
IconComponent = IconPendingBadge;
break;
case Icons.BOT_PROFILE:
IconComponent = IconBotProfile;
break;
case Icons.CREATE_INGESTION:
IconComponent = IconCreateIngestion;
@ -811,7 +817,6 @@ const SVGIcons: FunctionComponent<Props> = ({
IconComponent = IconDeployIngestion;
break;
default:
IconComponent = null;