0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-24 22:05:56 -05:00

feat(console,phrases): add third party app list page (#5149)

* feat(console,phrases): add new third-party applicaiton to the guide library

add new third-party applicaiton to the guide library

* feat(console,phrases): add third party app list page

add third party app list page

* fix(console): fix page size

fix page size

* fix(console): fix rebase issue

fix rebase issue

* fix(test): fix rebase issue

fix rebase issue
This commit is contained in:
simeng-li 2024-01-05 12:27:27 +08:00 committed by GitHub
parent a85266284b
commit 1c3ada0123
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 463 additions and 32 deletions

View file

@ -11,7 +11,7 @@ import {
ApplicationDetailsTabs,
EnterpriseSsoDetailsTabs,
} from '@/consts';
import { isCloud } from '@/consts/env';
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
import { TenantsContext } from '@/contexts/TenantsProvider';
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
import useUserPreferences from '@/hooks/use-user-preferences';
@ -85,6 +85,12 @@ function ConsoleContent() {
<Route path="dashboard" element={<Dashboard />} />
<Route path="applications">
<Route index element={<Applications />} />
{isDevFeaturesEnabled && (
<Route
path="third-party-applications"
element={<Applications tab="thirdPartyApplications" />}
/>
)}
<Route path="create" element={<Applications />} />
<Route path=":id/guide/:guideId" element={<ApplicationDetails />} />
<Route path=":id">

View file

@ -19,6 +19,7 @@ import { addSupportQuotaToPlan } from '@/utils/subscription';
*/
const useSubscriptionPlans = () => {
const cloudApi = useCloudApi();
const useSwrResponse = useSWRImmutable<SubscriptionPlanResponse[], Error>(
isCloud && '/api/subscription-plans',
async () => cloudApi.get('/api/subscription-plans')

View file

@ -8,6 +8,7 @@ import {
useLocation,
useNavigate,
useHref,
type NavigateFunction,
} from 'react-router-dom';
import { isCloud } from '@/consts/env';
@ -43,7 +44,7 @@ type TenantPathname = {
*/
getTo: (to: To) => To;
/** Navigate to the given pathname in the current tenant. */
navigate: (to: To, options?: NavigateOptions) => void;
navigate: NavigateFunction;
/** Returns the full URL with the current tenant ID prepended. */
getUrl: (pathname: string) => URL;
};
@ -112,7 +113,13 @@ function useTenantPathname(): TenantPathname {
const data = useMemo(
() => ({
match,
navigate: (to: To, options?: NavigateOptions) => {
navigate: (to: To | number, options?: NavigateOptions) => {
// Navigate to the given index in the history stack
if (typeof to === 'number') {
navigate(to);
return;
}
navigate(getTo(to), options);
},
getPathname,

View file

@ -0,0 +1,108 @@
import { type Application } from '@logto/schemas';
import { useCallback, useEffect, useMemo, useState } from 'react';
import useSWR from 'swr';
import { defaultPageSize } from '@/consts';
import { type RequestError } from '@/hooks/use-api';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import { buildUrl } from '@/utils/url';
const pageSize = defaultPageSize;
const applicationsEndpoint = 'api/applications';
/**
* @typeof {Object} ApplicationData
*
* @property data - The application data of the current active tab, returned from useSWR.
* @property error - The request error of the current active tab returned from useSWR.
* @property mutate - The mutate function of the current active tab returned from useSWR.
* @property pagination - The pagination data of the current active tab. It contains the current page and page size.
* @property paginationRecords - Returns the global pagination records of the first party and third party applications.
* This is used to keep track of the pagination when switching between tabs by passing the page records to the tab navigation.
* @property updatePagination - The function to update the pagination of the current active tab.
* @property showThirdPartyApplicationTab - The flag to show the third party application tab. Hide the tab if there is no third party applications.
*/
/**
* This hook is used to keep track of the first party and third party applications data with pagination.
*
* @param isThirdParty
* @returns {ApplicationData}
*/
const useApplicationsData = (isThirdParty = false) => {
const [{ page }, updateSearchParameters] = useSearchParametersWatcher({
page: 1,
});
const [firstPartyApplicationPage, setFirstPartyApplicationPage] = useState(
isThirdParty ? 1 : page
);
const [thirdPartyApplicationPage, setThirdPartyApplicationPage] = useState(
isThirdParty ? page : 1
);
const updatePagination = useCallback(
(page: number) => {
updateSearchParameters({ page });
},
[updateSearchParameters]
);
useEffect(() => {
// Update the pagination records based on the current active tab.
if (isThirdParty) {
setThirdPartyApplicationPage(page);
} else {
setFirstPartyApplicationPage(page);
}
}, [page, isThirdParty]);
const firstPartyApplicationsFetchUrl = useMemo(
() =>
buildUrl(applicationsEndpoint, {
page: String(firstPartyApplicationPage),
page_size: String(pageSize),
}),
[firstPartyApplicationPage]
);
const thirdPartyApplicationsFetchUrl = useMemo(
() =>
buildUrl(applicationsEndpoint, {
page: String(thirdPartyApplicationPage),
page_size: String(pageSize),
isThirdParty: 'true',
}),
[thirdPartyApplicationPage]
);
const firstPartyApplicationsData = useSWR<[Application[], number], RequestError>(
firstPartyApplicationsFetchUrl
);
const thirdPartyApplicationsData = useSWR<[Application[], number], RequestError>(
thirdPartyApplicationsFetchUrl
);
const { data } = thirdPartyApplicationsData;
const [_, totalCount] = data ?? [];
const hasThirdPartyApplications = totalCount && totalCount > 0;
return {
...(isThirdParty ? thirdPartyApplicationsData : firstPartyApplicationsData),
pagination: {
page: isThirdParty ? thirdPartyApplicationPage : firstPartyApplicationPage,
pageSize,
},
paginationRecords: {
firstPartyApplicationPage,
thirdPartyApplicationPage,
},
showThirdPartyApplicationTab: hasThirdPartyApplications,
updatePagination,
};
};
export default useApplicationsData;

View file

@ -0,0 +1,60 @@
import { type Application } from '@logto/schemas';
import { useCallback, useMemo } from 'react';
import useSWR from 'swr';
import { defaultPageSize } from '@/consts';
import { type RequestError } from '@/hooks/use-api';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import { buildUrl } from '@/utils/url';
const pageSize = defaultPageSize;
const applicationsEndpoint = 'api/applications';
/**
* This hook is the forked version of useApplicationsData. @see {@link (./use-application-data.ts)}
* But have all the third party application related request and code removed.
* This will be applied directly on the current application page.
* To prevent the third party api request and code from being triggered.
*
* We use the isDevFeatureEnabled to determine if we should use this legacy hook or the new one.
* This hook will be removed once we have the third-party application feature ready for production.
*/
const useLegacyApplicationsData = () => {
const [{ page }, updateSearchParameters] = useSearchParametersWatcher({
page: 1,
});
const updatePagination = useCallback(
(page: number) => {
updateSearchParameters({ page });
},
[updateSearchParameters]
);
const url = useMemo(
() =>
buildUrl(applicationsEndpoint, {
page: String(page),
page_size: String(pageSize),
}),
[page]
);
const data = useSWR<[Application[], number], RequestError>(url);
return {
...data,
pagination: {
page,
pageSize,
},
paginationRecords: {
firstPartyApplicationPage: page,
thirdPartyApplicationPage: page,
},
showThirdPartyApplicationTab: false,
updatePagination,
};
};
export default useLegacyApplicationsData;

View file

@ -12,6 +12,10 @@
width: 360px;
}
.tabs {
margin-top: _.unit(4);
}
.guideLibraryContainer {
flex: 1;
overflow-y: auto;

View file

@ -1,24 +1,21 @@
import { withAppInsights } from '@logto/app-insights/react';
import type { Application } from '@logto/schemas';
import { joinPath } from '@silverhand/essentials';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import useSWR from 'swr';
import Plus from '@/assets/icons/plus.svg';
import ApplicationIcon from '@/components/ApplicationIcon';
import ChargeNotification from '@/components/ChargeNotification';
import ItemPreview from '@/components/ItemPreview';
import PageMeta from '@/components/PageMeta';
import { defaultPageSize } from '@/consts';
import { isCloud } from '@/consts/env';
import { isDevFeaturesEnabled, isCloud } from '@/consts/env';
import Button from '@/ds-components/Button';
import CardTitle from '@/ds-components/CardTitle';
import CopyToClipboard from '@/ds-components/CopyToClipboard';
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
import TabNav, { TabNavItem } from '@/ds-components/TabNav';
import Table from '@/ds-components/Table';
import type { RequestError } from '@/hooks/use-api';
import useApplicationsUsage from '@/hooks/use-applications-usage';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import useTenantPathname from '@/hooks/use-tenant-pathname';
import * as pageLayout from '@/scss/page-layout.module.scss';
import { applicationTypeI18nKey } from '@/types/applications';
@ -26,29 +23,53 @@ import { buildUrl } from '@/utils/url';
import GuideLibrary from './components/GuideLibrary';
import GuideLibraryModal from './components/GuideLibraryModal';
import useApplicationsData from './hooks/use-application-data';
import useLegacyApplicationsData from './hooks/use-legacy-application-data';
import * as styles from './index.module.scss';
const pageSize = defaultPageSize;
const tabs = Object.freeze({
thirdPartyApplications: 'third-party-applications',
});
const applicationsPathname = '/applications';
const createApplicationPathname = `${applicationsPathname}/create`;
const buildDetailsPathname = (id: string) => `${applicationsPathname}/${id}`;
function Applications() {
// Build the path with pagination query param for the tabs
const buildTabPathWithPagePagination = (page: number, tab?: keyof typeof tabs) => {
const pathname = tab
? joinPath(applicationsPathname, tabs.thirdPartyApplications)
: applicationsPathname;
return page > 1 ? buildUrl(pathname, { page: String(page) }) : pathname;
};
// @simeng-li FIXME: Remove this when the third party applications is production ready
const useApplicationDataHook = isDevFeaturesEnabled
? useApplicationsData
: useLegacyApplicationsData;
type Props = {
tab?: keyof typeof tabs;
};
function Applications({ tab }: Props) {
const { search } = useLocation();
const { match, navigate } = useTenantPathname();
const isCreating = match(createApplicationPathname);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const isCreating = match(createApplicationPathname);
const { hasMachineToMachineAppsSurpassedLimit } = useApplicationsUsage();
const [{ page }, updateSearchParameters] = useSearchParametersWatcher({
page: 1,
});
const url = buildUrl('api/applications', {
page: String(page),
page_size: String(pageSize),
});
const { data, error, mutate } = useSWR<[Application[], number], RequestError>(url);
const {
data,
error,
mutate,
pagination,
updatePagination,
paginationRecords,
showThirdPartyApplicationTab,
} = useApplicationDataHook(tab === 'thirdPartyApplications');
const isLoading = !data && !error;
const [applications, totalCount] = data ?? [];
@ -81,6 +102,27 @@ function Applications() {
checkedFlagKey="machineToMachineApp"
/>
)}
{showThirdPartyApplicationTab && (
<TabNav className={styles.tabs}>
<TabNavItem
href={buildTabPathWithPagePagination(paginationRecords.firstPartyApplicationPage)}
isActive={!tab}
>
{t('applications.tab.my_applications')}
</TabNavItem>
<TabNavItem
href={buildTabPathWithPagePagination(
paginationRecords.thirdPartyApplicationPage,
'thirdPartyApplications'
)}
isActive={tab === 'thirdPartyApplications'}
>
{t('applications.tab.third_party_applications')}
</TabNavItem>
</TabNav>
)}
{!isLoading && !applications?.length && (
<OverlayScrollbar className={styles.guideLibraryContainer}>
<CardTitle
@ -103,10 +145,14 @@ function Applications() {
title: t('applications.application_name'),
dataIndex: 'name',
colSpan: 6,
render: ({ id, name, type }) => (
render: ({ id, name, type, isThirdParty }) => (
<ItemPreview
title={name}
subtitle={t(`${applicationTypeI18nKey[type]}.title`)}
subtitle={
isThirdParty
? t('applications.type.third_party.title')
: t(`${applicationTypeI18nKey[type]}.title`)
}
icon={<ApplicationIcon className={styles.icon} type={type} />}
to={buildDetailsPathname(id)}
/>
@ -123,12 +169,9 @@ function Applications() {
navigate(buildDetailsPathname(id));
}}
pagination={{
page,
...pagination,
totalCount,
pageSize,
onChange: (page) => {
updateSearchParameters({ page });
},
onChange: updatePagination,
}}
onRetry={async () => mutate(undefined, true)}
/>
@ -136,10 +179,7 @@ function Applications() {
<GuideLibraryModal
isOpen={isCreating}
onClose={() => {
navigate({
pathname: applicationsPathname,
search,
});
navigate(-1);
}}
/>
</div>

View file

@ -12,6 +12,12 @@ const applications = {
select_application_type: 'Wähle einen Anwendungstyp',
no_application_type_selected: 'Du hast noch keinen Anwendungstyp ausgewählt',
application_created: 'Die Anwendung wurde erfolgreich erstellt.',
tab: {
/** UNTRANSLATED */
my_applications: 'My apps',
/** UNTRANSLATED */
third_party_applications: 'Third party apps',
},
app_id: 'App ID',
type: {
native: {
@ -35,6 +41,14 @@ const applications = {
subtitle: 'Eine Anwendung (normalerweise ein Dienst), die direkt mit Ressourcen kommuniziert',
description: 'z.B. Backend Dienst',
},
third_party: {
/** UNTRANSLATED */
title: 'Third-party app',
/** UNTRANSLATED */
subtitle: 'An app that is used as a third-party IdP connector',
/** UNTRANSLATED */
description: 'E.g., OIDC, SAML',
},
},
placeholder_title: 'Wähle einen Anwendungstyp, um fortzufahren',
placeholder_description:

View file

@ -12,6 +12,10 @@ const applications = {
select_application_type: 'Select an application type',
no_application_type_selected: 'You havent selected any application type yet',
application_created: 'Application created successfully.',
tab: {
my_applications: 'My apps',
third_party_applications: 'Third party apps',
},
app_id: 'App ID',
type: {
native: {
@ -34,6 +38,11 @@ const applications = {
subtitle: 'An app (usually a service) that directly talks to resources',
description: 'E.g., Backend service',
},
third_party: {
title: 'Third-party app',
subtitle: 'An app that is used as a third-party IdP connector',
description: 'E.g., OIDC, SAML',
},
},
placeholder_title: 'Select an application type to continue',
placeholder_description:

View file

@ -12,6 +12,12 @@ const applications = {
select_application_type: 'Seleccionar un tipo de aplicación',
no_application_type_selected: 'Aún no has seleccionado ningún tipo de aplicación',
application_created: '¡La aplicación se ha creado correctamente.',
tab: {
/** UNTRANSLATED */
my_applications: 'My apps',
/** UNTRANSLATED */
third_party_applications: 'Third party apps',
},
app_id: 'App ID',
type: {
native: {
@ -35,6 +41,14 @@ const applications = {
subtitle: 'Una aplicación (generalmente un servicio) que habla directamente con recursos',
description: 'Por ejemplo, servicio backend',
},
third_party: {
/** UNTRANSLATED */
title: 'Third-party app',
/** UNTRANSLATED */
subtitle: 'An app that is used as a third-party IdP connector',
/** UNTRANSLATED */
description: 'E.g., OIDC, SAML',
},
},
placeholder_title: 'Selecciona un tipo de aplicación para continuar',
placeholder_description:

View file

@ -12,6 +12,12 @@ const applications = {
select_application_type: "Sélectionner un type d'application",
no_application_type_selected: "Vous n'avez pas encore sélectionné de type d'application",
application_created: "L'application a été créée avec succès.",
tab: {
/** UNTRANSLATED */
my_applications: 'My apps',
/** UNTRANSLATED */
third_party_applications: 'Third party apps',
},
app_id: 'App ID',
type: {
native: {
@ -36,6 +42,14 @@ const applications = {
'Une application (généralement un service) qui communique directement avec les ressources',
description: 'Par exemple, un service backend',
},
third_party: {
/** UNTRANSLATED */
title: 'Third-party app',
/** UNTRANSLATED */
subtitle: 'An app that is used as a third-party IdP connector',
/** UNTRANSLATED */
description: 'E.g., OIDC, SAML',
},
},
placeholder_title: "Sélectionnez un type d'application pour continuer",
placeholder_description:

View file

@ -12,6 +12,12 @@ const applications = {
select_application_type: 'Seleziona un tipo di applicazione',
no_application_type_selected: 'Non hai ancora selezionato alcun tipo di applicazione',
application_created: "L'applicazione è stata creata con successo.",
tab: {
/** UNTRANSLATED */
my_applications: 'My apps',
/** UNTRANSLATED */
third_party_applications: 'Third party apps',
},
app_id: 'App ID',
type: {
native: {
@ -35,6 +41,14 @@ const applications = {
subtitle: "Un'app (solitamente un servizio) che comunica direttamente con le risorse",
description: 'E.g., servizio backend',
},
third_party: {
/** UNTRANSLATED */
title: 'Third-party app',
/** UNTRANSLATED */
subtitle: 'An app that is used as a third-party IdP connector',
/** UNTRANSLATED */
description: 'E.g., OIDC, SAML',
},
},
placeholder_title: 'Seleziona un tipo di applicazione per continuare',
placeholder_description:

View file

@ -12,6 +12,12 @@ const applications = {
select_application_type: 'アプリケーションタイプを選択してください',
no_application_type_selected: 'まだアプリケーションタイプが選択されていません',
application_created: 'アプリケーションが正常に作成されました。',
tab: {
/** UNTRANSLATED */
my_applications: 'My apps',
/** UNTRANSLATED */
third_party_applications: 'Third party apps',
},
app_id: 'App ID',
type: {
native: {
@ -34,6 +40,14 @@ const applications = {
subtitle: 'リソースに直接アクセスするアプリケーション(通常はサービス)',
description: '例:バックエンドサービス',
},
third_party: {
/** UNTRANSLATED */
title: 'Third-party app',
/** UNTRANSLATED */
subtitle: 'An app that is used as a third-party IdP connector',
/** UNTRANSLATED */
description: 'E.g., OIDC, SAML',
},
},
placeholder_title: '続行するにはアプリケーションタイプを選択してください',
placeholder_description:

View file

@ -12,6 +12,12 @@ const applications = {
select_application_type: '어플리케이션 종류 선택',
no_application_type_selected: '어플리케이션 종류를 선택하지 않았어요.',
application_created: '어플리케이션이 성공적으로 생성되었어요.',
tab: {
/** UNTRANSLATED */
my_applications: 'My apps',
/** UNTRANSLATED */
third_party_applications: 'Third party apps',
},
app_id: 'App ID',
type: {
native: {
@ -34,6 +40,14 @@ const applications = {
subtitle: '직접 리소스에 접근하는 엡(서비스)',
description: '예) 백엔드 서비스',
},
third_party: {
/** UNTRANSLATED */
title: 'Third-party app',
/** UNTRANSLATED */
subtitle: 'An app that is used as a third-party IdP connector',
/** UNTRANSLATED */
description: 'E.g., OIDC, SAML',
},
},
placeholder_title: '애플리케이션 유형을 선택하여 계속하세요',
placeholder_description:

View file

@ -12,6 +12,12 @@ const applications = {
select_application_type: 'Wybierz typ aplikacji',
no_application_type_selected: 'Nie wybrałeś jeszcze żadnego typu aplikacji',
application_created: 'Aplikacja ostała pomyślnie utworzona.',
tab: {
/** UNTRANSLATED */
my_applications: 'My apps',
/** UNTRANSLATED */
third_party_applications: 'Third party apps',
},
app_id: 'App ID',
type: {
native: {
@ -35,6 +41,14 @@ const applications = {
subtitle: 'Aplikacja (zazwyczaj usługa), która bezpośrednio komunikuje się z zasobami',
description: 'Na przykład usługa backendowa',
},
third_party: {
/** UNTRANSLATED */
title: 'Third-party app',
/** UNTRANSLATED */
subtitle: 'An app that is used as a third-party IdP connector',
/** UNTRANSLATED */
description: 'E.g., OIDC, SAML',
},
},
placeholder_title: 'Wybierz typ aplikacji, aby kontynuować',
placeholder_description:

View file

@ -12,6 +12,12 @@ const applications = {
select_application_type: 'Selecione um tipo de aplicativo',
no_application_type_selected: 'Você ainda não selecionou nenhum tipo de aplicativo',
application_created: 'O aplicativo foi criado com sucesso.',
tab: {
/** UNTRANSLATED */
my_applications: 'My apps',
/** UNTRANSLATED */
third_party_applications: 'Third party apps',
},
app_id: 'App ID',
type: {
native: {
@ -35,6 +41,14 @@ const applications = {
subtitle: 'Um aplicativo (geralmente um serviço) que fala diretamente com os recursos',
description: 'Ex: serviço de backend',
},
third_party: {
/** UNTRANSLATED */
title: 'Third-party app',
/** UNTRANSLATED */
subtitle: 'An app that is used as a third-party IdP connector',
/** UNTRANSLATED */
description: 'E.g., OIDC, SAML',
},
},
placeholder_title: 'Selecione um tipo de aplicativo para continuar',
placeholder_description:

View file

@ -12,6 +12,12 @@ const applications = {
select_application_type: 'Selecione o tipo de aplicação',
no_application_type_selected: 'Ainda não selecionou nenhum tipo de aplicação',
application_created: 'A aplicação foi criada com sucesso.',
tab: {
/** UNTRANSLATED */
my_applications: 'My apps',
/** UNTRANSLATED */
third_party_applications: 'Third party apps',
},
app_id: 'App ID',
type: {
native: {
@ -34,6 +40,14 @@ const applications = {
subtitle: 'Uma aplicação (normalmente um serviço) que se comunica diretamente com recursos',
description: 'Ex., serviço back-end',
},
third_party: {
/** UNTRANSLATED */
title: 'Third-party app',
/** UNTRANSLATED */
subtitle: 'An app that is used as a third-party IdP connector',
/** UNTRANSLATED */
description: 'E.g., OIDC, SAML',
},
},
placeholder_title: 'Selecione um tipo de aplicação para continuar',
placeholder_description:

View file

@ -12,6 +12,12 @@ const applications = {
select_application_type: 'Выбрать тип приложения',
no_application_type_selected: 'Вы еще не выбрали тип приложения',
application_created: 'Приложение успешно создано.',
tab: {
/** UNTRANSLATED */
my_applications: 'My apps',
/** UNTRANSLATED */
third_party_applications: 'Third party apps',
},
app_id: 'App ID',
type: {
native: {
@ -34,6 +40,14 @@ const applications = {
subtitle: 'Приложение (обычно сервис), которое напрямую общается с ресурсами',
description: 'Например, backend-сервис',
},
third_party: {
/** UNTRANSLATED */
title: 'Third-party app',
/** UNTRANSLATED */
subtitle: 'An app that is used as a third-party IdP connector',
/** UNTRANSLATED */
description: 'E.g., OIDC, SAML',
},
},
placeholder_title: 'Выберите тип приложения, чтобы продолжить',
placeholder_description:

View file

@ -12,6 +12,12 @@ const applications = {
select_application_type: 'Uygulama tipi seçiniz',
no_application_type_selected: 'Henüz bir uygulama tipi seçmediniz',
application_created: 'Uygulaması başarıyla oluşturuldu.',
tab: {
/** UNTRANSLATED */
my_applications: 'My apps',
/** UNTRANSLATED */
third_party_applications: 'Third party apps',
},
app_id: 'App ID',
type: {
native: {
@ -35,6 +41,14 @@ const applications = {
subtitle: 'Kaynaklarla doğrudan iletişim kuran bir uygulama (genellikle bir servis)',
description: 'Örneğin, Backend servisi',
},
third_party: {
/** UNTRANSLATED */
title: 'Third-party app',
/** UNTRANSLATED */
subtitle: 'An app that is used as a third-party IdP connector',
/** UNTRANSLATED */
description: 'E.g., OIDC, SAML',
},
},
placeholder_title: 'Devam etmek için bir uygulama tipi seçin',
placeholder_description:

View file

@ -11,6 +11,12 @@ const applications = {
select_application_type: '选择应用类型',
no_application_type_selected: '你还没有选择应用类型',
application_created: '创建应用成功。',
tab: {
/** UNTRANSLATED */
my_applications: 'My apps',
/** UNTRANSLATED */
third_party_applications: 'Third party apps',
},
app_id: 'App ID',
type: {
native: {
@ -33,6 +39,14 @@ const applications = {
subtitle: '直接与资源对话的应用程序(通常是服务)',
description: '例如,后端服务',
},
third_party: {
/** UNTRANSLATED */
title: 'Third-party app',
/** UNTRANSLATED */
subtitle: 'An app that is used as a third-party IdP connector',
/** UNTRANSLATED */
description: 'E.g., OIDC, SAML',
},
},
placeholder_title: '选择应用程序类型以继续',
placeholder_description:

View file

@ -11,6 +11,12 @@ const applications = {
select_application_type: '選擇應用類型',
no_application_type_selected: '你還沒有選擇應用類型',
application_created: '應用創建成功。',
tab: {
/** UNTRANSLATED */
my_applications: 'My apps',
/** UNTRANSLATED */
third_party_applications: 'Third party apps',
},
app_id: 'App ID',
type: {
native: {
@ -33,6 +39,14 @@ const applications = {
subtitle: '直接與資源對話的應用程序(通常是服務)',
description: '例如,後端服務',
},
third_party: {
/** UNTRANSLATED */
title: 'Third-party app',
/** UNTRANSLATED */
subtitle: 'An app that is used as a third-party IdP connector',
/** UNTRANSLATED */
description: 'E.g., OIDC, SAML',
},
},
placeholder_title: '選擇應用程序類型以繼續',
placeholder_description:

View file

@ -11,6 +11,12 @@ const applications = {
select_application_type: '選擇應用類型',
no_application_type_selected: '你還沒有選擇應用類型',
application_created: '應用創建成功。',
tab: {
/** UNTRANSLATED */
my_applications: 'My apps',
/** UNTRANSLATED */
third_party_applications: 'Third party apps',
},
app_id: 'App ID',
type: {
native: {
@ -33,6 +39,14 @@ const applications = {
subtitle: '直接與資源對話的應用程序(通常是服務)',
description: '例如,後端服務',
},
third_party: {
/** UNTRANSLATED */
title: 'Third-party app',
/** UNTRANSLATED */
subtitle: 'An app that is used as a third-party IdP connector',
/** UNTRANSLATED */
description: 'E.g., OIDC, SAML',
},
},
placeholder_title: '選擇應用程序類型以繼續',
placeholder_description: