0
Fork 0
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:
Gao Sun 2023-06-13 18:16:34 +08:00 committed by GitHub
commit 298d29c39f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 84 additions and 58 deletions

View file

@ -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"

View file

@ -2,7 +2,7 @@
"extends": "@silverhand/ts-config/tsconfig.base",
"compilerOptions": {
"types": ["node", "jest"],
"declaration": false,
"declaration": true,
"outDir": "build",
"baseUrl": ".",
"paths": {

View file

@ -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"

View file

@ -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]

View 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) };
};

View file

@ -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));

View file

@ -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]);

View file

@ -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)));

View file

@ -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('?')) {

View file

@ -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,

View file

@ -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) {

View file

@ -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();
});

View file

@ -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==}