Web:Client:PortalSettings:OAuth: add table view

This commit is contained in:
Timofey Boyko 2023-09-27 14:00:11 +03:00
parent 6956adc231
commit d8a535432d
29 changed files with 1024 additions and 691 deletions

View File

@ -1,3 +1,12 @@
export interface OAuthProps {}
//@ts-ignore
import { ClientProps } from "@docspace/common/utils/oauth/dto";
export interface OAuthStore {}
//@ts-ignore
import { ViewAsType } from "SRC_DIR/store/OAuthStore";
export interface OAuthProps {
viewAs: ViewAsType;
clientList: ClientProps[];
isEmptyClientList: boolean;
fetchClients: (page: number) => Promise<void>;
}

View File

@ -1,6 +1,4 @@
import React from "react";
import { useNavigate } from "react-router-dom";
import { inject, observer } from "mobx-react";
import { useTranslation } from "react-i18next";
// @ts-ignore
@ -44,6 +42,4 @@ const OAuthSectionHeader = ({ isEdit }: OAuthSectionHeaderProps) => {
);
};
export default inject(({}) => {
return {};
})(observer(OAuthSectionHeader));
export default OAuthSectionHeader;

View File

@ -4,25 +4,3 @@ export const OAuthContainer = styled.div`
width: 100%;
margin-top: 5px;
`;
export const Property = styled.div`
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
width: 500px;
`;
export const StyledCategory = styled.div`
display: flex;
flex-direction: row;
gap: 4px;
margin-bottom: 16px;
align-items: center;
`;
export const StyledTooltip = styled.div`
.subtitle {
margin-bottom: 10px;
}
`;

View File

@ -2,20 +2,66 @@ import React from "react";
import { inject, observer } from "mobx-react";
import { useTranslation } from "react-i18next";
//@ts-ignore
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
import OAuthEmptyScreen from "./sub-components/EmptyScreen";
import List from "./sub-components/List";
import { OAuthContainer } from "./StyledOAuth";
import { OAuthProps } from "./OAuth.types";
const OAuth = ({}) => {
const MIN_LOADER_TIME = 500;
const OAuth = ({
clientList,
viewAs,
isEmptyClientList,
fetchClients,
}: OAuthProps) => {
const { t } = useTranslation(["OAuth"]);
const [isLoading, setIsLoading] = React.useState<boolean>(true);
const startLoadingRef = React.useRef<null | Date>(null);
const getClientList = React.useCallback(async (page = 0) => {
await fetchClients(page);
if (startLoadingRef.current) {
const currentDate = new Date();
const ms = Math.abs(
startLoadingRef.current.getTime() - currentDate.getTime()
);
if (ms < MIN_LOADER_TIME)
return setTimeout(() => {
setIsLoading(false);
}, MIN_LOADER_TIME - ms);
}
setIsLoading(false);
}, []);
React.useEffect(() => {
startLoadingRef.current = new Date();
getClientList();
}, [getClientList]);
return (
<OAuthContainer>
<OAuthEmptyScreen t={t} />
{isLoading ? (
<div>Loading...</div>
) : isEmptyClientList ? (
<OAuthEmptyScreen t={t} />
) : (
<List t={t} clients={clientList} viewAs={viewAs} />
)}
</OAuthContainer>
);
};
export default inject(({}) => {
return {};
export default inject(({ oauthStore }: { oauthStore: OAuthStoreProps }) => {
const { viewAs, clientList, isEmptyClientList, fetchClients } = oauthStore;
return { viewAs, clientList, isEmptyClientList, fetchClients };
})(observer(OAuth));

View File

@ -1,59 +0,0 @@
import InfoReactSvgUrl from "PUBLIC_DIR/images/info.react.svg?url";
import React from "react";
import Text from "@docspace/components/text";
import HelpButton from "@docspace/components/help-button";
import Link from "@docspace/components/link";
import { Base } from "@docspace/components/themes";
import { StyledCategory, StyledTooltip } from "../StyledOAuth";
const Category = (props) => {
const {
t,
title,
tooltipTitle,
tooltipUrl,
theme,
currentColorScheme,
} = props;
const tooltip = () => (
<StyledTooltip>
<Text className={tooltipUrl ? "subtitle" : ""} fontSize="12px">
{tooltipTitle}
</Text>
{tooltipUrl && (
<Link
fontSize="12px"
target="_blank"
isHovered
href={tooltipUrl}
color={currentColorScheme.main.accent}
>
{t("Common:LearnMore")}
</Link>
)}
</StyledTooltip>
);
return (
<StyledCategory>
<Text fontSize="16px" fontWeight="700">
{title}
</Text>
<HelpButton
iconName={InfoReactSvgUrl}
displayType="dropdown"
place="right"
offsetRight={0}
getContent={tooltip}
tooltipColor={theme.client.settings.security.owner.tooltipColor}
/>
</StyledCategory>
);
};
Category.defaultProps = {
theme: Base,
};
export default Category;

View File

@ -1,4 +1,4 @@
import { ClientProps, ScopeDTO } from "@docspace/common/utils/oauth/dto";
import { ClientProps, Scope } from "@docspace/common/utils/oauth/interfaces";
export interface InputProps {
value: string;
@ -38,13 +38,19 @@ export interface ClientFormProps {
id?: string;
client?: ClientProps;
scopeList?: ScopeDTO[];
tenant?: number;
fetchTenant?: () => Promise<number>;
scopeList?: Scope[];
fetchClient?: (clientId: string) => Promise<ClientProps>;
fetchScopes?: () => Promise<void>;
saveClient: (client: ClientProps) => Promise<ClientProps>;
updateClient: (clientId: string, client: ClientProps) => Promise<ClientProps>;
saveClient?: (client: ClientProps) => Promise<ClientProps>;
updateClient?: (
clientId: string,
client: ClientProps
) => Promise<ClientProps>;
regenerateSecret?: (clientId: string) => Promise<string>;
}

View File

@ -1,5 +1,3 @@
import React from "react";
import { BlockContainer } from "../ClientForm.styled";
import { BlockProps } from "../ClientForm.types";

View File

@ -1,5 +1,3 @@
import React from "react";
import Text from "@docspace/components/text";
//@ts-ignore
import HelpButton from "@docspace/components/help-button";

View File

@ -1,5 +1,3 @@
import React from "react";
import Checkbox from "@docspace/components/checkbox";
import Text from "@docspace/components/text";

View File

@ -1,4 +1,3 @@
import React from "react";
import copy from "copy-to-clipboard";
import InputBlock from "@docspace/components/input-block";
@ -45,8 +44,8 @@ const Input = ({
size={"base"}
isReadOnly={isReadOnly}
isDisabled={isReadOnly}
iconName={withCopy ? CopyReactSvgUrl : ""}
onIconClick={onCopy}
iconName={withCopy ? CopyReactSvgUrl : null}
onIconClick={withCopy && onCopy}
scale={true}
type={isSecret ? "password" : "text"}
/>

View File

@ -1,5 +1,3 @@
import React from "react";
import Text from "@docspace/components/text";
const InputHeader = ({ header }: { header: string }) => {

View File

@ -3,7 +3,7 @@ import { inject, observer } from "mobx-react";
import { useNavigate } from "react-router-dom";
import { isMobileOnly } from "react-device-detect";
import { ClientProps, ScopeDTO } from "@docspace/common/utils/oauth/dto";
import { ClientProps, Scope } from "@docspace/common/utils/oauth/interfaces";
import Button from "@docspace/components/button";
@ -27,6 +27,9 @@ const ClientForm = ({
scopeList,
tenant,
fetchTenant,
fetchClient,
fetchScopes,
@ -45,20 +48,19 @@ const ClientForm = ({
appName: "",
appIcon: "",
description: "",
redirectUrl: "",
termsURL: "",
privacyURL: "",
logoutRedirectUrl: "",
privacyURL: "",
authenticationMethod: "",
});
const [clientId, setClientId] = React.useState<string>(
"23b2ec16-6a10-462b-8084-16be8e105b73"
);
const [secret, setSecret] = React.useState<string>(
"d2c083aa-9a2d-4147-9328-df32b7be0294"
);
const [clientId, setClientId] = React.useState<string>("");
const [secret, setSecret] = React.useState<string>("");
const [scopes, setScopes] = React.useState<ScopeDTO[]>([]);
const [scopes, setScopes] = React.useState<Scope[]>([]);
const [checkedScopes, setCheckedScopes] = React.useState<string[]>([]);
const onInputChange = React.useCallback(
@ -101,8 +103,17 @@ const ClientForm = ({
newClient.scopes = [...checkedScopes];
if (!id) {
if (!saveClient) return;
if (tenant === -1 && fetchTenant) {
const t = await fetchTenant();
newClient.tenant = t;
}
await saveClient(newClient);
} else {
if (!updateClient) return;
await updateClient(clientId, newClient);
}
@ -137,11 +148,15 @@ const ClientForm = ({
const setClient = React.useCallback(async (client: ClientProps) => {
setForm({
appName: client.name,
appIcon: client.logoUrl,
appIcon: client.logoUrl || "",
description: client.description,
redirectUrl: client.redirectUri,
logoutRedirectUrl: client.logoutRedirectUri,
privacyURL: client.policyUrl,
termsUrl: client.termsUrl,
logoutRedirectUrl: client.logoutRedirectUri,
authenticationMethod: client.authenticationMethod,
});
setSecret(client.secret);
@ -178,7 +193,7 @@ const ClientForm = ({
let isValid = false;
for (let key in form) {
if (!!form[key]) {
if (!!form[key] || key === "appIcon" || key === "authenticationMethod") {
if (initClient) {
switch (key) {
case "appName":
@ -186,7 +201,7 @@ const ClientForm = ({
break;
case "appIcon":
isValid = isValid || initClient.logoUrl !== form[key];
isValid = isValid || initClient.name !== form[key];
break;
case "description":
@ -204,6 +219,11 @@ const ClientForm = ({
case "privacyUrl":
isValid = isValid || initClient.policyUrl !== form[key];
break;
case "termsUrl":
isValid = isValid || initClient.termsUrl !== form[key];
break;
}
}
@ -266,35 +286,49 @@ const ClientForm = ({
</InputGroup>
</Block>
<Block>
<BlockHeader header={"Client"} helpButtonText="" />
<InputGroup>
<InputHeader header={"ID"} />
<Input
value={clientId}
name={"ID"}
placeholder={"Enter id"}
onChange={onInputChange}
isReadOnly
withCopy
/>
</InputGroup>
<InputGroup>
<InputHeader header={"Secret"} />
<Input
value={secret}
name={"secret"}
placeholder={"Enter secret"}
onChange={onInputChange}
isReadOnly
isSecret
withCopy
withButton
buttonLabel="Reset"
onClickButton={onResetClick}
/>
</InputGroup>
</Block>
{id && (
<Block>
<BlockHeader header={"Client"} helpButtonText="" />
<InputGroup>
<InputHeader header={"ID"} />
<Input
value={clientId}
name={"ID"}
placeholder={"Enter id"}
onChange={onInputChange}
isReadOnly
withCopy
/>
</InputGroup>
<InputGroup>
<InputHeader header={"Secret"} />
<Input
value={secret}
name={"secret"}
placeholder={"Enter secret"}
onChange={onInputChange}
isReadOnly
isSecret
withCopy
withButton
buttonLabel="Reset"
onClickButton={onResetClick}
/>
</InputGroup>
<InputGroup>
<InputHeader header={"Authentication method "} />
<Input
value={form.authenticationMethod}
name={"authenticationMethod"}
placeholder={"Enter secret"}
onChange={onInputChange}
isReadOnly
withCopy
/>
</InputGroup>
</Block>
)}
<Block>
<BlockHeader header={"OAuth URLs"} helpButtonText="" />
@ -316,15 +350,6 @@ const ClientForm = ({
onChange={onInputChange}
/>
</InputGroup>
{/* <InputGroup>
<InputHeader header={"Allowed origins"} />
<Input
value={form.allowedOrigins}
name={"allowedOrigins"}
placeholder={"Enter URL"}
onChange={onInputChange}
/>
</InputGroup> */}
</Block>
<Block>
@ -345,15 +370,7 @@ const ClientForm = ({
<Block>
<BlockHeader header={"Support & Legal info"} helpButtonText="" />
{/* <InputGroup>
<InputHeader header={"Website URL"} />
<Input
value={form.websiteUrl}
name={"websiteUrl"}
placeholder={"Enter URL"}
onChange={onInputChange}
/>
</InputGroup> */}
<InputGroup>
<InputHeader header={"Privacy policy URL"} />
<Input
@ -363,15 +380,15 @@ const ClientForm = ({
onChange={onInputChange}
/>
</InputGroup>
{/* <InputGroup>
<InputGroup>
<InputHeader header={"Terms of Service URL"} />
<Input
value={form.serviceUrl}
name={"serviceUrl"}
value={form.termsURL}
name={"termsURL"}
placeholder={"Enter URL"}
onChange={onInputChange}
/>
</InputGroup> */}
</InputGroup>
</Block>
<div className="button-container">
@ -408,6 +425,9 @@ export default inject(
fetchClient,
fetchScopes,
tenant,
fetchTenant,
saveClient,
updateClient,
@ -420,6 +440,9 @@ export default inject(
fetchClient,
fetchScopes,
tenant,
fetchTenant,
saveClient,
updateClient,

View File

@ -1,5 +1,3 @@
import React from "react";
// @ts-ignore
import EmptyScreenContainer from "@docspace/components/empty-screen-container";

View File

@ -1,116 +0,0 @@
import React, { useState } from "react";
import TableHeader from "@docspace/components/table-container/TableHeader";
import { useTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
const TABLE_VERSION = "1";
const TABLE_COLUMNS = `oauthConfigColumns_ver-${TABLE_VERSION}`;
const getColumns = (defaultColumns, userId) => {
const storageColumns = localStorage.getItem(`${TABLE_COLUMNS}=${userId}`);
const columns = [];
if (storageColumns) {
const splitColumns = storageColumns.split(",");
for (let col of defaultColumns) {
const column = splitColumns.find((key) => key === col.key);
column ? (col.enable = true) : (col.enable = false);
columns.push(col);
}
return columns;
} else {
return defaultColumns;
}
};
const Header = (props) => {
const {
userId,
sectionWidth,
tableRef,
columnStorageName,
columnInfoPanelStorageName,
setHideColumns,
} = props;
const { t } = useTranslation(["Webhooks", "Common"]);
const defaultColumns = [
{
key: "Logo",
title: "Logo",
enable: true,
active: true,
resizable: false,
defaultSize: 64,
onChange: onColumnChange,
},
{
key: "Name",
title: t("Common:Name"),
resizable: true,
enable: true,
default: true,
active: true,
minWidth: 150,
onChange: onColumnChange,
},
{
key: "Description",
title: "Description",
resizable: true,
enable: true,
minWidth: 150,
onChange: onColumnChange,
},
{
key: "Enable",
title: "Enable",
enable: true,
resizable: false,
defaultSize: 64,
onChange: onColumnChange,
},
];
const [columns, setColumns] = useState(getColumns(defaultColumns, userId));
function onColumnChange(key, e) {
const columnIndex = columns.findIndex((c) => c.key === key);
if (columnIndex === -1) return;
setColumns((prevColumns) =>
prevColumns.map((item, index) =>
index === columnIndex ? { ...item, enabled: !item.enabled } : item
)
);
const tableColumns = columns.map((c) => c.enabled && c.key);
localStorage.setItem(`${TABLE_COLUMNS}=${userId}`, tableColumns);
}
return (
<TableHeader
checkboxSize="48px"
containerRef={tableRef}
columns={columns}
columnStorageName={columnStorageName}
columnInfoPanelStorageName={columnInfoPanelStorageName}
sectionWidth={sectionWidth}
checkboxMargin="12px"
showSettings={false}
useReactWindow
setHideColumns={setHideColumns}
infoPanelVisible={false}
/>
);
};
export default inject(({ auth }) => {
return {
userId: auth.userStore.user.id,
};
})(observer(Header));

View File

@ -0,0 +1,68 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
//@ts-ignore
import TableHeader from "@docspace/components/table-container/TableHeader";
const TABLE_VERSION = "1";
const TABLE_COLUMNS = `oauthConfigColumns_ver-${TABLE_VERSION}`;
interface HeaderProps {
sectionWidth: number;
tableRef: HTMLDivElement;
columnStorageName: string;
}
const Header = (props: HeaderProps) => {
const { sectionWidth, tableRef, columnStorageName } = props;
const { t } = useTranslation(["Webhooks", "Common"]);
const defaultColumns: {
[key: string]:
| string
| number
| boolean
| ((key: string, e: any) => void | undefined);
}[] = [
{
key: "Name",
title: t("Common:Name"),
resizable: true,
enable: true,
default: true,
active: false,
minWidth: 210,
},
{
key: "Description",
title: "Description",
resizable: true,
enable: true,
minWidth: 150,
},
{
key: "Enable",
title: "Enable",
enable: true,
resizable: false,
defaultSize: 64,
},
];
return (
<TableHeader
checkboxSize="48px"
containerRef={tableRef}
columns={defaultColumns}
columnStorageName={columnStorageName}
sectionWidth={sectionWidth}
checkboxMargin="12px"
showSettings={false}
useReactWindow
infoPanelVisible={false}
/>
);
};
export default Header;

View File

@ -1,148 +0,0 @@
import React, { useState } from "react";
import styled from "styled-components";
import TableRow from "@docspace/components/table-container/TableRow";
import TableCell from "@docspace/components/table-container/TableCell";
import Text from "@docspace/components/text";
import ToggleButton from "@docspace/components/toggle-button";
import SettingsIcon from "PUBLIC_DIR/images/catalog.settings.react.svg?url";
import HistoryIcon from "PUBLIC_DIR/images/history.react.svg?url";
import DeleteIcon from "PUBLIC_DIR/images/delete.react.svg?url";
import LinuxIcon from "PUBLIC_DIR/images/linux.react.svg?url";
//import StatusBadge from "../../StatusBadge";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
const StyledWrapper = styled.div`
display: contents;
`;
const StyledTableRow = styled(TableRow)`
.table-container_cell {
padding-right: 30px;
text-overflow: ellipsis;
}
.mr-8 {
margin-right: 8px;
}
.textOverflow {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.toggleButton {
display: contents;
}
`;
const Row = (props) => {
const {
item,
setEnabled,
openDeleteModal,
setCurrentClient,
hideColumns,
} = props;
const navigate = useNavigate();
const { t } = useTranslation(["Webhooks", "Common"]);
const [isChecked, setIsChecked] = useState(item.enabled);
const openClientSettings = () => {
navigate(window.location.pathname + `/${item.id}`);
};
const handleToggleEnabled = () => {
setEnabled(item.id);
setIsChecked((prevIsChecked) => !prevIsChecked);
};
const onSettingsOpen = () => {
setCurrentClient(item);
openClientSettings();
};
const onDeleteOpen = () => {
setCurrentClient(item);
openDeleteModal();
};
const handleRowClick = (e) => {
if (
e.target.closest(".checkbox") ||
e.target.closest(".table-container_row-checkbox") ||
e.target.closest(".type-combobox") ||
e.target.closest(".table-container_row-context-menu-wrapper") ||
e.target.closest(".toggleButton") ||
e.detail === 0
) {
return;
}
onSettingsOpen();
};
const contextOptions = [
{
key: "settings",
label: t("Common:Settings"),
icon: SettingsIcon,
onClick: onSettingsOpen,
},
{
key: "Separator dropdownItem",
isSeparator: true,
},
{
key: "delete",
label: t("Common:Delete"),
icon: DeleteIcon,
onClick: onDeleteOpen,
},
];
return (
<>
<StyledWrapper onClick={handleRowClick}>
<StyledTableRow
contextOptions={contextOptions}
hideColumns={hideColumns}
>
<TableCell>
<img src={LinuxIcon} />
</TableCell>
<TableCell>
<Text as="span" fontWeight={600} className="mr-8 textOverflow">
{item.name}
</Text>
</TableCell>
<TableCell>
<Text as="span" fontWeight={400} className="mr-8 textOverflow">
{item.description}
</Text>
</TableCell>
<TableCell>
<ToggleButton
className="toggle toggleButton"
isChecked={isChecked}
onChange={handleToggleEnabled}
/>
</TableCell>
</StyledTableRow>
</StyledWrapper>
</>
);
};
export default inject(({ oauthStore }) => {
const { setCurrentClient, setEnabled } = oauthStore;
return { setEnabled, setCurrentClient };
})(observer(Row));

View File

@ -0,0 +1,195 @@
import React from "react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import styled, { css } from "styled-components";
//@ts-ignore
import TableRow from "@docspace/components/table-container/TableRow";
//@ts-ignore
import TableCell from "@docspace/components/table-container/TableCell";
import Text from "@docspace/components/text";
import ToggleButton from "@docspace/components/toggle-button";
import { Base } from "@docspace/components/themes";
import SettingsIcon from "PUBLIC_DIR/images/catalog.settings.react.svg?url";
import DeleteIcon from "PUBLIC_DIR/images/delete.react.svg?url";
import { ClientProps } from "@docspace/common/utils/oauth/interfaces";
import NameCell from "./columns/name";
const StyledWrapper = styled.div`
display: contents;
`;
const StyledTableRow = styled(TableRow)`
.table-container_cell {
text-overflow: ellipsis;
padding-right: 8px;
}
.mr-8 {
margin-right: 8px;
}
.textOverflow {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.toggleButton {
display: contents;
input {
position: relative;
margin-left: -8px;
}
}
.table-container_row-loader {
margin-left: 8px;
margin-right: 16px;
}
:hover {
.table-container_cell {
cursor: pointer;
background: ${(props) =>
`${props.theme.filesSection.tableView.row.backgroundActive} !important`};
margin-top: -1px;
border-top: ${(props) =>
`1px solid ${props.theme.filesSection.tableView.row.borderColor}`};
}
.table-container_file-name-cell {
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
margin-right: -24px;
padding-right: 24px;
`
: css`
margin-left: -24px;
padding-left: 24px;
`}
}
.table-container_row-context-menu-wrapper {
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
margin-left: -20px;
padding-left: 18px;
`
: css`
margin-right: -20px;
padding-right: 18px;
`}
}
}
`;
StyledTableRow.defaultProps = { theme: Base };
interface RowProps {
item: ClientProps;
isChecked: boolean;
inProgress: boolean;
setSelection?: (clientId: string) => void;
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
}
const Row = (props: RowProps) => {
const { item, changeClientStatus, isChecked, inProgress, setSelection } =
props;
const navigate = useNavigate();
const { t } = useTranslation(["Webhooks", "Common"]);
const editClient = () => {
navigate(window.location.pathname + `/${item.clientId}`);
};
const handleToggleEnabled = async () => {
if (!changeClientStatus) return;
await changeClientStatus(item.clientId, !item.enabled);
};
const onDeleteOpen = () => {};
const handleRowClick = (e: any) => {
if (
e.target.closest(".checkbox") ||
e.target.closest(".table-container_row-checkbox") ||
e.target.closest(".type-combobox") ||
e.target.closest(".table-container_row-context-menu-wrapper") ||
e.target.closest(".toggleButton") ||
e.detail === 0
) {
return;
}
editClient();
};
const contextOptions = [
{
key: "settings",
label: t("Common:Settings"),
icon: SettingsIcon,
onClick: editClient,
},
{
key: "Separator dropdownItem",
isSeparator: true,
},
{
key: "delete",
label: t("Common:Delete"),
icon: DeleteIcon,
onClick: onDeleteOpen,
},
];
return (
<>
<StyledWrapper className="handle">
<StyledTableRow
contextOptions={contextOptions}
onClick={handleRowClick}
>
<TableCell className={"table-container_file-name-cell"}>
<NameCell
name={item.name}
icon={item.logoUrl}
isChecked={isChecked}
inProgress={inProgress}
clientId={item.clientId}
setSelection={setSelection}
/>
</TableCell>
<TableCell>
{/* @ts-ignore */}
<Text as="span" fontWeight={400} className="mr-8 textOverflow">
{item.description}
</Text>
</TableCell>
<TableCell>
<ToggleButton
className="toggle toggleButton"
isChecked={item.enabled}
onChange={handleToggleEnabled}
/>
</TableCell>
</StyledTableRow>
</StyledWrapper>
</>
);
};
export default Row;

View File

@ -0,0 +1,88 @@
import React from "react";
import styled from "styled-components";
import Text from "@docspace/components/text";
import Checkbox from "@docspace/components/checkbox";
//@ts-ignore
import TableCell from "@docspace/components/table-container/TableCell";
import Loader from "@docspace/components/loader";
const StyledContainer = styled.div`
.table-container_row-checkbox {
margin-left: -8px;
width: 16px;
padding: 16px 8px 16px 16px;
}
`;
const StyledImage = styled.img`
width: 32px;
height: 32px;
`;
interface NameCellProps {
name: string;
icon?: string;
clientId: string;
inProgress?: boolean;
isChecked?: boolean;
setSelection?: (clientId: string) => void;
}
const NameCell = ({
name,
icon,
clientId,
inProgress,
isChecked,
setSelection,
}: NameCellProps) => {
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSelection && setSelection(clientId);
};
return (
<>
{inProgress ? (
<Loader
className="table-container_row-loader"
type="oval"
size="16px"
/>
) : (
<TableCell
className="table-container_element-wrapper"
hasAccess={true}
checked={isChecked}
>
<StyledContainer className="table-container_element-container">
<div className="table-container_element">
{icon && <StyledImage src={icon} alt={"App icon"} />}
</div>
<Checkbox
className="table-container_row-checkbox"
onChange={onChange}
isChecked={isChecked}
title={name}
/>
</StyledContainer>
</TableCell>
)}
{/* @ts-ignore */}
<Text
type="page"
title={name}
fontWeight="600"
fontSize="13px"
isTextOverflow
>
{name}
</Text>
</>
);
};
export default NameCell;

View File

@ -1,116 +0,0 @@
import React, { useState, useRef, useEffect } from "react";
import { inject, observer } from "mobx-react";
import { isMobile } from "react-device-detect";
import styled from "styled-components";
import TableContainer from "@docspace/components/table-container/TableContainer";
import TableBody from "@docspace/components/table-container/TableBody";
import Row from "./Row";
import Header from "./Header";
import { Base } from "@docspace/components/themes";
const TableWrapper = styled(TableContainer)`
margin-top: 16px;
.header-container-text {
font-size: 12px;
}
.table-container_header {
position: absolute;
}
.table-list-item {
margin-top: -1px;
&:hover {
cursor: pointer;
background-color: ${(props) =>
props.theme.isBase ? "#F8F9F9" : "#282828"};
}
}
`;
TableWrapper.defaultProps = { theme: Base };
const TABLE_VERSION = "1";
const COLUMNS_SIZE = `oauthConfigColumnsSize_ver-${TABLE_VERSION}`;
const INFO_PANEL_COLUMNS_SIZE = `infoPanelOauthConfigColumnsSize_ver-${TABLE_VERSION}`;
const TableView = (props) => {
const {
items,
getClients,
sectionWidth,
viewAs,
setViewAs,
openSettingsModal,
openDeleteModal,
userId,
} = props;
const tableRef = useRef(null);
const [hideColumns, setHideColumns] = useState(false);
useEffect(() => {
if (!sectionWidth) return;
if (sectionWidth < 1025 || isMobile) {
viewAs !== "row" && setViewAs("row");
} else {
viewAs !== "table" && setViewAs("table");
}
}, [sectionWidth]);
const columnStorageName = `${COLUMNS_SIZE}=${userId}`;
const columnInfoPanelStorageName = `${INFO_PANEL_COLUMNS_SIZE}=${userId}`;
return (
<TableWrapper forwardedRef={tableRef} useReactWindow>
<Header
sectionWidth={sectionWidth}
tableRef={tableRef}
columnStorageName={columnStorageName}
columnInfoPanelStorageName={columnInfoPanelStorageName}
setHideColumns={setHideColumns}
/>
<TableBody
itemHeight={49}
useReactWindow
infoPanelVisible={false}
columnStorageName={columnStorageName}
columnInfoPanelStorageName={columnInfoPanelStorageName}
filesLength={items.length}
fetchMoreFiles={getClients}
hasMoreFiles={false}
itemCount={items.length}
>
{items.map((item, index) => (
<Row
key={item.id}
item={item}
index={index}
openSettingsModal={openSettingsModal}
openDeleteModal={openDeleteModal}
hideColumns={hideColumns}
/>
))}
</TableBody>
</TableWrapper>
);
};
export default inject(({ oauthStore, setup, auth }) => {
const { getClients } = oauthStore;
const { viewAs, setViewAs } = setup;
const { id: userId } = auth.userStore.user;
return {
viewAs,
setViewAs,
getClients,
userId,
};
})(observer(TableView));

View File

@ -0,0 +1,140 @@
import React from "react";
import { inject, observer } from "mobx-react";
import { isMobile } from "react-device-detect";
import styled from "styled-components";
import { ClientProps } from "@docspace/common/utils/oauth/interfaces";
//@ts-ignore
import TableContainer from "@docspace/components/table-container/TableContainer";
//@ts-ignore
import TableBody from "@docspace/components/table-container/TableBody";
//@ts-ignore
import { OAuthStoreProps, ViewAsType } from "SRC_DIR/store/OAuthStore";
import Row from "./Row";
import Header from "./Header";
const TableWrapper = styled(TableContainer)`
margin-top: 0px;
.header-container-text {
font-size: 12px;
}
.table-container_header {
position: absolute;
}
`;
const TABLE_VERSION = "1";
const COLUMNS_SIZE = `oauthConfigColumnsSize_ver-${TABLE_VERSION}`;
interface TableViewProps {
items: ClientProps[];
sectionWidth: number;
viewAs?: ViewAsType;
setViewAs?: (value: ViewAsType) => void;
userId?: string;
selection?: string[];
setSelection?: (clientId: string) => void;
bufferSelection?: ClientProps | null;
currentClient?: ClientProps | null;
setBufferSelection?: (clientId: string) => void;
changeClientStatus?: (clientId: string, status: boolean) => Promise<void>;
}
const TableView = ({
items,
sectionWidth,
viewAs,
setViewAs,
selection,
bufferSelection,
setSelection,
changeClientStatus,
userId,
}: TableViewProps) => {
const tableRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (!sectionWidth || !setViewAs) return;
if (sectionWidth < 1025 || isMobile) {
viewAs !== "row" && setViewAs("row");
} else {
viewAs !== "table" && setViewAs("table");
}
}, [sectionWidth, viewAs, setViewAs]);
const columnStorageName = `${COLUMNS_SIZE}=${userId}`;
return (
<TableWrapper forwardedRef={tableRef} useReactWindow>
<Header
sectionWidth={sectionWidth}
//@ts-ignore
tableRef={tableRef}
columnStorageName={columnStorageName}
/>
<TableBody
itemHeight={49}
useReactWindow
columnStorageName={columnStorageName}
filesLength={items.length}
fetchMoreFiles={() => console.log("call")}
hasMoreFiles={false}
itemCount={items.length}
>
{items.map((item, index) => (
<Row
key={item.clientId}
item={item}
isChecked={selection?.includes(item.clientId) || false}
inProgress={false}
setSelection={setSelection}
changeClientStatus={changeClientStatus}
/>
))}
</TableBody>
</TableWrapper>
);
};
export default inject(
({ auth, oauthStore }: { auth: any; oauthStore: OAuthStoreProps }) => {
const { id: userId } = auth.userStore.user;
const {
viewAs,
setViewAs,
selection,
bufferSelection,
setSelection,
setBufferSelection,
changeClientStatus,
currentClient,
} = oauthStore;
return {
viewAs,
setViewAs,
userId,
changeClientStatus,
selection,
bufferSelection,
setSelection,
setBufferSelection,
currentClient,
};
}
)(observer(TableView));

View File

@ -1,30 +0,0 @@
import React from "react";
import { inject, observer } from "mobx-react";
import { Consumer } from "@docspace/components/utils/context";
import TableView from "./TableView";
const List = ({ viewAs, openSettingsModal, openDeleteModal, clients }) => {
return (
<Consumer>
{(context) => (
<TableView
items={clients}
sectionWidth={context.sectionWidth}
openSettingsModal={openSettingsModal}
openDeleteModal={openDeleteModal}
/>
)}
</Consumer>
);
};
export default inject(({ setup, oauthStore }) => {
const { viewAs } = setup;
const { clients } = oauthStore;
return {
viewAs,
clients,
};
})(observer(List));

View File

@ -0,0 +1,94 @@
import styled from "styled-components";
//@ts-ignore
import { ClientProps } from "@docspace/common/utils/oauth/interfaces";
import Text from "@docspace/components/text";
//@ts-ignore
import { Consumer } from "@docspace/components/utils/context";
//@ts-ignore
import { ViewAsType } from "SRC_DIR/store/OAuthStore";
import TableView from "./TableView";
import RegisterNewButton from "../RegisterNewButton";
const StyledContainer = styled.div`
width: 100%;
display: flex;
flex-direction: column;
.header {
margin-bottom: 8px;
}
.description {
margin-bottom: 8px;
}
button {
width: fit-content;
margin-bottom: 12px;
}
`;
interface ListProps {
t: any;
clients: ClientProps[];
viewAs?: ViewAsType;
setViewAs?: (value: ViewAsType) => void;
}
const List = ({ t, clients, viewAs, setViewAs }: ListProps) => {
return (
<StyledContainer>
<Text
fontSize={"16px"}
fontWeight={700}
lineHeight={"22px"}
title={"OAuth applications"}
tag={""}
as={"p"}
color={""}
textAlign={""}
className="header"
>
{"OAuth applications"}
</Text>
<Text
fontSize={"12px"}
fontWeight={400}
lineHeight={"16px"}
title={"OAuth description"}
tag={""}
as={"p"}
color={""}
textAlign={""}
className="description"
>
{"OAuth description"}
</Text>
<RegisterNewButton t={t} />
<Consumer>
{(context: { sectionWidth: number; sectionHeight: number }) => (
<>
{viewAs === "table" ? (
<TableView
viewAs={viewAs}
setViewAs={setViewAs}
items={clients || []}
sectionWidth={context.sectionWidth}
/>
) : (
<div>row</div>
)}
</>
)}
</Consumer>
</StyledContainer>
);
};
export default List;

View File

@ -1,4 +1,3 @@
import React from "react";
import { useNavigate } from "react-router-dom";
import { isMobile } from "react-device-detect";

View File

@ -1,108 +1,239 @@
import { makeAutoObservable } from "mobx";
//@ts-ignore
import { getPortal } from "@docspace/common/api/portal";
import {
addClient,
deleteClient,
getClient,
updateClient,
changeClientStatus,
regenerateSecret,
deleteClient,
getClientList,
getScope,
getScopeList,
regenerateSecret,
updateClient,
} from "@docspace/common/api/oauth";
import {
ClientDTO,
ClientListDTO,
ClientListProps,
ClientProps,
ScopeDTO,
} from "@docspace/common/utils/oauth/dto";
Scope,
} from "@docspace/common/utils/oauth/interfaces";
const PAGE_LIMIT = 20;
export type ViewAsType = "table" | "row";
export interface OAuthStoreProps {
viewAs: ViewAsType;
deleteDialogVisible: boolean;
setDeleteDialogVisible: (value: boolean) => void;
clients: ClientProps[];
currentClient: null | ClientProps;
currentPage: number;
totalPages: number;
scopes: ScopeDTO[];
selection: string[];
setSelection: (clientId: string) => void;
fetchClient: (clientId: string) => Promise<ClientProps>;
fetchClients: (page: number, limit: number) => Promise<void>;
saveClient: (client: ClientProps) => Promise<ClientProps>;
updateClient: (clientId: string, client: ClientProps) => Promise<ClientProps>;
regenerateSecret: (clientId: string) => Promise<string>;
bufferSelection: ClientProps | null;
setBufferSelection: (clientId: string) => void;
tenant: number;
fetchTenant: () => Promise<number>;
scopes: Scope[];
setViewAs: (value: "table" | "row") => void;
fetchClient: (clientId: string) => Promise<ClientProps | undefined>;
fetchClients: (page: number) => Promise<void>;
saveClient: (client: ClientProps) => Promise<void>;
updateClient: (clientId: string, client: ClientProps) => Promise<void>;
changeClientStatus: (clientId: string, status: boolean) => Promise<void>;
regenerateSecret: (clientId: string) => Promise<string | undefined>;
deleteClient: (clientId: string) => Promise<void>;
fetchScope: (name: string) => Promise<ScopeDTO>;
fetchScope: (name: string) => Promise<Scope | undefined>;
fetchScopes: () => Promise<void>;
clientList: ClientProps[];
scopeList: ScopeDTO[];
isEmptyClientList: boolean;
hasNextPage: boolean;
scopeList: Scope[];
}
class OAuthStore implements OAuthStoreProps {
clients: ClientProps[] = [];
currentClient: ClientProps | null = null;
viewAs: "table" | "row" = "table";
scopes: ScopeDTO[] = [];
currentPage: number = 0;
totalPages: number = 0;
deleteDialogVisible: boolean = false;
selection: string[] = [];
bufferSelection: ClientProps | null = null;
tenant: number = -1;
clients: ClientProps[] = [];
scopes: Scope[] = [];
constructor() {
makeAutoObservable(this);
}
setViewAs = (value: "table" | "row") => {
this.viewAs = value;
};
setDeleteDialogVisible = (value: boolean) => {
this.deleteDialogVisible = value;
};
setSelection = (clientId: string) => {
if (this.selection.includes(clientId)) {
this.selection = this.selection.filter((s) => s !== clientId);
} else {
this.selection.push(clientId);
}
};
setBufferSelection = (clientId: string) => {
const client = this.clients.find((c) => c.clientId === clientId);
if (client) {
this.bufferSelection = { ...client, scopes: [...client.scopes] };
}
};
fetchTenant = async () => {
if (this.tenant > -1) return this.tenant;
const { tenant } = await getPortal();
this.tenant = tenant;
return tenant;
};
fetchClient = async (clientId: string) => {
const client = await getClient(clientId);
try {
const client = await getClient(clientId);
return client;
return client;
} catch (e) {
console.log(e);
}
};
fetchClients = async (page: number, limit: number) => {
const clientList: ClientListProps = await getClientList(0, 20);
fetchClients = async (page: number) => {
try {
const clientList: ClientListProps = await getClientList(0, PAGE_LIMIT);
this.clients = clientList.content;
this.totalPages = clientList.totalPages;
this.currentPage = page;
this.clients = clientList.content;
} catch (e) {
console.log(e);
}
};
//TODO: add tenant and other params
//TODO: OAuth, add tenant and other params
saveClient = async (client: ClientProps) => {
client.tenant = 1;
client.authenticationMethod = "zxc";
client.termsUrl = "zxc";
const newClient = await addClient(client);
try {
client.tenant = 1;
client.authenticationMethod = "zxc";
client.termsUrl = "zxc";
const newClient = await addClient(client);
return newClient;
this.clients.push(newClient);
} catch (e) {
console.log(e);
}
};
updateClient = async (clientId: string, client: ClientProps) => {
const newClient = await updateClient(clientId, client);
try {
const newClient = await updateClient(clientId, client);
return newClient;
const idx = this.clients.findIndex((c) => c.clientId === clientId);
if (idx > -1) {
this.clients[idx] = newClient;
}
} catch (e) {
console.log(e);
}
};
changeClientStatus = async (clientId: string, status: boolean) => {
try {
await changeClientStatus(clientId, status);
const idx = this.clients.findIndex((c) => c.clientId === clientId);
if (idx > -1) {
this.clients[idx] = { ...this.clients[idx], enabled: status };
}
} catch (e) {
console.log(e);
}
};
regenerateSecret = async (clientId: string) => {
const secret = await regenerateSecret(clientId);
try {
const secret = await regenerateSecret(clientId);
return secret;
return secret;
} catch (e) {
console.log(e);
}
};
deleteClient = async (clientId: string) => {
await deleteClient(clientId);
try {
await deleteClient(clientId);
} catch (e) {
console.log(e);
}
};
fetchScope = async (name: string) => {
const scope = await getScope(name);
try {
const scope = await getScope(name);
return scope;
return scope;
} catch (e) {
console.log(e);
}
};
fetchScopes = async () => {
const scopes = await getScopeList();
try {
const scopes = await getScopeList();
this.scopes = scopes;
this.scopes = scopes;
} catch (e) {
console.log(e);
}
};
get clientList() {
return this.clients;
}
get isEmptyClientList() {
return this.clientList.length === 0;
}
get hasNextPage() {
return this.totalPages - this.currentPage !== 0;
}
get scopeList() {
return this.scopes;
}

View File

@ -2,22 +2,23 @@ import axios, { AxiosRequestConfig } from "axios";
import {
transformToClientProps,
transformToClientDTO,
transformToClientReqDTO,
} from "./../../utils/oauth/index";
import {
ClientDTO,
ClientListDTO,
ClientListProps,
ClientProps,
ScopeDTO,
} from "../../utils/oauth/dto";
ClientResDTO,
ClientListProps,
ClientListDTO,
Scope,
} from "../../utils/oauth/interfaces";
const axiosConfig: AxiosRequestConfig = {
baseURL: "/api",
responseType: "json",
timeout: 0,
withCredentials: true,
// TODO: OAuth, remove X-Tenant into API
headers: { "X-API-Version": "1", "X-Tenant": "1" },
};
@ -36,11 +37,14 @@ const request = (options: any): Promise<any> => {
};
export const getClient = async (clientId: string): Promise<ClientProps> => {
const client: ClientDTO = await request({
const client: ClientResDTO = await request({
method: "get",
url: `/clients/${clientId}`,
headers: {},
});
client.enabled = true;
return transformToClientProps(client);
};
@ -58,6 +62,9 @@ export const getClientList = async (
data.content.forEach((item) => {
const client = transformToClientProps(item);
// TODO: OAuth, get it from request
client.enabled = true;
clients.content.push({ ...client });
});
@ -65,12 +72,15 @@ export const getClientList = async (
};
export const addClient = async (data: ClientProps): Promise<ClientProps> => {
const client: ClientDTO = await request({
const client: ClientResDTO = await request({
method: "post",
url: `/clients`,
data: transformToClientDTO(data),
data: transformToClientReqDTO(data),
});
// TODO: OAuth, get it from request
client.enabled = true;
return transformToClientProps(client);
};
@ -78,15 +88,27 @@ export const updateClient = async (
clientId: string,
data: ClientProps
): Promise<ClientProps> => {
const client: ClientDTO = await request({
const client: ClientResDTO = await request({
method: "put",
url: `/clients/${clientId}`,
data: transformToClientDTO(data),
data: transformToClientReqDTO(data),
});
// TODO: OAuth, get it from request
client.enabled = true;
return transformToClientProps(client);
};
export const changeClientStatus = async (
clientId: string,
status: boolean
): Promise<boolean> => {
console.log(`Change client:${clientId} status to ${status}`);
return !status;
};
export const regenerateSecret = async (clientId: string): Promise<string> => {
const clientSecret: string = (
await request({
@ -105,8 +127,8 @@ export const deleteClient = async (clientId: string): Promise<void> => {
});
};
export const getScope = async (name: string): Promise<ScopeDTO> => {
const scope: ScopeDTO = await request({
export const getScope = async (name: string): Promise<Scope> => {
const scope: Scope = await request({
method: "get",
url: `/scopes/${name}`,
});
@ -114,8 +136,8 @@ export const getScope = async (name: string): Promise<ScopeDTO> => {
return scope;
};
export const getScopeList = async (): Promise<ScopeDTO[]> => {
const scopeList: ScopeDTO[] = await request({
export const getScopeList = async (): Promise<Scope[]> => {
const scopeList: Scope[] = await request({
method: "get",
url: `/scopes`,
});

View File

@ -1,10 +0,0 @@
export const enum OauthScopes {
ReadFiles = "files:read",
WriteFiles = "files:write",
ReadRooms = "rooms:read",
WriteRooms = "rooms:write",
ReadAccount = "account.self:read",
WriteAccount = "account.self:write",
ReadAccounts = "accounts:read",
WriteAccounts = "accounts:write",
}

View File

@ -1,6 +1,8 @@
import { ClientDTO, ClientProps } from "./dto";
import { ClientResDTO, ClientReqDTO, ClientProps } from "./interfaces";
export const transformToClientProps = (clientDto: ClientDTO): ClientProps => {
export const transformToClientProps = (
clientDto: ClientResDTO
): ClientProps => {
const {
client_id,
client_secret,
@ -8,13 +10,14 @@ export const transformToClientProps = (clientDto: ClientDTO): ClientProps => {
terms_url,
policy_url,
logo_url,
authenticationMethod,
authentication_method,
redirect_uri,
logout_redirect_uri,
scopes,
tenant,
invalidated,
name,
enabled,
} = clientDto;
const client: ClientProps = {
@ -24,22 +27,24 @@ export const transformToClientProps = (clientDto: ClientDTO): ClientProps => {
termsUrl: terms_url,
policyUrl: policy_url,
logoUrl: logo_url,
authenticationMethod,
authenticationMethod: authentication_method,
redirectUri: redirect_uri,
logoutRedirectUri: logout_redirect_uri,
scopes,
tenant,
invalidated,
name,
enabled,
};
return client;
};
export const transformToClientDTO = (clientProps: ClientProps): ClientDTO => {
export const transformToClientReqDTO = (
clientProps: ClientProps
): ClientReqDTO => {
const {
clientId: client_id,
secret: client_secret,
name,
description,
termsUrl: terms_url,
policyUrl: policy_url,
@ -49,24 +54,21 @@ export const transformToClientDTO = (clientProps: ClientProps): ClientDTO => {
logoutRedirectUri: logout_redirect_uri,
scopes,
tenant,
invalidated,
name,
} = clientProps;
const client: ClientDTO = {
client_id,
client_secret,
const client: ClientReqDTO = {
name,
description,
terms_url,
policy_url,
logo_url,
authenticationMethod,
redirect_uri,
logout_redirect_uri,
terms_url,
policy_url,
scopes,
tenant,
invalidated,
name,
};
return client;

View File

@ -1,4 +1,4 @@
export type ScopeDTO = {
export type Scope = {
name: string;
description: string;
};
@ -6,34 +6,61 @@ export type ScopeDTO = {
export interface ClientProps {
clientId: string;
secret: string;
description: string;
termsUrl?: string;
policyUrl: string;
logoUrl: string;
authenticationMethod?: string;
redirectUri: string;
logoutRedirectUri: string;
scopes: string[];
tenant?: number;
invalidated?: boolean;
name: string;
}
export type ClientDTO = {
client_id: string;
client_secret: string;
name: string;
description: string;
terms_url: string;
policy_url: string;
logo_url: string;
logoUrl?: string;
redirectUri: string;
policyUrl: string;
termsUrl: string;
logoutRedirectUri: string;
authenticationMethod: string;
redirect_uri: string;
logout_redirect_uri: string;
scopes: string[];
enabled: boolean;
tenant: number;
invalidated?: boolean;
}
export interface ClientReqDTO {
name: string;
};
description: string;
logo_url?: string;
redirect_uri: string;
policy_url: string;
terms_url: string;
logout_redirect_uri: string;
scopes: string[];
tenant: number;
}
export interface ClientResDTO {
client_id: string;
client_secret: string;
name: string;
description: string;
logo_url?: string;
redirect_uri: string;
terms_url: string;
policy_url: string;
logout_redirect_uri: string;
authentication_method: string;
scopes: string[];
enabled: boolean;
tenant: number;
invalidated?: boolean;
}
export interface ClientListProps {
content: ClientProps[];
@ -65,7 +92,7 @@ export interface ClientListProps {
}
export type ClientListDTO = {
content: ClientDTO[];
content: ClientResDTO[];
empty: boolean;
first: boolean;
last: true;

View File

@ -127,8 +127,7 @@ class InputBlock extends React.Component {
forwardedRef={forwardedRef}
{...props}
/>
{
//iconNames.includes(iconName) && (
{iconName && (
<div className="append">
<StyledIconBlock
className={`input-block-icon ${iconButtonClassName}`}
@ -148,7 +147,7 @@ class InputBlock extends React.Component {
/>
</StyledIconBlock>
</div>
}
)}
</StyledInputGroup>
);
}