mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
Merge pull request #4026 from logto-io/gao-upgrade-withtyped
refactor(console): use withtyped client for SWR
This commit is contained in:
commit
298d29c39f
13 changed files with 84 additions and 58 deletions
|
@ -7,6 +7,9 @@
|
|||
"license": "Elastic-2.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"exports": {
|
||||
"./routes": "./build/routes/index.js"
|
||||
},
|
||||
"imports": {
|
||||
"#src/*": "./build/*"
|
||||
},
|
||||
|
@ -18,6 +21,7 @@
|
|||
"lint:report": "pnpm lint --format json --output-file report.json",
|
||||
"dev": "rm -rf build/ && nodemon",
|
||||
"start": "NODE_ENV=production node .",
|
||||
"prepack": "pnpm build",
|
||||
"test:only": "NODE_OPTIONS=\"--experimental-vm-modules --max_old_space_size=4096\" jest --logHeapUsage",
|
||||
"test": "pnpm build:test && pnpm test:only",
|
||||
"test:ci": "pnpm test:only --coverage --silent"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"extends": "@silverhand/ts-config/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jest"],
|
||||
"declaration": false,
|
||||
"declaration": true,
|
||||
"outDir": "build",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
"@fontsource/roboto-mono": "^5.0.0",
|
||||
"@jest/types": "^29.5.0",
|
||||
"@logto/app-insights": "workspace:^1.2.0",
|
||||
"@logto/cloud": "workspace:^",
|
||||
"@logto/connector-kit": "workspace:^1.1.1",
|
||||
"@logto/core-kit": "workspace:^2.0.0",
|
||||
"@logto/language-kit": "workspace:^1.0.0",
|
||||
|
@ -60,6 +61,7 @@
|
|||
"@types/react-helmet": "^6.1.6",
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"@types/react-syntax-highlighter": "^15.5.1",
|
||||
"@withtyped/client": "^0.7.10",
|
||||
"buffer": "^5.7.1",
|
||||
"classnames": "^2.3.1",
|
||||
"clean-deep": "^3.4.0",
|
||||
|
@ -145,13 +147,16 @@
|
|||
],
|
||||
"import/no-unused-modules": [
|
||||
"error",
|
||||
{ "unusedExports": true }
|
||||
{
|
||||
"unusedExports": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.d.ts", "**/mdx-components/*/index.tsx"
|
||||
"*.d.ts",
|
||||
"**/mdx-components/*/index.tsx"
|
||||
],
|
||||
"rules": {
|
||||
"import/no-unused-modules": "off"
|
||||
|
|
|
@ -1,23 +1,20 @@
|
|||
import type router from '@logto/cloud/routes';
|
||||
import { useLogto } from '@logto/react';
|
||||
import ky from 'ky';
|
||||
import Client from '@withtyped/client';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { cloudApi } from '@/consts';
|
||||
|
||||
export const useCloudApi = () => {
|
||||
export const useCloudApi = (): Client<typeof router> => {
|
||||
const { isAuthenticated, getAccessToken } = useLogto();
|
||||
const api = useMemo(
|
||||
() =>
|
||||
ky.create({
|
||||
hooks: {
|
||||
beforeRequest: [
|
||||
async (request) => {
|
||||
if (isAuthenticated) {
|
||||
const accessToken = await getAccessToken(cloudApi.indicator);
|
||||
request.headers.set('Authorization', `Bearer ${accessToken ?? ''}`);
|
||||
}
|
||||
},
|
||||
],
|
||||
new Client<typeof router>({
|
||||
baseUrl: window.location.origin,
|
||||
headers: async () => {
|
||||
if (isAuthenticated) {
|
||||
return { Authorization: `Bearer ${(await getAccessToken(cloudApi.indicator)) ?? ''}` };
|
||||
}
|
||||
},
|
||||
}),
|
||||
[getAccessToken, isAuthenticated]
|
||||
|
|
23
packages/console/src/cloud/hooks/use-cloud-swr.ts
Normal file
23
packages/console/src/cloud/hooks/use-cloud-swr.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import type router from '@logto/cloud/routes';
|
||||
import { type RouterRoutes } from '@withtyped/client';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { useCloudApi } from './use-cloud-api';
|
||||
|
||||
type GetRoutes = RouterRoutes<typeof router>['get'];
|
||||
|
||||
const normalizeError = (error: unknown) => {
|
||||
if (error === undefined || error === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
return error instanceof Error ? error : new Error(String(error));
|
||||
};
|
||||
|
||||
export const useCloudSwr = <Key extends keyof GetRoutes>(key: Key) => {
|
||||
const cloudApi = useCloudApi();
|
||||
const response = useSWR(key, async () => cloudApi.get(key));
|
||||
|
||||
// By default, `useSWR()` uses `any` for the error type which is unexpected under our lint rule set.
|
||||
return { ...response, error: normalizeError(response.error) };
|
||||
};
|
|
@ -57,9 +57,8 @@ function CreateTenantModal({ isOpen, onClose }: Props) {
|
|||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
const { name, tag } = data;
|
||||
const newTenant = await cloudApi
|
||||
.post('/api/tenants', { json: { name, tag } })
|
||||
.json<TenantInfo>();
|
||||
const newTenant = await cloudApi.post('/api/tenants', { body: { name, tag } });
|
||||
|
||||
onClose(newTenant);
|
||||
} catch (error: unknown) {
|
||||
toast.error(error instanceof Error ? error.message : String(error));
|
||||
|
|
|
@ -24,9 +24,7 @@ function Tenants({ data, onAdd }: Props) {
|
|||
* `name` and `tag` are required for POST /tenants API, add fixed value to avoid throwing error.
|
||||
* This page page will be removed in upcoming changes on multi-tenancy cloud console.
|
||||
*/
|
||||
await api
|
||||
.post('api/tenants', { json: { name: 'My Project', tag: TenantTag.Development } })
|
||||
.json<TenantInfo>()
|
||||
await api.post('/api/tenants', { body: { name: 'My Project', tag: TenantTag.Development } })
|
||||
);
|
||||
}, [api, onAdd]);
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { useLogto } from '@logto/react';
|
||||
import type { TenantInfo } from '@logto/schemas/models';
|
||||
import { conditional, yes } from '@silverhand/essentials';
|
||||
import { HTTPError } from 'ky';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
|
@ -25,7 +24,7 @@ function Protected() {
|
|||
setError(undefined);
|
||||
|
||||
try {
|
||||
const data = await api.get('/api/tenants').json<TenantInfo[]>();
|
||||
const data = await api.get('/api/tenants');
|
||||
setTenants(data);
|
||||
} catch (error: unknown) {
|
||||
setError(error instanceof Error ? error : new Error(String(error)));
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { HTTPError, type Options } from 'ky';
|
||||
import { HTTPError } from 'ky';
|
||||
import type ky from 'ky';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { BareFetcher } from 'swr';
|
||||
import type { Fetcher } from 'swr';
|
||||
|
||||
import { RequestError } from './use-api';
|
||||
|
||||
|
@ -10,18 +10,18 @@ type KyInstance = typeof ky;
|
|||
|
||||
type WithTotalNumber<T> = Array<Awaited<T> | number>;
|
||||
|
||||
type useSwrFetcherHook = {
|
||||
<T>(api: KyInstance): BareFetcher<T>;
|
||||
<T extends unknown[]>(api: KyInstance): BareFetcher<WithTotalNumber<T>>;
|
||||
type UseSwrFetcherHook = {
|
||||
<T>(api: KyInstance): Fetcher<T>;
|
||||
<T extends unknown[]>(api: KyInstance): Fetcher<WithTotalNumber<T>>;
|
||||
};
|
||||
|
||||
const useSwrFetcher: useSwrFetcherHook = <T>(api: KyInstance) => {
|
||||
const useSwrFetcher: UseSwrFetcherHook = <T>(api: KyInstance) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const fetcher = useCallback<BareFetcher<T | WithTotalNumber<T>>>(
|
||||
async (resource: string | URL, init: Options) => {
|
||||
const fetcher = useCallback<Fetcher<T | WithTotalNumber<T>>>(
|
||||
async (resource: string) => {
|
||||
try {
|
||||
const response = await api.get(resource, init);
|
||||
const response = await api.get(resource);
|
||||
const data = await response.json<T>();
|
||||
|
||||
if (typeof resource === 'string' && resource.includes('?')) {
|
||||
|
|
|
@ -1,19 +1,13 @@
|
|||
import { useLogto } from '@logto/react';
|
||||
import { type TenantInfo } from '@logto/schemas/models';
|
||||
import { type Optional, trySafe } from '@silverhand/essentials';
|
||||
import type ky from 'ky';
|
||||
import { useCallback, useContext, useEffect, useMemo } from 'react';
|
||||
import useSWR, { type KeyedMutator } from 'swr';
|
||||
import { type KeyedMutator } from 'swr';
|
||||
|
||||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import { useCloudSwr } from '@/cloud/hooks/use-cloud-swr';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
|
||||
import useSwrFetcher from './use-swr-fetcher';
|
||||
|
||||
type KyInstance = typeof ky;
|
||||
|
||||
type TenantsHook = {
|
||||
api: KyInstance;
|
||||
currentTenant?: TenantInfo;
|
||||
currentTenantId: string;
|
||||
error?: Error;
|
||||
|
@ -26,16 +20,9 @@ type TenantsHook = {
|
|||
};
|
||||
|
||||
const useTenants = (): TenantsHook => {
|
||||
const cloudApi = useCloudApi();
|
||||
const { signIn, getAccessToken } = useLogto();
|
||||
const { currentTenantId, setCurrentTenantId, isSettle, setIsSettle } = useContext(TenantsContext);
|
||||
|
||||
const fetcher = useSwrFetcher<TenantInfo[]>(cloudApi);
|
||||
const {
|
||||
data: availableTenants,
|
||||
error,
|
||||
mutate,
|
||||
} = useSWR<TenantInfo[], Error>('/api/tenants', fetcher);
|
||||
const { data: availableTenants, error, mutate } = useCloudSwr('/api/tenants');
|
||||
const isLoading = !availableTenants && !error;
|
||||
const isLoaded = Boolean(availableTenants && !error);
|
||||
|
||||
|
@ -62,7 +49,6 @@ const useTenants = (): TenantsHook => {
|
|||
}, [currentTenant, validate]);
|
||||
|
||||
return {
|
||||
api: cloudApi,
|
||||
currentTenant,
|
||||
currentTenantId,
|
||||
error,
|
||||
|
|
|
@ -3,6 +3,7 @@ import classNames from 'classnames';
|
|||
import { useEffect, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import AppError from '@/components/AppError';
|
||||
import AppLoading from '@/components/AppLoading';
|
||||
import PageMeta from '@/components/PageMeta';
|
||||
|
@ -23,8 +24,8 @@ const tenantProfileToForm = (tenant?: TenantInfo): TenantSettingsForm => {
|
|||
};
|
||||
|
||||
function TenantBasicSettings() {
|
||||
const api = useCloudApi();
|
||||
const {
|
||||
api: cloudApi,
|
||||
currentTenant,
|
||||
currentTenantId,
|
||||
error: requestError,
|
||||
|
@ -58,11 +59,10 @@ function TenantBasicSettings() {
|
|||
|
||||
const saveData = async (data: { name?: string; tag?: TenantTag }) => {
|
||||
try {
|
||||
const { name, tag } = await cloudApi
|
||||
.patch(`/api/tenants/${currentTenantId}`, {
|
||||
json: data,
|
||||
})
|
||||
.json<TenantInfo>();
|
||||
const { name, tag } = await api.patch(`/api/tenants/:tenantId`, {
|
||||
params: { tenantId: currentTenantId },
|
||||
body: data,
|
||||
});
|
||||
reset({ profile: { name, tag } });
|
||||
void mutate();
|
||||
} catch (error: unknown) {
|
||||
|
@ -96,7 +96,7 @@ function TenantBasicSettings() {
|
|||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await cloudApi.delete(`/api/tenants/${currentTenantId}`);
|
||||
await api.delete(`/api/tenants/:tenantId`, { params: { tenantId: currentTenantId } });
|
||||
setIsDeletionModalOpen(false);
|
||||
await mutate();
|
||||
if (tenants?.[0]?.id) {
|
||||
|
|
|
@ -171,6 +171,8 @@ describe('smoke testing for cloud', () => {
|
|||
await expect(page).toClick('button[name=submit]');
|
||||
await page.waitForNavigation({ waitUntil: 'networkidle0' });
|
||||
|
||||
console.log('???', page.url());
|
||||
|
||||
expect(page.url().startsWith(logtoCloudUrl.href)).toBeTruthy();
|
||||
expect(new URL(page.url()).pathname.endsWith('/onboarding/welcome')).toBeTruthy();
|
||||
});
|
||||
|
|
|
@ -2775,6 +2775,9 @@ importers:
|
|||
'@logto/app-insights':
|
||||
specifier: workspace:^1.2.0
|
||||
version: link:../app-insights
|
||||
'@logto/cloud':
|
||||
specifier: workspace:^
|
||||
version: link:../cloud
|
||||
'@logto/connector-kit':
|
||||
specifier: workspace:^1.1.1
|
||||
version: link:../toolkit/connector-kit
|
||||
|
@ -2877,6 +2880,9 @@ importers:
|
|||
'@types/react-syntax-highlighter':
|
||||
specifier: ^15.5.1
|
||||
version: 15.5.1
|
||||
'@withtyped/client':
|
||||
specifier: ^0.7.10
|
||||
version: 0.7.10(zod@3.20.2)
|
||||
buffer:
|
||||
specifier: ^5.7.1
|
||||
version: 5.7.1
|
||||
|
@ -9808,6 +9814,15 @@ packages:
|
|||
eslint-visitor-keys: 3.3.0
|
||||
dev: true
|
||||
|
||||
/@withtyped/client@0.7.10(zod@3.20.2):
|
||||
resolution: {integrity: sha512-8EndTFr1+V6WbAW1kldxPZztk4uIbmoBnfWKG1zvVC6TTPwEOiB3SP3BsjiW8si7wUmO8GWjazKqnq5CYlSjGg==}
|
||||
dependencies:
|
||||
'@withtyped/server': 0.11.1(zod@3.20.2)
|
||||
'@withtyped/shared': 0.2.0
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
dev: true
|
||||
|
||||
/@withtyped/postgres@0.11.0(@withtyped/server@0.11.1):
|
||||
resolution: {integrity: sha512-PHnx6ake/MDdyy4sZXS/7l5XNBtjqlPSqSHrlmCYUXYxUV0sHSrXECKxX7deAvWZtcHVh9VaWEpiQBhFS06Vig==}
|
||||
peerDependencies:
|
||||
|
@ -9829,11 +9844,9 @@ packages:
|
|||
'@silverhand/essentials': 2.6.2
|
||||
'@withtyped/shared': 0.2.0
|
||||
zod: 3.20.2
|
||||
dev: false
|
||||
|
||||
/@withtyped/shared@0.2.0:
|
||||
resolution: {integrity: sha512-SADIVEospfIWAVK0LxX7F1T04hsWMZ0NkfR3lNfvJqOktJ52GglI3FOTVYOM1NJYReDT6pR0XFlCfaF8TVPt8w==}
|
||||
dev: false
|
||||
|
||||
/@xmldom/xmldom@0.8.7:
|
||||
resolution: {integrity: sha512-sI1Ly2cODlWStkINzqGrZ8K6n+MTSbAeQnAipGyL+KZCXuHaRlj2gyyy8B/9MvsFFqN7XHryQnB2QwhzvJXovg==}
|
||||
|
|
Loading…
Reference in a new issue