mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-24 13:58:50 +08:00
feat(core): improve byok ux (#15303)
fix #15265 #### PR Dependency Tree * **PR #15303** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal)
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import { AuthSession } from '@affine/core/modules/cloud/entities/session';
|
||||
import { AuthService } from '@affine/core/modules/cloud/services/auth';
|
||||
import { FetchService } from '@affine/core/modules/cloud/services/fetch';
|
||||
import { ServerService } from '@affine/core/modules/cloud/services/server';
|
||||
import { AuthStore } from '@affine/core/modules/cloud/stores/auth';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs/services/dialog';
|
||||
import { NbstoreService } from '@affine/core/modules/storage';
|
||||
import { UrlService } from '@affine/core/modules/url/services/url';
|
||||
import { ServerDeploymentType } from '@affine/graphql';
|
||||
import { Framework } from '@toeverything/infra';
|
||||
import { of } from 'rxjs';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
describe('AuthService native password sign-in', () => {
|
||||
test.each(['android', 'ios'])(
|
||||
'waits for the %s session bootstrap before completing',
|
||||
async () => {
|
||||
let resolveSession!: () => void;
|
||||
const revalidateOnce = vi.fn(
|
||||
() =>
|
||||
new Promise<void>(resolve => {
|
||||
resolveSession = resolve;
|
||||
})
|
||||
);
|
||||
const signInPassword = vi.fn().mockResolvedValue(undefined);
|
||||
const framework = new Framework();
|
||||
|
||||
framework.entity(
|
||||
AuthSession,
|
||||
() =>
|
||||
({
|
||||
account$: of(null),
|
||||
revalidate: vi.fn(),
|
||||
revalidateOnce,
|
||||
}) as any
|
||||
);
|
||||
framework.service(FetchService, { fetch: vi.fn() } as any);
|
||||
framework.store(AuthStore, {
|
||||
signInPassword,
|
||||
setCachedSignInUser: vi.fn(),
|
||||
} as any);
|
||||
framework.service(UrlService, { getClientScheme: () => null } as any);
|
||||
framework.service(GlobalDialogService, { open: vi.fn() } as any);
|
||||
framework.service(NbstoreService, {
|
||||
realtime: { subscribe: () => of() },
|
||||
} as any);
|
||||
framework.service(ServerService, {
|
||||
server: {
|
||||
['config$']: {
|
||||
value: {
|
||||
type: ServerDeploymentType.Selfhosted,
|
||||
version: '0.27.2',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
framework.service(AuthService, [
|
||||
FetchService,
|
||||
AuthStore,
|
||||
UrlService,
|
||||
GlobalDialogService,
|
||||
NbstoreService,
|
||||
ServerService,
|
||||
]);
|
||||
|
||||
const auth = framework.provider().get(AuthService);
|
||||
let completed = false;
|
||||
const signInPromise = auth
|
||||
.signInPassword({
|
||||
email: 'user@example.com',
|
||||
password: 'password',
|
||||
})
|
||||
.then(() => {
|
||||
completed = true;
|
||||
});
|
||||
|
||||
await vi.waitFor(() => expect(revalidateOnce).toHaveBeenCalledOnce());
|
||||
expect(completed).toBe(false);
|
||||
resolveSession();
|
||||
await signInPromise;
|
||||
|
||||
expect(signInPassword).toHaveBeenCalledOnce();
|
||||
expect(completed).toBe(true);
|
||||
}
|
||||
);
|
||||
});
|
||||
+20
-4
@@ -11,7 +11,13 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { logByokError } from './errors';
|
||||
import * as styles from './index.css';
|
||||
import { readLocalKeys, upsertLocalKey } from './local-storage';
|
||||
import { byokT, providerLabels, storageLabel } from './metadata';
|
||||
import {
|
||||
byokT,
|
||||
endpointHintKey,
|
||||
providerLabels,
|
||||
shouldShowEndpoint,
|
||||
storageLabel,
|
||||
} from './metadata';
|
||||
import type {
|
||||
ByokKey,
|
||||
ByokSettings,
|
||||
@@ -32,6 +38,7 @@ export const AddKeyModal = ({
|
||||
localStorageSupported,
|
||||
canAddServerKey,
|
||||
canAddLocalKey,
|
||||
isSelfHosted,
|
||||
gql,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
@@ -45,6 +52,7 @@ export const AddKeyModal = ({
|
||||
localStorageSupported: boolean;
|
||||
canAddServerKey: boolean;
|
||||
canAddLocalKey: boolean;
|
||||
isSelfHosted: boolean;
|
||||
gql?: GqlFn;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
@@ -61,6 +69,10 @@ export const AddKeyModal = ({
|
||||
editingKey?.storage === ByokKeyStorage.server &&
|
||||
editingKey.provider === provider;
|
||||
const canTest = !!apiKey || canTestStoredConfig;
|
||||
const endpointHint = endpointHintKey(
|
||||
settings.customEndpointSupported,
|
||||
settings.privateEndpointSupported
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
@@ -275,19 +287,23 @@ export const AddKeyModal = ({
|
||||
}}
|
||||
type="password"
|
||||
/>
|
||||
</label>
|
||||
{settings.customEndpointSupported ? (
|
||||
<label className={styles.field}>
|
||||
</label>{' '}
|
||||
{shouldShowEndpoint(isSelfHosted, settings.customEndpointSupported) ? (
|
||||
<label className={styles.endpointField}>
|
||||
<span className={styles.label}>{byokT(t, 'field.endpoint')}</span>
|
||||
<input
|
||||
className={styles.input}
|
||||
value={endpoint}
|
||||
disabled={!settings.customEndpointSupported}
|
||||
onChange={event => {
|
||||
setEndpoint(event.target.value);
|
||||
setTestResult(null);
|
||||
}}
|
||||
placeholder="https://api.example.com/v1"
|
||||
/>
|
||||
{endpointHint ? (
|
||||
<span className={styles.fieldHint}>{byokT(t, endpointHint)}</span>
|
||||
) : null}
|
||||
</label>
|
||||
) : null}
|
||||
<div className={styles.modalActions}>
|
||||
|
||||
@@ -186,11 +186,19 @@ export const field = style({
|
||||
height: '3em',
|
||||
});
|
||||
|
||||
export const endpointField = style([field, { height: 'auto' }]);
|
||||
|
||||
export const label = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
color: cssVarV2('text/secondary'),
|
||||
});
|
||||
|
||||
export const fieldHint = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '18px',
|
||||
color: cssVarV2('text/secondary'),
|
||||
});
|
||||
|
||||
export const input = style({
|
||||
height: 32,
|
||||
width: '100%',
|
||||
|
||||
+6
@@ -47,6 +47,10 @@ const ByokKeyTestStatus = vi.hoisted(() => ({
|
||||
passed: 'passed',
|
||||
failed: 'failed',
|
||||
}));
|
||||
const ServerDeploymentType = vi.hoisted(() => ({
|
||||
Affine: 'Affine',
|
||||
Selfhosted: 'Selfhosted',
|
||||
}));
|
||||
|
||||
const workspaceByokSettingsQuery = vi.hoisted(() =>
|
||||
Symbol('workspaceByokSettingsQuery')
|
||||
@@ -130,6 +134,7 @@ vi.mock('@affine/graphql', () => ({
|
||||
ByokKeyStorage,
|
||||
ByokKeyTestStatus,
|
||||
ByokProvider,
|
||||
ServerDeploymentType,
|
||||
clearWorkspaceByokConfigsMutation,
|
||||
deleteWorkspaceByokConfigMutation,
|
||||
testWorkspaceByokConfigMutation,
|
||||
@@ -220,6 +225,7 @@ vi.mock('@toeverything/infra', async importOriginal => {
|
||||
if (token === WorkspaceServerServiceToken) {
|
||||
return {
|
||||
server: {
|
||||
['config$']: { value: { type: ServerDeploymentType.Affine } },
|
||||
gql: gqlMock,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
clearWorkspaceByokConfigsMutation as clearByokMutation,
|
||||
deleteWorkspaceByokConfigMutation as deleteByokMutation,
|
||||
type GraphQLQuery,
|
||||
ServerDeploymentType,
|
||||
workspaceByokSettingsQuery as byokSettingsQuery,
|
||||
} from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -114,6 +115,9 @@ export const WorkspaceByokSetting = () => {
|
||||
(settings?.localEntitled ?? false) &&
|
||||
(settings?.localStorageSupported ?? false);
|
||||
const canManageKeys = canAddServerKey || canAddLocalKey;
|
||||
const isSelfHosted =
|
||||
workspaceServer.server?.config$.value.type ===
|
||||
ServerDeploymentType.Selfhosted;
|
||||
|
||||
const clearAll = useCallback(async () => {
|
||||
if (!settings) {
|
||||
@@ -356,6 +360,7 @@ export const WorkspaceByokSetting = () => {
|
||||
localStorageSupported={settings.localStorageSupported}
|
||||
canAddServerKey={canAddServerKey}
|
||||
canAddLocalKey={canAddLocalKey}
|
||||
isSelfHosted={isSelfHosted}
|
||||
gql={workspaceServer.server?.gql as GqlFn | undefined}
|
||||
/>
|
||||
</>
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { endpointHintKey, shouldShowEndpoint } from './metadata';
|
||||
|
||||
describe('endpointHintKey', () => {
|
||||
test.each([
|
||||
[false, false, 'endpoint.custom-disabled'],
|
||||
[true, false, 'endpoint.private-disabled'],
|
||||
[true, true, null],
|
||||
] as const)(
|
||||
'maps custom=%s private=%s to %s',
|
||||
(customEndpointSupported, privateEndpointSupported, expected) => {
|
||||
expect(
|
||||
endpointHintKey(customEndpointSupported, privateEndpointSupported)
|
||||
).toBe(expected);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('shouldShowEndpoint', () => {
|
||||
test.each([
|
||||
[true, false, true],
|
||||
[true, true, true],
|
||||
[false, true, true],
|
||||
[false, false, false],
|
||||
])(
|
||||
'self-hosted %s with custom endpoint support %s returns %s',
|
||||
(isSelfHosted, customEndpointSupported, expected) => {
|
||||
expect(shouldShowEndpoint(isSelfHosted, customEndpointSupported)).toBe(
|
||||
expected
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -24,6 +24,26 @@ export function storageLabel(t: I18nInstance, storage: ByokStorage) {
|
||||
: byokT(t, 'storage.server');
|
||||
}
|
||||
|
||||
export function endpointHintKey(
|
||||
customEndpointSupported: boolean,
|
||||
privateEndpointSupported: boolean
|
||||
) {
|
||||
if (!customEndpointSupported) {
|
||||
return 'endpoint.custom-disabled';
|
||||
}
|
||||
if (!privateEndpointSupported) {
|
||||
return 'endpoint.private-disabled';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function shouldShowEndpoint(
|
||||
isSelfHosted: boolean,
|
||||
customEndpointSupported: boolean
|
||||
) {
|
||||
return isSelfHosted || customEndpointSupported;
|
||||
}
|
||||
|
||||
export function capabilitiesFor(provider: ByokProvider, storage: ByokStorage) {
|
||||
switch (provider) {
|
||||
case ByokProvider.openai:
|
||||
|
||||
@@ -53,6 +53,7 @@ export type ByokSettings = {
|
||||
allowedProviders: ByokProvider[];
|
||||
localStorageSupported: boolean;
|
||||
customEndpointSupported: boolean;
|
||||
privateEndpointSupported: boolean;
|
||||
hasAiPlan: boolean;
|
||||
warnings: Array<{
|
||||
featureKind: string;
|
||||
|
||||
Reference in New Issue
Block a user