mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-14 18:03:38 +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) => {
|
export const deleteUser = (id: string) => {
|
||||||
return APIClient.delete(`/users/${id}`);
|
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,
|
users,
|
||||||
isUsersLoading,
|
isUsersLoading,
|
||||||
admins,
|
admins,
|
||||||
bots,
|
|
||||||
activeUserTab,
|
activeUserTab,
|
||||||
userSearchTerm,
|
userSearchTerm,
|
||||||
selectedUserList,
|
selectedUserList,
|
||||||
@ -78,10 +77,6 @@ const TeamsAndUsers = ({
|
|||||||
name: UserType.ADMINS,
|
name: UserType.ADMINS,
|
||||||
data: 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_NAME = ':glossaryName';
|
||||||
const PLACEHOLDER_GLOSSARY_TERMS_FQN = ':glossaryTermsFQN';
|
const PLACEHOLDER_GLOSSARY_TERMS_FQN = ':glossaryTermsFQN';
|
||||||
const PLACEHOLDER_USER_NAME = ':username';
|
const PLACEHOLDER_USER_NAME = ':username';
|
||||||
|
const PLACEHOLDER_BOTS_NAME = ':botsName';
|
||||||
|
|
||||||
export const pagingObject = { after: '', before: '', total: 0 };
|
export const pagingObject = { after: '', before: '', total: 0 };
|
||||||
|
|
||||||
@ -204,6 +205,8 @@ export const ROUTES = {
|
|||||||
ADD_GLOSSARY_TERMS: `/glossary/${PLACEHOLDER_GLOSSARY_NAME}/add-term`,
|
ADD_GLOSSARY_TERMS: `/glossary/${PLACEHOLDER_GLOSSARY_NAME}/add-term`,
|
||||||
GLOSSARY_TERMS: `/glossary/${PLACEHOLDER_GLOSSARY_NAME}/term/${PLACEHOLDER_GLOSSARY_TERMS_FQN}`,
|
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`,
|
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>> = {
|
export const IN_PAGE_SEARCH_ROUTES: Record<string, Array<string>> = {
|
||||||
@ -400,6 +403,13 @@ export const getAddGlossaryTermsPath = (
|
|||||||
return path;
|
return path;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getBotsPath = (botsName: string) => {
|
||||||
|
let path = ROUTES.BOTS_PROFILE;
|
||||||
|
path = path.replace(PLACEHOLDER_BOTS_NAME, botsName);
|
||||||
|
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
|
||||||
export const TIMEOUT = {
|
export const TIMEOUT = {
|
||||||
USER_LIST: 60000, // 60 seconds for user retrieval
|
USER_LIST: 60000, // 60 seconds for user retrieval
|
||||||
TOAST_DELAY: 5000, // 5 seconds timeout for toaster autohide delay
|
TOAST_DELAY: 5000, // 5 seconds timeout for toaster autohide delay
|
||||||
@ -412,6 +422,7 @@ export const navLinkDevelop = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const navLinkSettings = [
|
export const navLinkSettings = [
|
||||||
|
{ name: 'Bots', to: '/bots', disabled: false },
|
||||||
{ name: 'Glossaries', to: '/glossary', disabled: false },
|
{ name: 'Glossaries', to: '/glossary', disabled: false },
|
||||||
{ name: 'Roles', to: '/roles', disabled: false, isAdminOnly: true },
|
{ name: 'Roles', to: '/roles', disabled: false, isAdminOnly: true },
|
||||||
{ name: 'Services', to: '/services', disabled: false },
|
{ 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 AddIngestionPage from '../pages/AddIngestionPage/AddIngestionPage.component';
|
||||||
import AddServicePage from '../pages/AddServicePage/AddServicePage.component';
|
import AddServicePage from '../pages/AddServicePage/AddServicePage.component';
|
||||||
import AddWebhookPage from '../pages/AddWebhookPage/AddWebhookPage.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 CreateUserPage from '../pages/CreateUserPage/CreateUserPage.component';
|
||||||
import DashboardDetailsPage from '../pages/DashboardDetailsPage/DashboardDetailsPage.component';
|
import DashboardDetailsPage from '../pages/DashboardDetailsPage/DashboardDetailsPage.component';
|
||||||
import DatabaseDetails from '../pages/database-details/index';
|
import DatabaseDetails from '../pages/database-details/index';
|
||||||
@ -164,6 +166,13 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
|
|||||||
component={CreateUserPage}
|
component={CreateUserPage}
|
||||||
path={ROUTES.CREATE_USER}
|
path={ROUTES.CREATE_USER}
|
||||||
/>
|
/>
|
||||||
|
<AdminProtectedRoute exact component={BotsListPage} path={ROUTES.BOTS} />
|
||||||
|
<AdminProtectedRoute
|
||||||
|
exact
|
||||||
|
component={BotsPage}
|
||||||
|
path={ROUTES.BOTS_PROFILE}
|
||||||
|
/>
|
||||||
|
|
||||||
<Redirect to={ROUTES.NOT_FOUND} />
|
<Redirect to={ROUTES.NOT_FOUND} />
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import IconAnnouncement from '../assets/svg/announcements.svg';
|
|||||||
import IconAPI from '../assets/svg/api.svg';
|
import IconAPI from '../assets/svg/api.svg';
|
||||||
import IconArrowDownPrimary from '../assets/svg/arrow-down-primary.svg';
|
import IconArrowDownPrimary from '../assets/svg/arrow-down-primary.svg';
|
||||||
import IconArrowRightPrimary from '../assets/svg/arrow-right-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 IconSuccess from '../assets/svg/check.svg';
|
||||||
import IconCheckboxPrimary from '../assets/svg/checkbox-primary.svg';
|
import IconCheckboxPrimary from '../assets/svg/checkbox-primary.svg';
|
||||||
import IconCircleCheckbox from '../assets/svg/circle-checkbox.svg';
|
import IconCircleCheckbox from '../assets/svg/circle-checkbox.svg';
|
||||||
@ -277,6 +278,7 @@ export const Icons = {
|
|||||||
SUCCESS_BADGE: 'success-badge',
|
SUCCESS_BADGE: 'success-badge',
|
||||||
FAIL_BADGE: 'fail-badge',
|
FAIL_BADGE: 'fail-badge',
|
||||||
PENDING_BADGE: 'pending-badge',
|
PENDING_BADGE: 'pending-badge',
|
||||||
|
BOT_PROFILE: 'bot-profile',
|
||||||
CREATE_INGESTION: 'create-ingestion',
|
CREATE_INGESTION: 'create-ingestion',
|
||||||
DEPLOY_INGESTION: 'deploy-ingestion',
|
DEPLOY_INGESTION: 'deploy-ingestion',
|
||||||
};
|
};
|
||||||
@ -802,6 +804,10 @@ const SVGIcons: FunctionComponent<Props> = ({
|
|||||||
case Icons.PENDING_BADGE:
|
case Icons.PENDING_BADGE:
|
||||||
IconComponent = IconPendingBadge;
|
IconComponent = IconPendingBadge;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case Icons.BOT_PROFILE:
|
||||||
|
IconComponent = IconBotProfile;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case Icons.CREATE_INGESTION:
|
case Icons.CREATE_INGESTION:
|
||||||
IconComponent = IconCreateIngestion;
|
IconComponent = IconCreateIngestion;
|
||||||
@ -811,7 +817,6 @@ const SVGIcons: FunctionComponent<Props> = ({
|
|||||||
IconComponent = IconDeployIngestion;
|
IconComponent = IconDeployIngestion;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
IconComponent = null;
|
IconComponent = null;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user