refactor: login method (#1676)

This commit is contained in:
Himself65 2023-03-23 16:29:29 -05:00 committed by GitHub
parent a415e4aa5c
commit 56acb2bdeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 626 additions and 61 deletions

View File

@ -44,6 +44,7 @@
"devDependencies": {
"@perfsee/webpack": "^1.5.0",
"@redux-devtools/extension": "^3.2.5",
"@rich-data/viewer": "^2.2.4",
"@swc-jotai/debug-label": "^0.0.9",
"@swc-jotai/react-refresh": "^0.0.7",
"@types/react": "^18.0.28",

View File

@ -4,7 +4,7 @@ import { messages } from '@affine/datacenter';
import type React from 'react';
import { memo, useEffect, useState } from 'react';
import { useOnGoogleLogout } from '../../../hooks/use-on-google-logout';
import { useAffineLogOut } from '../../../hooks/affine/use-affine-log-out';
import { apis } from '../../../shared/apis';
declare global {
@ -17,7 +17,7 @@ declare global {
export const MessageCenter: React.FC = memo(function MessageCenter() {
const [popup, setPopup] = useState(false);
const onLogout = useOnGoogleLogout();
const onLogout = useAffineLogOut();
useEffect(() => {
const listener = (
event: CustomEvent<{

View File

@ -0,0 +1,26 @@
import { toast } from '@affine/component';
import {
createAffineAuth,
setLoginStorage,
SignMethod,
} from '@affine/workspace/affine/login';
import { useRouter } from 'next/router';
import { useCallback } from 'react';
import { apis } from '../../shared/apis';
export const affineAuth = createAffineAuth();
export function useAffineLogIn() {
const router = useRouter();
return useCallback(async () => {
const response = await affineAuth.generateToken(SignMethod.Google);
if (response) {
setLoginStorage(response);
apis.auth.setLogin(response);
router.reload();
} else {
toast('Login failed');
}
}, [router]);
}

View File

@ -1,13 +1,14 @@
import { clearLoginStorage } from '@affine/workspace/affine/login';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { useSetAtom } from 'jotai';
import { useRouter } from 'next/router';
import { useCallback } from 'react';
import { jotaiWorkspacesAtom } from '../atoms';
import { WorkspacePlugins } from '../plugins';
import { apis } from '../shared/apis';
import { jotaiWorkspacesAtom } from '../../atoms';
import { WorkspacePlugins } from '../../plugins';
import { apis } from '../../shared/apis';
export function useOnGoogleLogout() {
export function useAffineLogOut() {
const set = useSetAtom(jotaiWorkspacesAtom);
const router = useRouter();
return useCallback(() => {
@ -18,6 +19,7 @@ export function useOnGoogleLogout() {
)
);
WorkspacePlugins[WorkspaceFlavour.AFFINE].cleanup?.();
clearLoginStorage();
router.reload();
}, [router, set]);
}

View File

@ -0,0 +1,36 @@
import { DebugLogger } from '@affine/debug';
import {
getLoginStorage,
isExpired,
parseIdToken,
setLoginStorage,
} from '@affine/workspace/affine/login';
import useSWR from 'swr';
import { apis } from '../../shared/apis';
import { affineAuth } from './use-affine-log-in';
const logger = new DebugLogger('auth-token');
const revalidate = async () => {
const storage = getLoginStorage();
if (storage) {
const tokenMessage = parseIdToken(storage.token);
logger.debug('revalidate affine user');
if (isExpired(tokenMessage)) {
logger.debug('need to refresh token');
const response = await affineAuth.refreshToken(storage);
if (response) {
setLoginStorage(response);
apis.auth.setLogin(response);
}
}
}
return true;
};
export function useAffineRefreshAuthToken() {
useSWR('autoRefreshToken', {
fetcher: revalidate,
});
}

View File

@ -19,6 +19,7 @@ import {
import { HelpIsland } from '../components/pure/help-island';
import { PageLoading } from '../components/pure/loading';
import WorkSpaceSliderBar from '../components/pure/workspace-slider-bar';
import { useAffineRefreshAuthToken } from '../hooks/affine/use-affine-refresh-auth-token';
import { useCurrentPageId } from '../hooks/current/use-current-page-id';
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
import { useBlockSuiteWorkspaceHelper } from '../hooks/use-blocksuite-workspace-helper';
@ -92,6 +93,11 @@ export const WorkspaceLayout: React.FC<React.PropsWithChildren> =
);
};
function AffineWorkspaceEffect() {
useAffineRefreshAuthToken();
return null;
}
export const WorkspaceLayoutInner: React.FC<React.PropsWithChildren> = ({
children,
}) => {
@ -196,6 +202,7 @@ export const WorkspaceLayoutInner: React.FC<React.PropsWithChildren> = ({
paths={isPublicWorkspace ? publicPathGenerator : pathGenerator}
/>
<StyledWrapper>
<AffineWorkspaceEffect />
{children}
<StyledToolWrapper>
{/* fixme(himself65): remove this */}

View File

@ -67,39 +67,6 @@ const App = function App({
>
<Head>
<title>AFFiNE</title>
<meta
name="viewport"
content="initial-scale=1, width=device-width"
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="https://app.affine.pro/" />
<meta
name="twitter:title"
content="AFFiNEThere can be more than Notion and Miro."
/>
<meta
name="twitter:description"
content="There can be more than Notion and Miro. AFFiNE is a next-gen knowledge base that brings planning, sorting and creating all together."
/>
<meta name="twitter:site" content="@AffineOfficial" />
<meta
name="twitter:image"
content="https://affine.pro/og.jpeg"
/>
<meta
property="og:title"
content="AFFiNEThere can be more than Notion and Miro."
/>
<meta property="og:type" content="website" />
<meta
property="og:description"
content="There can be more than Notion and Miro. AFFiNE is a next-gen knowledge base that brings planning, sorting and creating all together."
/>
<meta property="og:url" content="https://app.affine.pro/" />
<meta
property="og:image"
content="https://affine.pro/og.jpeg"
/>
</Head>
{getLayout(<Component {...pageProps} />)}
</ProviderComposer>

View File

@ -11,8 +11,16 @@ import {
} from '@affine/workspace/affine/login';
import { useAtom } from 'jotai';
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import { useMemo } from 'react';
const Viewer = dynamic(
() => import('@rich-data/viewer').then(m => ({ default: m.JsonViewer })),
{ ssr: false }
);
import { useTheme } from 'next-themes';
import { StyledPage, StyledWrapper } from '../../layouts/styles';
const LoginDevPage: NextPage = () => {
@ -69,7 +77,24 @@ const LoginDevPage: NextPage = () => {
>
Reset Storage
</Button>
{user && JSON.stringify(user)}
<Button
onClick={async () => {
const status = await fetch('/api/workspace', {
method: 'GET',
headers: {
'Cache-Control': 'no-cache',
Authorization: getLoginStorage()?.token ?? '',
},
}).then(r => r.status);
toast(`Response Status: ${status}`);
}}
>
Check Permission
</Button>
<Viewer
theme={useTheme().resolvedTheme === 'light' ? 'light' : 'dark'}
value={user}
/>
</StyledWrapper>
</StyledPage>
);

View File

@ -51,6 +51,30 @@ export default class AppDocument extends Document<{
/>
<link rel="icon" sizes="192x192" href="/chrome-192x192.png" />
<meta name="emotion-insertion-point" content="" />
<meta name="viewport" content="initial-scale=1, width=device-width" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="https://app.affine.pro/" />
<meta
name="twitter:title"
content="AFFiNEThere can be more than Notion and Miro."
/>
<meta
name="twitter:description"
content="There can be more than Notion and Miro. AFFiNE is a next-gen knowledge base that brings planning, sorting and creating all together."
/>
<meta name="twitter:site" content="@AffineOfficial" />
<meta name="twitter:image" content="https://affine.pro/og.jpeg" />
<meta
property="og:title"
content="AFFiNEThere can be more than Notion and Miro."
/>
<meta property="og:type" content="website" />
<meta
property="og:description"
content="There can be more than Notion and Miro. AFFiNE is a next-gen knowledge base that brings planning, sorting and creating all together."
/>
<meta property="og:url" content="https://app.affine.pro/" />
<meta property="og:image" content="https://affine.pro/og.jpeg" />
{this.props.emotionStyleTags}
</Head>
<body>

View File

@ -9,13 +9,13 @@ import {
openCreateWorkspaceModalAtom,
openWorkspacesModalAtom,
} from '../atoms';
import { useAffineLogIn } from '../hooks/affine/use-affine-log-in';
import { useAffineLogOut } from '../hooks/affine/use-affine-log-out';
import { useCurrentUser } from '../hooks/current/use-current-user';
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
import { useOnGoogleLogout } from '../hooks/use-on-google-logout';
import { useRouterHelper } from '../hooks/use-router-helper';
import { useWorkspaces, useWorkspacesHelper } from '../hooks/use-workspaces';
import { WorkspaceSubPath } from '../shared';
import { apis } from '../shared/apis';
const WorkspaceListModal = dynamic(
async () =>
@ -69,12 +69,8 @@ export function Modals() {
},
[jumpToSubPath, setCurrentWorkspace, setOpenWorkspacesModal]
)}
onClickLogin={useCallback(() => {
apis.signInWithGoogle().then(() => {
router.reload();
});
}, [router])}
onClickLogout={useOnGoogleLogout()}
onClickLogin={useAffineLogIn()}
onClickLogout={useAffineLogOut()}
onCreateWorkspace={useCallback(() => {
setOpenCreateWorkspaceModal(true);
}, [setOpenCreateWorkspaceModal])}

View File

@ -5,6 +5,10 @@ import {
GoogleAuth,
} from '@affine/datacenter';
import { config } from '@affine/env';
import {
createUserApis,
createWorkspaceApis,
} from '@affine/workspace/affine/api';
import { isValidIPAddress } from '../utils/is-valid-ip-address';
@ -27,6 +31,22 @@ if (typeof window === 'undefined') {
params.get('prefixUrl') && (prefixUrl = params.get('prefixUrl') as string);
}
declare global {
// eslint-disable-next-line no-var
var affineApis:
| undefined
| (ReturnType<typeof createUserApis> &
ReturnType<typeof createWorkspaceApis>);
}
const affineApis = {} as ReturnType<typeof createUserApis> &
ReturnType<typeof createWorkspaceApis>;
Object.assign(affineApis, createUserApis(prefixUrl));
Object.assign(affineApis, createWorkspaceApis(prefixUrl));
if (!globalThis.affineApis) {
globalThis.affineApis = affineApis;
}
const bareAuth = createBareClient(prefixUrl);
const googleAuth = new GoogleAuth(bareAuth);
export const clientAuth = createAuthClient(bareAuth, googleAuth);

View File

@ -6,7 +6,8 @@
"license": "MPL-2.0",
"workspaces": [
"apps/*",
"packages/*"
"packages/*",
"tests/fixtures"
],
"scripts": {
"dev": "dev-web",

View File

@ -15,6 +15,7 @@
"dependencies": {
"@affine/debug": "workspace:*",
"@blocksuite/blocks": "0.5.0-20230323085636-3110abb",
"@blocksuite/global": "0.5.0-20230323085636-3110abb",
"@blocksuite/store": "0.5.0-20230323085636-3110abb",
"@tauri-apps/api": "^1.2.0",
"encoding": "^0.1.13",

View File

@ -4,9 +4,11 @@
"exports": {
"./utils": "./src/utils.ts",
"./type": "./src/type.ts",
"./affine/*": "./src/affine/*.ts"
"./affine/*": "./src/affine/*.ts",
"./affine/api": "./src/affine/api/index.ts"
},
"dependencies": {
"@affine-test/fixtures": "workspace:^",
"@affine/component": "workspace:*",
"@affine/debug": "workspace:*",
"@affine/env": "workspace:*",
@ -17,6 +19,7 @@
"js-base64": "^3.7.5",
"ky": "^0.33.3",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"zod": "^3.21.4"
}
}

View File

@ -0,0 +1,86 @@
/**
* @vitest-environment happy-dom
*/
import 'fake-indexeddb/auto';
import userA from '@affine-test/fixtures/userA.json';
import { Workspace } from '@blocksuite/store';
import { beforeAll, beforeEach, describe, expect, test } from 'vitest';
import { createWorkspaceApis, createWorkspaceResponseSchema } from '../api';
import { loginResponseSchema, setLoginStorage } from '../login';
let workspaceApis: ReturnType<typeof createWorkspaceApis>;
beforeAll(() => {
workspaceApis = createWorkspaceApis('http://localhost:3000/');
});
beforeEach(async () => {
let data;
// first step: try to log in
const response = await fetch('http://localhost:3000/api/user/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'DebugLoginUser',
email: userA.email,
password: userA.password,
}),
});
if (!response.ok) {
data = await fetch('http://localhost:3000/api/user/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'DebugCreateUser',
...userA,
}),
}).then(r => r.json());
setLoginStorage(data);
} else {
setLoginStorage((data = await response.json()));
}
loginResponseSchema.parse(data);
});
describe('api', () => {
test(
'create workspace',
async () => {
const workspace = new Workspace({
id: 'test',
});
const binary = Workspace.Y.encodeStateAsUpdate(workspace.doc);
const data = await workspaceApis.createWorkspace(new Blob([binary]));
createWorkspaceResponseSchema.parse(data);
},
{
timeout: 30000,
}
);
test(
'delete workspace',
async () => {
const workspace = new Workspace({
id: 'test',
});
const binary = Workspace.Y.encodeStateAsUpdate(workspace.doc);
const data = await workspaceApis.createWorkspace(new Blob([binary]));
createWorkspaceResponseSchema.parse(data);
const id = data.id;
const response = await workspaceApis.deleteWorkspace({
id,
});
expect(response).toBe(true);
},
{
timeout: 30000,
}
);
});

View File

@ -0,0 +1,293 @@
import { assertExists } from '@blocksuite/global/utils';
import { z } from 'zod';
import { getLoginStorage } from '../login';
export interface User {
id: string;
name: string;
email: string;
avatar_url: string;
create_at: string;
}
export interface GetUserByEmailParams {
email: string;
workspace_id: string;
}
export function createUserApis(prefixUrl = '/') {
return {
getUserByEmail: async (
params: GetUserByEmailParams
): Promise<User[] | null> => {
const auth = getLoginStorage();
assertExists(auth);
const target = new URL(prefixUrl + 'api/user', window.location.origin);
target.searchParams.append('email', params.email);
target.searchParams.append('workspace_id', params.workspace_id);
return fetch(target, {
method: 'GET',
headers: {
Authorization: auth.token,
},
}).then(r => r.json());
},
} as const;
}
export interface GetWorkspaceDetailParams {
id: string;
}
export enum WorkspaceType {
Private = 0,
Normal = 1,
}
export enum PermissionType {
Read = 0,
Write = 1,
Admin = 10,
Owner = 99,
}
export interface Workspace {
id: string;
type: WorkspaceType;
public: boolean;
permission: PermissionType;
}
export interface WorkspaceDetail extends Workspace {
owner: User;
member_count: number;
}
export interface Permission {
id: string;
type: PermissionType;
workspace_id: string;
user_id: string;
user_email: string;
accepted: boolean;
create_at: number;
}
export interface RegisteredUser extends User {
type: 'Registered';
}
export interface UnregisteredUser {
type: 'Unregistered';
email: string;
}
export interface Member extends Permission {
user: RegisteredUser | UnregisteredUser;
}
export interface GetWorkspaceMembersParams {
id: string;
}
export interface CreateWorkspaceParams {
name: string;
}
export interface UpdateWorkspaceParams {
id: string;
public: boolean;
}
export interface DeleteWorkspaceParams {
id: string;
}
export interface InviteMemberParams {
id: string;
email: string;
}
export interface RemoveMemberParams {
permissionId: number;
}
export interface AcceptInvitingParams {
invitingCode: string;
}
export interface LeaveWorkspaceParams {
id: number | string;
}
export const createWorkspaceResponseSchema = z.object({
id: z.string(),
public: z.boolean(),
type: z.nativeEnum(WorkspaceType),
created_at: z.number(),
});
export function createWorkspaceApis(prefixUrl = '/') {
return {
getWorkspaces: async (): Promise<Workspace[]> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + 'api/workspace', {
method: 'GET',
headers: {
Authorization: auth.token,
'Cache-Control': 'no-cache',
},
}).then(r => r.json());
},
getWorkspaceDetail: async (
params: GetWorkspaceDetailParams
): Promise<WorkspaceDetail | null> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${params.id}`, {
method: 'GET',
headers: {
Authorization: auth.token,
},
}).then(r => r.json());
},
getWorkspaceMembers: async (
params: GetWorkspaceDetailParams
): Promise<Member[]> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${params.id}/permission`, {
method: 'GET',
headers: {
Authorization: auth.token,
},
}).then(r => r.json());
},
createWorkspace: async (encodedYDoc: Blob): Promise<{ id: string }> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + 'api/workspace', {
method: 'POST',
body: encodedYDoc,
headers: {
Authorization: auth.token,
},
}).then(r => r.json());
},
updateWorkspace: async (
params: UpdateWorkspaceParams
): Promise<{ public: boolean | null }> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${params.id}`, {
method: 'POST',
body: JSON.stringify({
public: params.public,
}),
headers: {
'Content-Type': 'application/json',
Authorization: auth.token,
},
}).then(r => r.json());
},
deleteWorkspace: async (
params: DeleteWorkspaceParams
): Promise<boolean> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${params.id}`, {
method: 'DELETE',
headers: {
Authorization: auth.token,
},
}).then(r => r.ok);
},
/**
* Notice: Only support normal(contrast to private) workspace.
*/
inviteMember: async (params: InviteMemberParams): Promise<void> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${params.id}/permission`, {
method: 'POST',
body: JSON.stringify({
email: params.email,
}),
headers: {
'Content-Type': 'application/json',
Authorization: auth.token,
},
}).then(r => r.json());
},
removeMember: async (params: RemoveMemberParams): Promise<void> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + `api/permission/${params.permissionId}`, {
method: 'DELETE',
headers: {
Authorization: auth.token,
},
}).then(r => r.json());
},
acceptInviting: async (
params: AcceptInvitingParams
): Promise<Permission> => {
return fetch(prefixUrl + `api/invitation/${params.invitingCode}`, {
method: 'POST',
}).then(r => r.json());
},
uploadBlob: async (params: { blob: Blob }): Promise<string> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + 'api/blob', {
method: 'PUT',
body: params.blob,
headers: {
Authorization: auth.token,
},
}).then(r => r.text());
},
getBlob: async (params: { blobId: string }): Promise<ArrayBuffer> => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + `api/blob/${params.blobId}`, {
method: 'GET',
headers: {
Authorization: auth.token,
},
}).then(r => r.arrayBuffer());
},
leaveWorkspace: async ({ id }: LeaveWorkspaceParams) => {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${id}/permission`, {
method: 'DELETE',
headers: {
Authorization: auth.token,
},
}).then(r => r.json());
},
downloadWorkspace: async (
workspaceId: string,
published = false
): Promise<ArrayBuffer> => {
if (published) {
return fetch(prefixUrl + `api/public/doc/${workspaceId}`, {
method: 'GET',
}).then(r => r.arrayBuffer());
} else {
const auth = getLoginStorage();
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${workspaceId}/doc`, {
method: 'GET',
headers: {
Authorization: auth.token,
},
}).then(r => r.arrayBuffer());
}
},
} as const;
}

View File

@ -1,4 +1,7 @@
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
export const currentAffineUserAtom = atom<AccessTokenMessage | null>(null);
export const currentAffineUserAtom = atomWithStorage<AccessTokenMessage | null>(
'affine-user-atom',
null
);

View File

@ -10,6 +10,7 @@ import {
signInWithPopup,
} from 'firebase/auth';
import { decode } from 'js-base64';
import { z } from 'zod';
// Connect emulators based on env vars
const envConnectEmulators = process.env.REACT_APP_FIREBASE_EMULATORS === 'true';
@ -27,12 +28,12 @@ export type LoginParams = {
token: string;
};
export type LoginResponse = {
// access token, expires in a very short time
token: string;
// Refresh token
refresh: string;
};
export const loginResponseSchema = z.object({
token: z.string(),
refresh: z.string(),
});
export type LoginResponse = z.infer<typeof loginResponseSchema>;
const logger = new DebugLogger('token');

6
tests/fixtures/package.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"name": "@affine-test/fixtures",
"exports": {
"./*": "./*"
}
}

4
tests/fixtures/tsconfig.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["./src"]
}

View File

@ -28,7 +28,8 @@
"@affine/env": ["./packages/env"],
"@affine/env/*": ["./packages/env/src/*"],
"@affine/utils": ["./packages/utils"],
"@affine/workspace/*": ["./packages/workspace/src/*"]
"@affine/workspace/*": ["./packages/workspace/src/*"],
"@affine-test/fixtures/*": ["./tests/fixtures/*"]
}
},
"references": [
@ -58,6 +59,9 @@
},
{
"path": "./packages/workspace"
},
{
"path": "./tests/fixtures"
}
],
"files": [],

View File

@ -12,6 +12,12 @@ __metadata:
languageName: node
linkType: hard
"@affine-test/fixtures@workspace:^, @affine-test/fixtures@workspace:tests/fixtures":
version: 0.0.0-use.local
resolution: "@affine-test/fixtures@workspace:tests/fixtures"
languageName: unknown
linkType: soft
"@affine/app@workspace:apps/web":
version: 0.0.0-use.local
resolution: "@affine/app@workspace:apps/web"
@ -34,6 +40,7 @@ __metadata:
"@mui/material": ^5.11.13
"@perfsee/webpack": ^1.5.0
"@redux-devtools/extension": ^3.2.5
"@rich-data/viewer": ^2.2.4
"@swc-jotai/debug-label": ^0.0.9
"@swc-jotai/react-refresh": ^0.0.7
"@types/react": ^18.0.28
@ -166,6 +173,7 @@ __metadata:
dependencies:
"@affine/debug": "workspace:*"
"@blocksuite/blocks": 0.5.0-20230323085636-3110abb
"@blocksuite/global": 0.5.0-20230323085636-3110abb
"@blocksuite/store": 0.5.0-20230323085636-3110abb
"@tauri-apps/api": ^1.2.0
encoding: ^0.1.13
@ -256,6 +264,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@affine/workspace@workspace:packages/workspace"
dependencies:
"@affine-test/fixtures": "workspace:^"
"@affine/component": "workspace:*"
"@affine/debug": "workspace:*"
"@affine/env": "workspace:*"
@ -267,6 +276,7 @@ __metadata:
ky: ^0.33.3
react: ^18.2.0
react-dom: ^18.2.0
zod: ^3.21.4
languageName: unknown
linkType: soft
@ -4685,6 +4695,22 @@ __metadata:
languageName: node
linkType: hard
"@rich-data/viewer@npm:^2.2.4":
version: 2.14.1
resolution: "@rich-data/viewer@npm:2.14.1"
dependencies:
"@mui/material": ^5.11.13
copy-to-clipboard: ^3.3.3
zustand: ^4.3.6
peerDependencies:
"@emotion/react": ^11.10
"@emotion/styled": ^11.10
react: ^17 || ^18
react-dom: ^17 || ^18
checksum: a3fa1748eff116d5377ed2a3ecd24db3f6b0147d0663df66791ad9f065126ee77d2cd8b1158570be546b8735ae6db64f39081ad8f6dacda6c47455230aaefe54
languageName: node
linkType: hard
"@rollup/pluginutils@npm:^4.2.0":
version: 4.2.1
resolution: "@rollup/pluginutils@npm:4.2.1"
@ -9340,6 +9366,15 @@ __metadata:
languageName: node
linkType: hard
"copy-to-clipboard@npm:^3.3.3":
version: 3.3.3
resolution: "copy-to-clipboard@npm:3.3.3"
dependencies:
toggle-selection: ^1.0.6
checksum: e0a325e39b7615108e6c1c8ac110ae7b829cdc4ee3278b1df6a0e4228c490442cc86444cd643e2da344fbc424b3aab8909e2fec82f8bc75e7e5b190b7c24eecf
languageName: node
linkType: hard
"core-js-compat@npm:^3.25.1":
version: 3.29.1
resolution: "core-js-compat@npm:3.29.1"
@ -19499,6 +19534,13 @@ __metadata:
languageName: node
linkType: hard
"toggle-selection@npm:^1.0.6":
version: 1.0.6
resolution: "toggle-selection@npm:1.0.6"
checksum: a90dc80ed1e7b18db8f4e16e86a5574f87632dc729cfc07d9ea3ced50021ad42bb4e08f22c0913e0b98e3837b0b717e0a51613c65f30418e21eb99da6556a74c
languageName: node
linkType: hard
"toidentifier@npm:1.0.1":
version: 1.0.1
resolution: "toidentifier@npm:1.0.1"
@ -20077,7 +20119,7 @@ __metadata:
languageName: node
linkType: hard
"use-sync-external-store@npm:^1.2.0":
"use-sync-external-store@npm:1.2.0, use-sync-external-store@npm:^1.2.0":
version: 1.2.0
resolution: "use-sync-external-store@npm:1.2.0"
peerDependencies:
@ -21016,6 +21058,23 @@ __metadata:
languageName: node
linkType: hard
"zustand@npm:^4.3.6":
version: 4.3.6
resolution: "zustand@npm:4.3.6"
dependencies:
use-sync-external-store: 1.2.0
peerDependencies:
immer: ">=9.0"
react: ">=16.8"
peerDependenciesMeta:
immer:
optional: true
react:
optional: true
checksum: 4d3cec03526f04ff3de6dc45b6f038c47f091836af9660fbf5f682cae1628221102882df20e4048dfe699a43f67424e5d6afc1116f3838a80eea5dd4f95ddaed
languageName: node
linkType: hard
"zx@npm:^7.2.1":
version: 7.2.1
resolution: "zx@npm:7.2.1"