mirror of
https://github.com/logto-io/logto.git
synced 2025-02-03 21:48:55 -05:00
feat(console): add unsaved changes alert for connector config (#1414)
This commit is contained in:
parent
181e8a228d
commit
78407fc6c9
2 changed files with 135 additions and 73 deletions
|
@ -0,0 +1,126 @@
|
||||||
|
import { Connector, ConnectorDTO, ConnectorMetadata, ConnectorType } from '@logto/schemas';
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import CodeEditor from '@/components/CodeEditor';
|
||||||
|
import FormField from '@/components/FormField';
|
||||||
|
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||||
|
import useApi from '@/hooks/use-api';
|
||||||
|
import * as detailsStyles from '@/scss/details.module.scss';
|
||||||
|
|
||||||
|
import * as styles from '../index.module.scss';
|
||||||
|
import SenderTester from './SenderTester';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
connectorData: ConnectorDTO;
|
||||||
|
onConnectorUpdated: (connector: ConnectorDTO) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConnectorContent = ({ connectorData, onConnectorUpdated }: Props) => {
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
const [config, setConfig] = useState<string>();
|
||||||
|
const [saveError, setSaveError] = useState<string>();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const defaultConfig = useMemo(() => {
|
||||||
|
const hasData = Object.keys(connectorData.config).length > 0;
|
||||||
|
|
||||||
|
return hasData ? JSON.stringify(connectorData.config, null, 2) : connectorData.configTemplate;
|
||||||
|
}, [connectorData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setConfig(defaultConfig);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setConfig(defaultConfig);
|
||||||
|
};
|
||||||
|
}, [defaultConfig]);
|
||||||
|
|
||||||
|
const hasUnsavedChanges = useMemo(() => {
|
||||||
|
if (!config) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedConfig = JSON.stringify(JSON.parse(config), null, 2);
|
||||||
|
|
||||||
|
return parsedConfig !== defaultConfig;
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}, [config, defaultConfig]);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setSaveError(undefined);
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
setSaveError(t('connector_details.save_error_empty_config'));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const configJson = JSON.parse(config) as JSON;
|
||||||
|
setIsSubmitting(true);
|
||||||
|
const { metadata, ...reset } = await api
|
||||||
|
.patch(`/api/connectors/${connectorData.id}`, {
|
||||||
|
json: { config: configJson },
|
||||||
|
})
|
||||||
|
.json<
|
||||||
|
Connector & {
|
||||||
|
metadata: ConnectorMetadata;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
onConnectorUpdated({ ...reset, ...metadata });
|
||||||
|
toast.success(t('general.saved'));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof SyntaxError) {
|
||||||
|
setSaveError(t('connector_details.save_error_json_parse_error'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.main}>
|
||||||
|
<FormField title="admin_console.connector_details.edit_config_label">
|
||||||
|
<CodeEditor
|
||||||
|
className={styles.codeEditor}
|
||||||
|
language="json"
|
||||||
|
value={config}
|
||||||
|
onChange={(value) => {
|
||||||
|
setConfig(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
{connectorData.type !== ConnectorType.Social && (
|
||||||
|
<SenderTester
|
||||||
|
connectorId={connectorData.id}
|
||||||
|
connectorType={connectorData.type}
|
||||||
|
config={config}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{saveError && <div>{saveError}</div>}
|
||||||
|
</div>
|
||||||
|
<div className={detailsStyles.footer}>
|
||||||
|
<div className={detailsStyles.footerMain}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
title="admin_console.general.save_changes"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
onClick={handleSave}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<UnsavedChangesAlertModal hasUnsavedChanges={hasUnsavedChanges} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConnectorContent;
|
|
@ -1,6 +1,6 @@
|
||||||
import { AppearanceMode, ConnectorDTO, ConnectorType } from '@logto/schemas';
|
import { AppearanceMode, ConnectorDTO, ConnectorType } from '@logto/schemas';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
@ -9,11 +9,9 @@ import useSWR from 'swr';
|
||||||
import ActionMenu, { ActionMenuItem } from '@/components/ActionMenu';
|
import ActionMenu, { ActionMenuItem } from '@/components/ActionMenu';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import CodeEditor from '@/components/CodeEditor';
|
|
||||||
import CopyToClipboard from '@/components/CopyToClipboard';
|
import CopyToClipboard from '@/components/CopyToClipboard';
|
||||||
import DetailsSkeleton from '@/components/DetailsSkeleton';
|
import DetailsSkeleton from '@/components/DetailsSkeleton';
|
||||||
import Drawer from '@/components/Drawer';
|
import Drawer from '@/components/Drawer';
|
||||||
import FormField from '@/components/FormField';
|
|
||||||
import LinkButton from '@/components/LinkButton';
|
import LinkButton from '@/components/LinkButton';
|
||||||
import Markdown from '@/components/Markdown';
|
import Markdown from '@/components/Markdown';
|
||||||
import Status from '@/components/Status';
|
import Status from '@/components/Status';
|
||||||
|
@ -29,20 +27,17 @@ import Reset from '@/icons/Reset';
|
||||||
import * as detailsStyles from '@/scss/details.module.scss';
|
import * as detailsStyles from '@/scss/details.module.scss';
|
||||||
|
|
||||||
import CreateForm from '../Connectors/components/CreateForm';
|
import CreateForm from '../Connectors/components/CreateForm';
|
||||||
|
import ConnectorContent from './components/ConnectorContent';
|
||||||
import ConnectorTabs from './components/ConnectorTabs';
|
import ConnectorTabs from './components/ConnectorTabs';
|
||||||
import ConnectorTypeName from './components/ConnectorTypeName';
|
import ConnectorTypeName from './components/ConnectorTypeName';
|
||||||
import SenderTester from './components/SenderTester';
|
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
const ConnectorDetails = () => {
|
const ConnectorDetails = () => {
|
||||||
const { connectorId } = useParams();
|
const { connectorId } = useParams();
|
||||||
const [isReadMeOpen, setIsReadMeOpen] = useState(false);
|
const [isReadMeOpen, setIsReadMeOpen] = useState(false);
|
||||||
const [config, setConfig] = useState<string>();
|
|
||||||
const [saveError, setSaveError] = useState<string>();
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const [isSetupOpen, setIsSetupOpen] = useState(false);
|
const [isSetupOpen, setIsSetupOpen] = useState(false);
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
const { data, error } = useSWR<ConnectorDTO, RequestError>(
|
const { data, error, mutate } = useSWR<ConnectorDTO, RequestError>(
|
||||||
connectorId && `/api/connectors/${connectorId}`
|
connectorId && `/api/connectors/${connectorId}`
|
||||||
);
|
);
|
||||||
const inUse = useConnectorInUse(data?.type, data?.target);
|
const inUse = useConnectorInUse(data?.type, data?.target);
|
||||||
|
@ -51,44 +46,6 @@ const ConnectorDetails = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
const hasData = Object.keys(data.config).length > 0;
|
|
||||||
setConfig(hasData ? JSON.stringify(data.config, null, 2) : data.configTemplate);
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
if (!connectorId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSaveError(undefined);
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
setSaveError(t('connector_details.save_error_empty_config'));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const configJson = JSON.parse(config) as JSON;
|
|
||||||
setIsSubmitting(true);
|
|
||||||
await api
|
|
||||||
.patch(`/api/connectors/${connectorId}`, {
|
|
||||||
json: { config: configJson },
|
|
||||||
})
|
|
||||||
.json<ConnectorDTO>();
|
|
||||||
toast.success(t('general.saved'));
|
|
||||||
} catch (error: unknown) {
|
|
||||||
if (error instanceof SyntaxError) {
|
|
||||||
setSaveError(t('connector_details.save_error_json_parse_error'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!connectorId) {
|
if (!connectorId) {
|
||||||
return;
|
return;
|
||||||
|
@ -208,33 +165,12 @@ const ConnectorDetails = () => {
|
||||||
{t('general.settings_nav')}
|
{t('general.settings_nav')}
|
||||||
</TabNavItem>
|
</TabNavItem>
|
||||||
</TabNav>
|
</TabNav>
|
||||||
<div className={styles.main}>
|
<ConnectorContent
|
||||||
<FormField title="admin_console.connector_details.edit_config_label">
|
connectorData={data}
|
||||||
<CodeEditor
|
onConnectorUpdated={(connector) => {
|
||||||
className={styles.codeEditor}
|
void mutate(connector);
|
||||||
language="json"
|
|
||||||
value={config}
|
|
||||||
onChange={(value) => {
|
|
||||||
setConfig(value);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
|
||||||
{data.type !== ConnectorType.Social && (
|
|
||||||
<SenderTester connectorId={data.id} connectorType={data.type} config={config} />
|
|
||||||
)}
|
|
||||||
{saveError && <div>{saveError}</div>}
|
|
||||||
</div>
|
|
||||||
<div className={detailsStyles.footer}>
|
|
||||||
<div className={detailsStyles.footerMain}>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
title="admin_console.general.save_changes"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
onClick={handleSave}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Reference in a new issue