mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-02 11:39:12 +00:00
parent
92913c6eaf
commit
642735dfb9
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 18 KiB |
@ -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}`);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user