From d2769823dabff843338c5341b389e9cd387687df Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Mon, 13 Mar 2023 16:01:12 +0800 Subject: [PATCH] refactor(console): add dedicated routes to AC guides (#3358) --- .../src/containers/ConsoleContent/index.tsx | 2 + .../src/pages/ApplicationDetails/index.tsx | 12 +- .../ApplicationsPlaceholder/index.tsx | 39 +--- .../components/CreateForm/index.tsx | 145 +++++++-------- .../Applications/components/Guide/index.tsx | 140 +++++++------- .../console/src/pages/Applications/index.tsx | 62 ++++--- .../components/ConnectorForm/ConfigForm.tsx | 2 +- .../components/CreateForm/index.tsx | 54 ++---- .../Connectors/components/Guide/index.tsx | 175 ++++++++++-------- .../console/src/pages/Connectors/index.tsx | 65 +++++-- 10 files changed, 362 insertions(+), 334 deletions(-) diff --git a/packages/console/src/containers/ConsoleContent/index.tsx b/packages/console/src/containers/ConsoleContent/index.tsx index 645216c10..da4fb3aaa 100644 --- a/packages/console/src/containers/ConsoleContent/index.tsx +++ b/packages/console/src/containers/ConsoleContent/index.tsx @@ -55,6 +55,7 @@ const ConsoleContent = () => { } /> } /> + } /> } /> @@ -77,6 +78,7 @@ const ConsoleContent = () => { } /> } /> } /> + } /> } /> diff --git a/packages/console/src/pages/ApplicationDetails/index.tsx b/packages/console/src/pages/ApplicationDetails/index.tsx index 959578d27..5d78785fe 100644 --- a/packages/console/src/pages/ApplicationDetails/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/index.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { toast } from 'react-hot-toast'; import { Trans, useTranslation } from 'react-i18next'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; import useSWR from 'swr'; import Back from '@/assets/images/back.svg'; @@ -42,6 +42,8 @@ const mapToUriOriginFormatArrays = (value?: string[]) => const ApplicationDetails = () => { const { id } = useParams(); + const { pathname } = useLocation(); + const isGuideView = !!id && pathname === `/applications/${id}/guide`; const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { data, error, mutate } = useSWR( id && `api/applications/${id}` @@ -224,6 +226,14 @@ const ApplicationDetails = () => { )} + {isGuideView && ( + { + navigate(`/applications/${id}`); + }} + /> + )} ); }; diff --git a/packages/console/src/pages/Applications/components/ApplicationsPlaceholder/index.tsx b/packages/console/src/pages/Applications/components/ApplicationsPlaceholder/index.tsx index 88bb470d0..161058958 100644 --- a/packages/console/src/pages/Applications/components/ApplicationsPlaceholder/index.tsx +++ b/packages/console/src/pages/Applications/components/ApplicationsPlaceholder/index.tsx @@ -3,27 +3,24 @@ import { ApplicationType } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import Modal from 'react-modal'; -import { useNavigate } from 'react-router-dom'; import Button from '@/components/Button'; import useApi from '@/hooks/use-api'; import useConfigs from '@/hooks/use-configs'; -import * as modalStyles from '@/scss/modal.module.scss'; import { applicationTypeI18nKey } from '@/types/applications'; -import Guide from '../Guide'; import TypeDescription from '../TypeDescription'; import * as styles from './index.module.scss'; const defaultAppName = 'My App'; -const ApplicationsPlaceholder = () => { +type Props = { + onCreate: (createdApp: Application) => void; +}; + +const ApplicationsPlaceholder = ({ onCreate }: Props) => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const navigate = useNavigate(); const [isCreating, setIsCreating] = useState(false); - const [createdApplication, setCreatedApplication] = useState(); - const isGetStartedModalOpen = Boolean(createdApplication); const api = useApi(); const { updateConfigs } = useConfigs(); @@ -41,28 +38,18 @@ const ApplicationsPlaceholder = () => { try { const createdApp = await api.post('api/applications', { json: payload }).json(); - setCreatedApplication(createdApp); - void updateConfigs({ applicationCreated: true, ...conditional( createdApp.type === ApplicationType.MachineToMachine && { m2mApplicationCreated: true } ), }); + onCreate(createdApp); } finally { setIsCreating(false); } }; - const closeGuideModal = () => { - if (!createdApplication) { - return; - } - - navigate(`/applications/${createdApplication.id}`); - setCreatedApplication(undefined); - }; - return (
{t('applications.placeholder_title')}
@@ -81,23 +68,13 @@ const ApplicationsPlaceholder = () => { className={styles.createButton} disabled={isCreating} title="general.create" - onClick={async () => { - await handleCreate(type); + onClick={() => { + void handleCreate(type); }} />
))} - {createdApplication && ( - - - - )} ); }; diff --git a/packages/console/src/pages/Applications/components/CreateForm/index.tsx b/packages/console/src/pages/Applications/components/CreateForm/index.tsx index 5de5dae6c..3d3038bf1 100644 --- a/packages/console/src/pages/Applications/components/CreateForm/index.tsx +++ b/packages/console/src/pages/Applications/components/CreateForm/index.tsx @@ -1,7 +1,6 @@ import type { Application } from '@logto/schemas'; import { ApplicationType } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; -import { useState } from 'react'; import { useController, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import Modal from 'react-modal'; @@ -16,7 +15,6 @@ import useConfigs from '@/hooks/use-configs'; import * as modalStyles from '@/scss/modal.module.scss'; import { applicationTypeI18nKey } from '@/types/applications'; -import Guide from '../Guide'; import TypeDescription from '../TypeDescription'; import * as styles from './index.module.scss'; @@ -27,13 +25,12 @@ type FormData = { }; type Props = { + isOpen: boolean; onClose?: (createdApp?: Application) => void; }; -const CreateForm = ({ onClose }: Props) => { +const CreateForm = ({ isOpen, onClose }: Props) => { const { updateConfigs } = useConfigs(); - const [createdApp, setCreatedApp] = useState(); - const [isGetStartedModalOpen, setIsGetStartedModalOpen] = useState(false); const { handleSubmit, control, @@ -46,10 +43,9 @@ const CreateForm = ({ onClose }: Props) => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const api = useApi(); - const closeModal = () => { - setIsGetStartedModalOpen(false); - onClose?.(createdApp); - }; + if (!isOpen) { + return null; + } const onSubmit = handleSubmit(async (data) => { if (isSubmitting) { @@ -57,83 +53,82 @@ const CreateForm = ({ onClose }: Props) => { } const createdApp = await api.post('api/applications', { json: data }).json(); - setCreatedApp(createdApp); - setIsGetStartedModalOpen(true); void updateConfigs({ applicationCreated: true, ...conditional( createdApp.type === ApplicationType.MachineToMachine && { m2mApplicationCreated: true } ), }); + onClose?.(createdApp); }); return ( - - } - onClose={onClose} + { + onClose?.(); + }} > -
- - - {Object.values(ApplicationType).map((value) => ( - - - - ))} - - {errors.type?.type === 'required' && ( -
{t('applications.no_application_type_selected')}
- )} -
- - - - - - -
- {createdApp && ( - - - - )} -
+ } + onClose={onClose} + > +
+ + + {Object.values(ApplicationType).map((value) => ( + + + + ))} + + {errors.type?.type === 'required' && ( +
{t('applications.no_application_type_selected')}
+ )} +
+ + + + + + +
+ + ); }; diff --git a/packages/console/src/pages/Applications/components/Guide/index.tsx b/packages/console/src/pages/Applications/components/Guide/index.tsx index d7b67ba3d..3481cb8d8 100644 --- a/packages/console/src/pages/Applications/components/Guide/index.tsx +++ b/packages/console/src/pages/Applications/components/Guide/index.tsx @@ -4,12 +4,14 @@ import type { Optional } from '@silverhand/essentials'; import i18next from 'i18next'; import type { MDXProps } from 'mdx/types'; import type { LazyExoticComponent } from 'react'; -import { useContext, cloneElement, lazy, Suspense, useEffect, useState } from 'react'; +import { useEffect, useContext, cloneElement, lazy, Suspense, useState } from 'react'; +import Modal from 'react-modal'; import CodeEditor from '@/components/CodeEditor'; import TextLink from '@/components/TextLink'; import { AppEndpointsContext } from '@/contexts/AppEndpointsProvider'; import DetailsSummary from '@/mdx-components/DetailsSummary'; +import * as modalStyles from '@/scss/modal.module.scss'; import type { SupportedSdk } from '@/types/applications'; import { applicationTypeAndSdkTypeMappings } from '@/types/applications'; @@ -19,9 +21,9 @@ import StepsSkeleton from '../StepsSkeleton'; import * as styles from './index.module.scss'; type Props = { - app: Application; + app?: Application; isCompact?: boolean; - onClose: () => void; + onClose: (id: string) => void; }; const Guides: Record JSX.Element>> = { @@ -50,83 +52,93 @@ const Guides: Record JSX.Elemen }; const Guide = ({ app, isCompact, onClose }: Props) => { - const { id: appId, secret: appSecret, name: appName, type: appType, oidcClientMetadata } = app; - const sdks = applicationTypeAndSdkTypeMappings[appType]; - const [selectedSdk, setSelectedSdk] = useState>(sdks[0]); + const sdks = app && applicationTypeAndSdkTypeMappings[app.type]; + const [selectedSdk, setSelectedSdk] = useState>(); const [activeStepIndex, setActiveStepIndex] = useState(-1); const { userEndpoint } = useContext(AppEndpointsContext); - // Directly close guide if no SDK available useEffect(() => { - if (!selectedSdk) { - onClose(); + if (sdks?.length) { + setSelectedSdk(sdks[0]); } - }, [onClose, selectedSdk]); + }, [sdks]); - if (!selectedSdk) { + if (!app || !sdks || !selectedSdk) { return null; } + const { id: appId, secret: appSecret, name: appName, oidcClientMetadata } = app; const locale = i18next.language; const guideI18nKey = `${selectedSdk}_${locale}`.toLowerCase(); const GuideComponent = Guides[guideI18nKey] ?? Guides[selectedSdk.toLowerCase()]; - return ( -
- -
- {cloneElement(, { - className: styles.banner, - isCompact, - onChange: setSelectedSdk, - onToggle: () => { - setActiveStepIndex(0); - }, - })} - { - const [, language] = /language-(\w+)/.exec(className ?? '') ?? []; + const closeModal = () => { + onClose(appId); + }; - return language ? ( - - ) : ( - {String(children)} - ); + return ( + +
+ +
+ {cloneElement(, { + className: styles.banner, + isCompact, + onChange: setSelectedSdk, + onToggle: () => { + setActiveStepIndex(0); }, - a: ({ children, ...props }) => ( - - {children} - - ), - details: DetailsSummary, - }} - > - }> - {GuideComponent && ( - { - setActiveStepIndex(nextIndex); - }} - onComplete={onClose} - /> - )} - - + })} + { + const [, language] = /language-(\w+)/.exec(className ?? '') ?? []; + + return language ? ( + + ) : ( + {String(children)} + ); + }, + a: ({ children, ...props }) => ( + + {children} + + ), + details: DetailsSummary, + }} + > + }> + {GuideComponent && ( + { + setActiveStepIndex(nextIndex); + }} + onComplete={closeModal} + /> + )} + + +
-
+ ); }; diff --git a/packages/console/src/pages/Applications/index.tsx b/packages/console/src/pages/Applications/index.tsx index 68588553f..6ea53a2b9 100644 --- a/packages/console/src/pages/Applications/index.tsx +++ b/packages/console/src/pages/Applications/index.tsx @@ -1,6 +1,6 @@ import type { Application } from '@logto/schemas'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import Modal from 'react-modal'; import { useLocation, useNavigate } from 'react-router-dom'; import useSWR from 'swr'; @@ -15,7 +15,6 @@ import Table from '@/components/Table'; import { defaultPageSize } from '@/consts'; import type { RequestError } from '@/hooks/use-api'; import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher'; -import * as modalStyles from '@/scss/modal.module.scss'; import * as resourcesStyles from '@/scss/resources.module.scss'; import { applicationTypeI18nKey } from '@/types/applications'; import { buildUrl } from '@/utils/url'; @@ -28,11 +27,12 @@ const pageSize = defaultPageSize; const applicationsPathname = '/applications'; const createApplicationPathname = `${applicationsPathname}/create`; const buildDetailsPathname = (id: string) => `${applicationsPathname}/${id}`; +const buildGuidePathname = (id: string) => `${buildDetailsPathname(id)}/guide`; const Applications = () => { const navigate = useNavigate(); const { pathname, search } = useLocation(); - const isCreateNew = pathname === createApplicationPathname; + const isShowingCreationForm = pathname === createApplicationPathname; const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const [{ page }, updateSearchParameters] = useSearchParametersWatcher({ @@ -49,6 +49,13 @@ const Applications = () => { const isLoading = !data && !error; const [applications, totalCount] = data ?? []; + const mutateApplicationList = useCallback( + async (newApp: Application) => { + await mutate([[newApp, ...(applications ?? [])], (totalCount ?? 0) + 1]); + }, + [applications, totalCount, mutate] + ); + return (
@@ -65,32 +72,6 @@ const Applications = () => { }); }} /> - { - navigate({ - pathname: applicationsPathname, - search, - }); - }} - > - { - if (createdApp) { - navigate(buildDetailsPathname(createdApp.id), { replace: true }); - - return; - } - navigate({ - pathname: applicationsPathname, - search, - }); - }} - /> -
{ render: ({ id }) => , }, ]} - placeholder={} + placeholder={ + { + await mutateApplicationList(newApp); + navigate(buildGuidePathname(newApp.id), { replace: true }); + }} + /> + } rowClickHandler={({ id }) => { navigate(buildDetailsPathname(id)); }} @@ -134,6 +122,20 @@ const Applications = () => { updateSearchParameters({ page }); }} /> + { + if (newApp) { + navigate(buildGuidePathname(newApp.id), { replace: true }); + + return; + } + navigate({ + pathname: applicationsPathname, + search, + }); + }} + /> ); }; diff --git a/packages/console/src/pages/Connectors/components/ConnectorForm/ConfigForm.tsx b/packages/console/src/pages/Connectors/components/ConnectorForm/ConfigForm.tsx index 68941810c..613af0d0b 100644 --- a/packages/console/src/pages/Connectors/components/ConnectorForm/ConfigForm.tsx +++ b/packages/console/src/pages/Connectors/components/ConnectorForm/ConfigForm.tsx @@ -20,7 +20,7 @@ type Props = { formItems?: ConnectorConfigFormItem[]; className?: string; connectorId: string; - connectorType: ConnectorType; + connectorType?: ConnectorType; }; const ConfigForm = ({ diff --git a/packages/console/src/pages/Connectors/components/CreateForm/index.tsx b/packages/console/src/pages/Connectors/components/CreateForm/index.tsx index 3219ac6d6..898e198f9 100644 --- a/packages/console/src/pages/Connectors/components/CreateForm/index.tsx +++ b/packages/console/src/pages/Connectors/components/CreateForm/index.tsx @@ -14,7 +14,6 @@ import type { RequestError } from '@/hooks/use-api'; import * as modalStyles from '@/scss/modal.module.scss'; import { getConnectorGroups } from '../../utils'; -import Guide from '../Guide'; import PlatformSelector from './PlatformSelector'; import * as styles from './index.module.scss'; import { getConnectorOrder } from './utils'; @@ -37,7 +36,6 @@ const CreateForm = ({ onClose, isOpen: isFormOpen, type }: Props) => { const isLoading = !factories && !existingConnectors && !connectorsError && !factoriesError; const [activeGroupId, setActiveGroupId] = useState(); const [activeFactoryId, setActiveFactoryId] = useState(); - const [isGetStartedModalOpen, setIsGetStartedModalOpen] = useState(false); const groups = useMemo(() => { if (!factories || !existingConnectors) { @@ -73,11 +71,6 @@ const CreateForm = ({ onClose, isOpen: isFormOpen, type }: Props) => { [activeGroupId, groups] ); - const activeFactory = useMemo( - () => factories?.find(({ id }) => id === activeFactoryId), - [activeFactoryId, factories] - ); - const cardTitle = useMemo(() => { if (type === ConnectorType.Email) { return 'connectors.setup_title.email'; @@ -90,6 +83,22 @@ const CreateForm = ({ onClose, isOpen: isFormOpen, type }: Props) => { return 'connectors.setup_title.social'; }, [type]); + const modalSize = useMemo(() => { + if (groups.length <= 2) { + return 'medium'; + } + + if (groups.length === 3) { + return 'large'; + } + + return 'xlarge'; + }, [groups.length]); + + if (!isFormOpen) { + return null; + } + const handleGroupChange = (groupId: string) => { setActiveGroupId(groupId); @@ -104,25 +113,6 @@ const CreateForm = ({ onClose, isOpen: isFormOpen, type }: Props) => { setActiveFactoryId(firstAvailableConnector?.id); }; - const closeModal = () => { - setIsGetStartedModalOpen(false); - onClose?.(activeFactoryId); - setActiveGroupId(undefined); - setActiveFactoryId(undefined); - }; - - const modalSize = useMemo(() => { - if (groups.length <= 2) { - return 'medium'; - } - - if (groups.length === 3) { - return 'large'; - } - - return 'xlarge'; - }, [groups]); - return ( { type="primary" disabled={!activeFactoryId} onClick={() => { - setIsGetStartedModalOpen(true); + onClose?.(activeFactoryId); }} /> } @@ -181,16 +171,6 @@ const CreateForm = ({ onClose, isOpen: isFormOpen, type }: Props) => { onConnectorIdChange={setActiveFactoryId} /> )} - {activeFactory && ( - - - - )} ); diff --git a/packages/console/src/pages/Connectors/components/Guide/index.tsx b/packages/console/src/pages/Connectors/components/Guide/index.tsx index 33194467f..d01c697a1 100644 --- a/packages/console/src/pages/Connectors/components/Guide/index.tsx +++ b/packages/console/src/pages/Connectors/components/Guide/index.tsx @@ -5,10 +5,11 @@ import { ConnectorType } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import i18next from 'i18next'; import { HTTPError } from 'ky'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; +import Modal from 'react-modal'; import { useNavigate } from 'react-router-dom'; import Close from '@/assets/images/close.svg'; @@ -21,6 +22,7 @@ import { ConnectorsTabs } from '@/consts/page-tabs'; import useApi from '@/hooks/use-api'; import useConfigs from '@/hooks/use-configs'; import SenderTester from '@/pages/ConnectorDetails/components/SenderTester'; +import * as modalStyles from '@/scss/modal.module.scss'; import type { ConnectorFormType } from '../../types'; import { SyncProfileMode } from '../../types'; @@ -34,30 +36,22 @@ import * as styles from './index.module.scss'; const targetErrorCode = 'connector.multiple_target_with_same_platform'; type Props = { - connector: ConnectorFactoryResponse; - onClose: () => void; + connector?: ConnectorFactoryResponse; + onClose: (id?: string) => void; }; const Guide = ({ connector, onClose }: Props) => { const api = useApi({ hideErrorToast: true }); const navigate = useNavigate(); - const [callbackConnectorId, setCallbackConnectorId] = useState(generateStandardId()); + const callbackConnectorId = useRef(generateStandardId()); const { updateConfigs } = useConfigs(); const parseJsonConfig = useConfigParser(); const [conflictConnectorName, setConflictConnectorName] = useState>(); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const { - id: connectorId, - type: connectorType, - name, - readme, - formItems, - target, - isStandard, - } = connector; - const { title, content } = splitMarkdownByTitle(readme); + const { type: connectorType, formItems, target, isStandard } = connector ?? {}; + const { language } = i18next; - const connectorName = conditional(isLanguageTag(language) && name[language]) ?? name.en; + const isSocialConnector = connectorType !== ConnectorType.Sms && connectorType !== ConnectorType.Email; const methods = useForm({ @@ -76,11 +70,19 @@ const Guide = ({ connector, onClose }: Props) => { } = methods; useEffect(() => { - if (isSocialConnector && !isStandard) { + if (isSocialConnector && !isStandard && target) { setValue('target', target); } }, [isSocialConnector, target, isStandard, setValue]); + if (!connector) { + return null; + } + + const { id: connectorId, name, readme, configTemplate } = connector; + const { title, content } = splitMarkdownByTitle(readme); + const connectorName = conditional(isLanguageTag(language) && name[language]) ?? name.en; + const onSubmit = handleSubmit(async (data) => { if (isSubmitting) { return; @@ -89,14 +91,13 @@ const Guide = ({ connector, onClose }: Props) => { // Recover error state setConflictConnectorName(undefined); - const { formItems, isStandard, id: connectorId, type } = connector; const config = formItems ? parseFormConfig(data, formItems) : parseJsonConfig(data.config); const { syncProfile, name, logo, logoDark, target } = data; const basePayload = { config, connectorId, - id: conditional(type === ConnectorType.Social && callbackConnectorId), + id: conditional(connectorType === ConnectorType.Social && callbackConnectorId.current), metadata: conditional( isStandard && { logo, @@ -149,77 +150,91 @@ const Guide = ({ connector, onClose }: Props) => { }); return ( -
-
- - - -
- {connectorName}} - subtitle="connectors.guide.subtitle" - /> -
-
-
-
README: {title}
- {content} + { + onClose(); + }} + > +
+
+ { + onClose(callbackConnectorId.current); + }} + > + + +
+ {connectorName}} + subtitle="connectors.guide.subtitle" + />
-
- -
- {isSocialConnector && ( +
+
+
README: {title}
+ {content} +
+
+ + + {isSocialConnector && ( +
+
+
1
+
{t('connectors.guide.general_setting')}
+
+ +
+ )}
-
1
-
{t('connectors.guide.general_setting')}
+
{isSocialConnector ? 2 : 1}
+
{t('connectors.guide.parameter_configuration')}
- -
- )} -
-
-
{isSocialConnector ? 2 : 1}
-
{t('connectors.guide.parameter_configuration')}
-
- -
- {!isSocialConnector && ( -
-
-
2
-
{t('connectors.guide.test_connection')}
-
-
- )} -
-
- -
+ {!isSocialConnector && ( +
+
+
2
+
{t('connectors.guide.test_connection')}
+
+ +
+ )} +
+
+ + +
-
+ ); }; diff --git a/packages/console/src/pages/Connectors/index.tsx b/packages/console/src/pages/Connectors/index.tsx index 8199e3358..fd09e0f95 100644 --- a/packages/console/src/pages/Connectors/index.tsx +++ b/packages/console/src/pages/Connectors/index.tsx @@ -1,9 +1,11 @@ +import type { ConnectorFactoryResponse } from '@logto/schemas'; import { ConnectorType } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import classNames from 'classnames'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useParams } from 'react-router-dom'; +import useSWR from 'swr'; import Plus from '@/assets/images/plus.svg'; import SocialConnectorEmptyDark from '@/assets/images/social-connector-empty-dark.svg'; @@ -15,6 +17,7 @@ import Table from '@/components/Table'; import TablePlaceholder from '@/components/Table/TablePlaceholder'; import { defaultEmailConnectorGroup, defaultSmsConnectorGroup } from '@/consts'; import { ConnectorsTabs } from '@/consts/page-tabs'; +import type { RequestError } from '@/hooks/use-api'; import useConnectorGroups from '@/hooks/use-connector-groups'; import useDocumentationUrl from '@/hooks/use-documentation-url'; import * as resourcesStyles from '@/scss/resources.module.scss'; @@ -24,6 +27,7 @@ import ConnectorStatus from './components/ConnectorStatus'; import ConnectorStatusField from './components/ConnectorStatusField'; import ConnectorTypeColumn from './components/ConnectorTypeColumn'; import CreateForm from './components/CreateForm'; +import Guide from './components/Guide'; import SignInExperienceSetupNotice from './components/SignInExperienceSetupNotice'; import * as styles from './index.module.scss'; @@ -31,10 +35,19 @@ const basePathname = '/connectors'; const passwordlessPathname = `${basePathname}/${ConnectorsTabs.Passwordless}`; const socialPathname = `${basePathname}/${ConnectorsTabs.Social}`; -const buildCreatePathname = (connectorType: ConnectorType) => { - const pathname = connectorType === ConnectorType.Social ? socialPathname : passwordlessPathname; +const buildTabPathname = (connectorType: ConnectorType) => + connectorType === ConnectorType.Social ? socialPathname : passwordlessPathname; - return `${pathname}/create/${connectorType}`; +const buildCreatePathname = (connectorType: ConnectorType) => { + const tabPath = buildTabPathname(connectorType); + + return `${tabPath}/create/${connectorType}`; +}; + +const buildGuidePathname = (connectorType: ConnectorType, factoryId: string) => { + const tabPath = buildTabPathname(connectorType); + + return `${tabPath}/guide/${factoryId}`; }; const isConnectorType = (value: string): value is ConnectorType => @@ -44,14 +57,20 @@ const parseToConnectorType = (value?: string): ConnectorType | undefined => conditional(value && isConnectorType(value) && value); const Connectors = () => { - const { tab = ConnectorsTabs.Passwordless, createType } = useParams(); + const { tab = ConnectorsTabs.Passwordless, createType, factoryId } = useParams(); const createConnectorType = parseToConnectorType(createType); const navigate = useNavigate(); const isSocial = tab === ConnectorsTabs.Social; const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { getDocumentationUrl } = useDocumentationUrl(); + const { data, error, mutate } = useConnectorGroups(); - const isLoading = !data && !error; + const { data: factories, error: factoriesError } = useSWR< + ConnectorFactoryResponse[], + RequestError + >('api/connector-factories'); + + const isLoading = !data && !factories && !error && !factoriesError; const passwordlessConnectors = useMemo(() => { const smsConnector = @@ -70,6 +89,12 @@ const Connectors = () => { const connectors = isSocial ? socialConnectors : passwordlessConnectors; + const connectorToShowInGuide = useMemo(() => { + if (factories && factoryId) { + return factories.find(({ id }) => id === factoryId); + } + }, [factoryId, factories]); + return ( <>
@@ -159,16 +184,26 @@ const Connectors = () => { onRetry={async () => mutate(undefined, true)} />
- {Boolean(createConnectorType) && ( - { - navigate(`${basePathname}/${tab}`); - void mutate(); - }} - /> - )} + { + await mutate(); + + if (createConnectorType && id) { + navigate(buildGuidePathname(createConnectorType, id), { replace: true }); + + return; + } + navigate(`${basePathname}/${tab}`); + }} + /> + { + navigate(`${basePathname}/${tab}`); + }} + /> ); };