mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-12 16:15:14 +00:00
Bring the connectors UI in line with DESIGN.md
From a design review against frontend/DESIGN.md: - Drawer panels use the subtle surface; ToolPermissions takes the surface it sits on (outline in the wizard, subtle in the drawer). - The OAuth sign-in is the wizard's footer action with a Cancel (useConnectorAuth), and sign-in buttons show the service's own glyph instead of a hard-coded Drive logo. Add Tool's links moved into its ModalActions footer. - Failed saves, disconnects and connector creates say so in a toast; the admin page reports save errors as a toast too. - Capability badges are neutral; status hues are left for states. - Row titles use SectionHeader actions, setup settings a DescriptionList, empty lists EmptyState, retry and row buttons their pill roles. - The drawer names its close button and describes itself with SheetDescription; counts are formatted for the app language. - One ConnectorSetupNotice for the drawer and Upload; warning Alerts use TriangleAlert; its link keeps the Alert colour (Approved exceptions). - Tools and Sources tiles show the same "via <service>" meta line; the paused badge explains itself in a Tooltip. - On phones the category filter is a Select, empty categories are not offered, and a composer's capability filter says so with Show all. - The admin page uses PageToolbar and SectionHeader descriptions, and is English throughout.
This commit is contained in:
1 parent
a564dea401
commit
ca8fb94777
28 files changed
+649
-382
No files matched your search
@@ -1588,6 +1588,7 @@ list stays reviewable.
|
||||
| `settings/PersonalAccessTokens.tsx` | `shadcn/no-restyle` | Token scope chips are identifiers, so the `neutral` Badge is set in mono (`font-mono`). One disable. |
|
||||
| `agents/workflow/WorkflowPreview.tsx` | `shadcn/no-restyle` | A step's state changes (keys and values the app serialised) are `neutral` Badges set in mono (`font-mono`), like token scopes. One disable. |
|
||||
| `settings/traces/TraceChips.tsx` | `shadcn/no-restyle` | Trace stat chips (durations, counts) use tabular figures so they don't jitter between rows (`tabular-nums` on Badge). One disable. |
|
||||
| `connectors/ConnectorSetupNotice.tsx` | `shadcn/no-restyle` | The setup-guide link inside the needs-setup warning Alert keeps the Alert's status colour (`text-current` on `link inline`). One disable. |
|
||||
| `modals/MCPServerModal.tsx` | `shadcn/no-restyle` | The authorization link inside the test-result Alert keeps the Alert's status colour (`text-current` on `link inline`). One disable. |
|
||||
| `components/MermaidRenderer.tsx` | `shadcn/no-restyle` | The zoom readout between − and + is a `link inline` Button on the `bg-black/70` overlay; it keeps the overlay's white 12px regular text (`text-xs font-normal text-current`). One disable, beside the two zoom-button ones above. |
|
||||
| `conversation/SharedConversation.tsx` | `shadcn/no-restyle` | The "DocsGPT" link sits in the `/share/:id` page's regular-weight byline (`font-normal`). One disable. |
|
||||
|
||||
@@ -16,6 +16,9 @@ vi.mock('../api/services/connectorsService', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
import actionToastReducer, {
|
||||
selectActionToast,
|
||||
} from '../notifications/actionToastSlice';
|
||||
import { prefSlice } from '../preferences/preferenceSlice';
|
||||
import Connectors from './Connectors';
|
||||
|
||||
@@ -79,10 +82,17 @@ describe('Admin Connectors', () => {
|
||||
container.remove();
|
||||
});
|
||||
|
||||
const render = async () => {
|
||||
const store = configureStore({
|
||||
reducer: { preference: prefSlice.reducer },
|
||||
let store: ReturnType<typeof makeStore>;
|
||||
const makeStore = () =>
|
||||
configureStore({
|
||||
reducer: {
|
||||
preference: prefSlice.reducer,
|
||||
actionToast: actionToastReducer,
|
||||
},
|
||||
});
|
||||
|
||||
const render = async () => {
|
||||
store = makeStore();
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<Provider store={store}>
|
||||
@@ -111,7 +121,9 @@ describe('Admin Connectors', () => {
|
||||
it('warns when credentials use the default encryption key', async () => {
|
||||
getAdmin.mockResolvedValue(payload({ default_encryption_key: true }));
|
||||
await render();
|
||||
expect(container.textContent).toContain('admin.connectors.defaultKey');
|
||||
expect(container.textContent).toContain(
|
||||
'Set ENCRYPTION_SECRET_KEY before connecting services.',
|
||||
);
|
||||
expect(container.textContent).toContain('docsgpt connectors reencrypt');
|
||||
});
|
||||
|
||||
@@ -138,7 +150,7 @@ describe('Admin Connectors', () => {
|
||||
).toBe('false');
|
||||
});
|
||||
|
||||
it('shows an error and keeps the page when a save fails', async () => {
|
||||
it('reports a failed save in a toast and keeps the page', async () => {
|
||||
getAdmin.mockResolvedValue(payload());
|
||||
updateAdmin.mockResolvedValue({ success: false });
|
||||
await render();
|
||||
@@ -146,7 +158,10 @@ describe('Admin Connectors', () => {
|
||||
container.querySelector<HTMLButtonElement>('#allow-custom-mcp')!.click(),
|
||||
);
|
||||
expect(updateAdmin).toHaveBeenCalledWith({ allow_custom_mcp: false }, null);
|
||||
expect(container.textContent).toContain('Could not save the change.');
|
||||
expect(selectActionToast(store.getState())).toMatchObject({
|
||||
variant: 'destructive',
|
||||
message: 'Could not save the change.',
|
||||
});
|
||||
expect(container.querySelectorAll('tbody tr')).toHaveLength(2);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { ExternalLink, TriangleAlert } from 'lucide-react';
|
||||
import { ExternalLink, Info, TriangleAlert } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import connectorsService from '../api/services/connectorsService';
|
||||
import CopyButton from '../components/CopyButton';
|
||||
import PageToolbar from '../components/PageToolbar';
|
||||
import { Alert, AlertDescription, AlertTitle } from '../components/ui/alert';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card } from '../components/ui/card';
|
||||
import {
|
||||
DescriptionItem,
|
||||
DescriptionList,
|
||||
} from '../components/ui/description-list';
|
||||
import { LoadingState } from '../components/ui/loading-state';
|
||||
import { Modal } from '../components/ui/modal';
|
||||
import { SectionHeader } from '../components/ui/section-header';
|
||||
@@ -31,6 +35,7 @@ import {
|
||||
TableRow,
|
||||
} from '../components/ui/table';
|
||||
import ConnectorIcon from '../connectors/ConnectorIcon';
|
||||
import { showActionToast } from '../notifications/actionToastSlice';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import { LoadError, fmtNumber } from './AdminUI';
|
||||
|
||||
@@ -105,23 +110,22 @@ function SetupGuide({
|
||||
</section>
|
||||
<section className="flex flex-col gap-2">
|
||||
<SectionHeader as="h3" size="xs" title="Server settings" />
|
||||
<ul className="flex flex-col gap-2">
|
||||
<DescriptionList layout="justified" size="xs">
|
||||
{connector.required_settings.map((setting) => (
|
||||
<li
|
||||
<DescriptionItem
|
||||
key={setting.name}
|
||||
className="flex items-center justify-between gap-3"
|
||||
label={<code className="font-mono">{setting.name}</code>}
|
||||
>
|
||||
<code className="font-mono text-xs">{setting.name}</code>
|
||||
<Badge variant={setting.set ? 'success' : 'warning'}>
|
||||
{setting.set ? 'Set' : 'Missing'}
|
||||
</Badge>
|
||||
</li>
|
||||
</DescriptionItem>
|
||||
))}
|
||||
</ul>
|
||||
</DescriptionList>
|
||||
</section>
|
||||
{connector.key === 'google_drive' && (
|
||||
<Alert variant="info" role="note">
|
||||
<TriangleAlert />
|
||||
<Info />
|
||||
<AlertDescription>
|
||||
Publish the Google OAuth app (or use an internal Workspace app).
|
||||
Apps left in Testing get refresh tokens that expire after seven
|
||||
@@ -152,11 +156,10 @@ function SetupGuide({
|
||||
* server. English only, like the rest of the admin pages.
|
||||
*/
|
||||
export default function Connectors() {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const token = useSelector(selectToken);
|
||||
const [data, setData] = useState<AdminConnectorsData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saveError, setSaveError] = useState(false);
|
||||
const [guide, setGuide] = useState<AdminConnector | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
@@ -178,13 +181,19 @@ export default function Connectors() {
|
||||
policies?: Record<string, { enabled?: boolean; credential_mode?: Policy }>;
|
||||
allow_custom_mcp?: boolean;
|
||||
}) => {
|
||||
setSaveError(false);
|
||||
const failed = () =>
|
||||
dispatch(
|
||||
showActionToast({
|
||||
variant: 'destructive',
|
||||
message: 'Could not save the change.',
|
||||
}),
|
||||
);
|
||||
try {
|
||||
const next = await connectorsService.updateAdmin(body, token);
|
||||
if (next?.success) setData(next);
|
||||
else setSaveError(true);
|
||||
else failed();
|
||||
} catch {
|
||||
setSaveError(true);
|
||||
failed();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -194,17 +203,14 @@ export default function Connectors() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Choose which connectors members can use and whose account a shared tool
|
||||
runs with. A connector that still needs server settings starts turned
|
||||
off and is hidden from members; it turns on once its settings are in
|
||||
place, unless you switch it off.
|
||||
</p>
|
||||
<PageToolbar intro="Choose which connectors members can use and whose account a shared tool runs with. A connector that still needs server settings starts turned off and is hidden from members; it turns on once its settings are in place, unless you switch it off." />
|
||||
|
||||
{data.default_encryption_key && (
|
||||
<Alert variant="destructive">
|
||||
<TriangleAlert />
|
||||
<AlertTitle>{t('admin.connectors.defaultKey')}</AlertTitle>
|
||||
<AlertTitle>
|
||||
Set ENCRYPTION_SECRET_KEY before connecting services.
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
Stored credentials are encrypted with ENCRYPTION_SECRET_KEY, which
|
||||
still has its public default value. Set your own, keep the old one
|
||||
@@ -216,12 +222,6 @@ export default function Connectors() {
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{saveError && (
|
||||
<Alert variant="destructive">
|
||||
<TriangleAlert />
|
||||
<AlertDescription>Could not save the change.</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<SettingRows>
|
||||
<SettingRow
|
||||
@@ -240,10 +240,10 @@ export default function Connectors() {
|
||||
</SettingRows>
|
||||
|
||||
<section className="flex flex-col gap-3">
|
||||
<SectionHeader title="Redirect URIs" />
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Register these with each provider's OAuth app.
|
||||
</p>
|
||||
<SectionHeader
|
||||
title="Redirect URIs"
|
||||
description="Register these with each provider's OAuth app."
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-x-4 gap-y-3 lg:grid-cols-2">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
|
||||
@@ -9,30 +9,35 @@ import { isTrustedConnectorMessage } from '../utils/connectorAuthUtils';
|
||||
import { Alert, AlertDescription } from './ui/alert';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
interface ConnectorAuthProps {
|
||||
interface ConnectorAuthOptions {
|
||||
provider: string;
|
||||
/** Reconnect this connection (the same account) rather than add one. */
|
||||
connectionId?: string;
|
||||
onSuccess: (data: { connection_id: string; user_email: string }) => void;
|
||||
onError: (error: string) => void;
|
||||
}
|
||||
|
||||
interface ConnectorAuthProps extends ConnectorAuthOptions {
|
||||
label?: string;
|
||||
/** The service's glyph on the sign-in button (e.g. a `ConnectorIcon`). */
|
||||
icon?: React.ReactNode;
|
||||
isConnected?: boolean;
|
||||
userEmail?: string;
|
||||
onDisconnect?: () => void;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
const ConnectorAuth: React.FC<ConnectorAuthProps> = ({
|
||||
/**
|
||||
* The OAuth pop-up sign-in: returns the function to call from a click. It
|
||||
* opens the pop-up inside the gesture, points it at the provider and reports
|
||||
* the connection it created (or an error) once the callback page answers.
|
||||
*/
|
||||
export function useConnectorAuth({
|
||||
provider,
|
||||
connectionId,
|
||||
onSuccess,
|
||||
onError,
|
||||
label,
|
||||
isConnected = false,
|
||||
userEmail = '',
|
||||
onDisconnect,
|
||||
errorMessage,
|
||||
}) => {
|
||||
}: ConnectorAuthOptions): () => void {
|
||||
const { t } = useTranslation();
|
||||
const token = useSelector(selectToken);
|
||||
const completedRef = useRef(false);
|
||||
@@ -209,6 +214,29 @@ const ConnectorAuth: React.FC<ConnectorAuthProps> = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
return handleAuth;
|
||||
}
|
||||
|
||||
const ConnectorAuth: React.FC<ConnectorAuthProps> = ({
|
||||
provider,
|
||||
connectionId,
|
||||
onSuccess,
|
||||
onError,
|
||||
label,
|
||||
icon,
|
||||
isConnected = false,
|
||||
userEmail = '',
|
||||
onDisconnect,
|
||||
errorMessage,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const handleAuth = useConnectorAuth({
|
||||
provider,
|
||||
connectionId,
|
||||
onSuccess,
|
||||
onError,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{errorMessage && (
|
||||
@@ -242,12 +270,7 @@ const ConnectorAuth: React.FC<ConnectorAuthProps> = ({
|
||||
</Alert>
|
||||
) : (
|
||||
<Button type="button" onClick={handleAuth} className="w-full">
|
||||
<svg className="size-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M6.28 3l5.72 10H24l-5.72-10H6.28zm11.44 0L12 13l5.72 10H24L18.28 3h-.56zM0 13l5.72 10h5.72L5.72 13H0z"
|
||||
/>
|
||||
</svg>
|
||||
{icon}
|
||||
{label}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -13,6 +13,8 @@ import { formatDateTime } from '../utils/dateTimeUtils';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import connectorsService from '../api/services/connectorsService';
|
||||
import ConnectorAuth from '../components/ConnectorAuth';
|
||||
import ConnectorIcon from '../connectors/ConnectorIcon';
|
||||
import { connectorIconKey } from '../connectors/i18n';
|
||||
import {
|
||||
loadConnectors,
|
||||
selectConnections,
|
||||
@@ -453,7 +455,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
value={activeConnectionId ?? undefined}
|
||||
onValueChange={switchAccount}
|
||||
>
|
||||
<SelectTrigger size="field" className="w-full">
|
||||
<SelectTrigger size="field" shape="pill" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -472,6 +474,9 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
|
||||
label={t('filePicker.connectTo', {
|
||||
provider: getProviderConfig(provider).displayName,
|
||||
})}
|
||||
icon={
|
||||
<ConnectorIcon icon={connectorIconKey(provider)} className="size-5" />
|
||||
}
|
||||
onSuccess={(data) => {
|
||||
setAuthError('');
|
||||
dispatch(loadConnectors({ token }));
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
selectConnections,
|
||||
} from '../connectors/connectorsSlice';
|
||||
import type { AppDispatch } from '../store';
|
||||
import ConnectorIcon from '../connectors/ConnectorIcon';
|
||||
import ConnectorAuth from './ConnectorAuth';
|
||||
import SkeletonLoader from './SkeletonLoader';
|
||||
import { Button } from './ui/button';
|
||||
@@ -239,6 +240,7 @@ const GoogleDrivePicker: React.FC<GoogleDrivePickerProps> = ({
|
||||
<ConnectorAuth
|
||||
provider="google_drive"
|
||||
label={t('modals.uploadDoc.connectors.googleDrive.connect')}
|
||||
icon={<ConnectorIcon icon="drive" className="size-5" />}
|
||||
onSuccess={(data) => {
|
||||
setAuthError('');
|
||||
dispatch(loadConnectors({ token }));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ArrowRight, Wrench } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
MultiSelectPopover,
|
||||
@@ -40,16 +41,16 @@ export default function ToolsTrigger({
|
||||
footer={
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
|
||||
<Button variant="link" size="inline" asChild>
|
||||
<a href="/settings/tools">
|
||||
<Link to="/settings/tools">
|
||||
{t('settings.tools.manageTools')}
|
||||
<ArrowRight aria-hidden="true" className="size-3" />
|
||||
</a>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="link" size="inline" asChild>
|
||||
<a href="/settings/connectors?capability=tools">
|
||||
<Link to="/settings/connectors?capability=tools">
|
||||
{t('conversation.sources.connectMore')}
|
||||
<ArrowRight aria-hidden="true" className="size-3" />
|
||||
</a>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -26,22 +26,14 @@ vi.mock('../api/services/connectorsService', () => ({ default: service }));
|
||||
// The OAuth popup is covered by ConnectorAuth's own tests; here it only has
|
||||
// to report a finished sign-in.
|
||||
vi.mock('../components/ConnectorAuth', () => ({
|
||||
default: ({
|
||||
onSuccess,
|
||||
label,
|
||||
}: {
|
||||
onSuccess: (data: { connection_id: string; user_email: string }) => void;
|
||||
label: string;
|
||||
}) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onSuccess({ connection_id: 'conn-drive', user_email: 'a@example.com' })
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
),
|
||||
useConnectorAuth:
|
||||
({
|
||||
onSuccess,
|
||||
}: {
|
||||
onSuccess: (data: { connection_id: string; user_email: string }) => void;
|
||||
}) =>
|
||||
() =>
|
||||
onSuccess({ connection_id: 'conn-drive', user_email: 'a@example.com' }),
|
||||
}));
|
||||
vi.mock('../components/FilePicker', () => ({
|
||||
FilePicker: ({
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { envVar } from '@/env';
|
||||
import connectorsService from '../api/services/connectorsService';
|
||||
import ConnectorAuth from '../components/ConnectorAuth';
|
||||
import { useConnectorAuth } from '../components/ConnectorAuth';
|
||||
import { FilePicker } from '../components/FilePicker';
|
||||
import GoogleDrivePicker from '../components/GoogleDrivePicker';
|
||||
import {
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
setSelectedDocs,
|
||||
} from '../preferences/preferenceSlice';
|
||||
import type { AppDispatch } from '../store';
|
||||
import { formatCount } from '../utils/dateTimeUtils';
|
||||
import ConnectorIcon from './ConnectorIcon';
|
||||
import CredentialForm, { credentialsComplete } from './CredentialForm';
|
||||
import { loadConnectors, selectConnections } from './connectorsSlice';
|
||||
@@ -134,6 +135,14 @@ export default function ConnectWizard({
|
||||
setStep(canSync ? 'setup' : 'done');
|
||||
};
|
||||
|
||||
const startSignIn = useConnectorAuth({
|
||||
provider: connector.key,
|
||||
connectionId:
|
||||
mode === 'reconnect' ? (connectionId ?? undefined) : undefined,
|
||||
onSuccess: (data) => afterSignIn(data.connection_id),
|
||||
onError: setError,
|
||||
});
|
||||
|
||||
const submitCredentials = async () => {
|
||||
setPending(true);
|
||||
setError('');
|
||||
@@ -253,9 +262,11 @@ export default function ConnectWizard({
|
||||
});
|
||||
const sourcesText = t('settings.connectors.wizard.sourcesCount', {
|
||||
count: sources.length,
|
||||
formatted: formatCount(sources.length),
|
||||
});
|
||||
const toolsText = t('settings.connectors.wizard.toolsCount', {
|
||||
count: toolCount,
|
||||
formatted: formatCount(toolCount),
|
||||
});
|
||||
const countsLine =
|
||||
sources.length && toolCount
|
||||
@@ -289,20 +300,7 @@ export default function ConnectWizard({
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{connector.auth_kind === 'oauth' ? (
|
||||
<ConnectorAuth
|
||||
provider={connector.key}
|
||||
connectionId={
|
||||
mode === 'reconnect' ? (connectionId ?? undefined) : undefined
|
||||
}
|
||||
label={t('settings.connectors.wizard.signIn', {
|
||||
name,
|
||||
interpolation: { escapeValue: false },
|
||||
})}
|
||||
onSuccess={(data) => afterSignIn(data.connection_id)}
|
||||
onError={setError}
|
||||
/>
|
||||
) : (
|
||||
{connector.auth_kind === 'oauth' ? null : (
|
||||
<CredentialForm
|
||||
connectorKey={connector.key}
|
||||
idPrefix={`connect-${connector.key}`}
|
||||
@@ -393,6 +391,7 @@ export default function ConnectWizard({
|
||||
<AccordionTrigger>
|
||||
{t('settings.connectors.wizard.toolsHeading', {
|
||||
count: toolCount,
|
||||
formatted: formatCount(toolCount),
|
||||
})}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
@@ -433,7 +432,17 @@ export default function ConnectWizard({
|
||||
|
||||
const footer =
|
||||
step === 'signin' ? (
|
||||
connector.auth_kind === 'oauth' ? undefined : (
|
||||
connector.auth_kind === 'oauth' ? (
|
||||
<ModalActions
|
||||
cancelLabel={t('cancel')}
|
||||
onCancel={onClose}
|
||||
submitLabel={t('settings.connectors.wizard.signIn', {
|
||||
name,
|
||||
interpolation: { escapeValue: false },
|
||||
})}
|
||||
onSubmit={startSignIn}
|
||||
/>
|
||||
) : (
|
||||
<ModalActions
|
||||
cancelLabel={t('cancel')}
|
||||
onCancel={onClose}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {
|
||||
CircleAlert,
|
||||
ExternalLink,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
RotateCw,
|
||||
Trash2,
|
||||
TriangleAlert,
|
||||
Unplug,
|
||||
} from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
@@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import connectorsService from '../api/services/connectorsService';
|
||||
import { Alert, AlertDescription, AlertTitle } from '../components/ui/alert';
|
||||
import { Alert, AlertDescription } from '../components/ui/alert';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card } from '../components/ui/card';
|
||||
@@ -23,15 +23,21 @@ import { ListRow, ListRows } from '../components/ui/list-row';
|
||||
import { LoadingState } from '../components/ui/loading-state';
|
||||
import { Modal, ModalActions } from '../components/ui/modal';
|
||||
import { SectionHeader } from '../components/ui/section-header';
|
||||
import { Sheet, SheetContent, SheetTitle } from '../components/ui/sheet';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetTitle,
|
||||
} from '../components/ui/sheet';
|
||||
import { ToggleGroup, ToggleGroupItem } from '../components/ui/toggle-group';
|
||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||
import { showActionToast } from '../notifications/actionToastSlice';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import type { AppDispatch } from '../store';
|
||||
import { formatDateTime } from '../utils/dateTimeUtils';
|
||||
import { formatCount, formatDateTime } from '../utils/dateTimeUtils';
|
||||
import { CapabilityBadges } from './ConnectorCard';
|
||||
import ConnectorIcon from './ConnectorIcon';
|
||||
import ConnectorSetupNotice from './ConnectorSetupNotice';
|
||||
import { loadConnectors, selectConnections } from './connectorsSlice';
|
||||
import { connectorDescription, connectorName } from './i18n';
|
||||
import ToolPermissions from './ToolPermissions';
|
||||
@@ -133,6 +139,7 @@ function RemoveConnectionModal({
|
||||
float={false}
|
||||
label={t('settings.connectors.remove.sourcesLabel', {
|
||||
count: detail.sources.length,
|
||||
formatted: formatCount(detail.sources.length),
|
||||
})}
|
||||
>
|
||||
<ToggleGroup
|
||||
@@ -156,6 +163,7 @@ function RemoveConnectionModal({
|
||||
float={false}
|
||||
label={t('settings.connectors.remove.toolsLabel', {
|
||||
count: detail.tools.length,
|
||||
formatted: formatCount(detail.tools.length),
|
||||
})}
|
||||
>
|
||||
<ToggleGroup
|
||||
@@ -219,7 +227,7 @@ function AccountSection({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<Card padding="none">
|
||||
<Card variant="subtle" padding="none">
|
||||
<ListRows>
|
||||
<ListRow
|
||||
title={accountTitle(detail)}
|
||||
@@ -236,7 +244,8 @@ function AccountSection({
|
||||
{detail.status !== 'connected' && (
|
||||
<Button
|
||||
type="button"
|
||||
size="xs"
|
||||
size="sm"
|
||||
shape="pill"
|
||||
variant="outline"
|
||||
onClick={() => onReconnect(detail)}
|
||||
>
|
||||
@@ -275,11 +284,13 @@ function AccountSection({
|
||||
}
|
||||
/>
|
||||
{detail.sources.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t('settings.connectors.detail.noSources')}
|
||||
</p>
|
||||
<EmptyState
|
||||
size="xs"
|
||||
illustration="none"
|
||||
title={t('settings.connectors.detail.noSources')}
|
||||
/>
|
||||
) : (
|
||||
<Card padding="none">
|
||||
<Card variant="subtle" padding="none">
|
||||
<ListRows>
|
||||
{detail.sources.map((source) => (
|
||||
<ListRow
|
||||
@@ -338,17 +349,23 @@ function AccountSection({
|
||||
/>
|
||||
{detail.tools.map((tool) => (
|
||||
<div key={tool.id} className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-foreground min-w-0 truncate text-sm font-medium">
|
||||
{tool.display_name}
|
||||
</span>
|
||||
<Badge variant={tool.status ? 'success' : 'neutral'}>
|
||||
{tool.status
|
||||
? t('settings.connectors.detail.toolOn')
|
||||
: t('settings.connectors.detail.toolOff')}
|
||||
</Badge>
|
||||
</div>
|
||||
<ToolPermissions connectionId={detail.id} tool={tool} />
|
||||
<SectionHeader
|
||||
as="h4"
|
||||
size="xs"
|
||||
title={tool.display_name}
|
||||
actions={
|
||||
<Badge variant={tool.status ? 'success' : 'neutral'}>
|
||||
{tool.status
|
||||
? t('settings.connectors.detail.toolOn')
|
||||
: t('settings.connectors.detail.toolOff')}
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
<ToolPermissions
|
||||
connectionId={detail.id}
|
||||
tool={tool}
|
||||
variant="subtle"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
@@ -420,10 +437,23 @@ export default function ConnectionDrawer({
|
||||
|
||||
const confirmDisconnect = () => {
|
||||
if (!toDisconnect) return;
|
||||
connectorsService.disconnect(toDisconnect.id, token).finally(() => {
|
||||
setToDisconnect(null);
|
||||
refresh();
|
||||
});
|
||||
connectorsService
|
||||
.disconnect(toDisconnect.id, token)
|
||||
.then((data) => {
|
||||
if (!data?.success) throw new Error('disconnect failed');
|
||||
})
|
||||
.catch(() =>
|
||||
dispatch(
|
||||
showActionToast({
|
||||
variant: 'destructive',
|
||||
message: t('settings.connectors.disconnect.failed'),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.finally(() => {
|
||||
setToDisconnect(null);
|
||||
refresh();
|
||||
});
|
||||
};
|
||||
|
||||
const reconnect = (detail: ConnectionDetail) => {
|
||||
@@ -452,8 +482,8 @@ export default function ConnectionDrawer({
|
||||
? {
|
||||
variant: 'success',
|
||||
message: t('settings.connectors.detail.refreshed', {
|
||||
added: data.added?.length ?? 0,
|
||||
removed: data.removed?.length ?? 0,
|
||||
added: formatCount(data.added?.length ?? 0),
|
||||
removed: formatCount(data.removed?.length ?? 0),
|
||||
}),
|
||||
}
|
||||
: {
|
||||
@@ -474,11 +504,12 @@ export default function ConnectionDrawer({
|
||||
<SheetContent
|
||||
side="right"
|
||||
size="detail"
|
||||
aria-describedby={undefined}
|
||||
closeLabel={t('agents.close')}
|
||||
className="overflow-y-auto"
|
||||
>
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
<div className="flex items-start gap-4 pr-8">
|
||||
{/* pr-12 keeps the header clear of the close X. */}
|
||||
<div className="flex items-start gap-4 pr-12">
|
||||
<span className="bg-muted flex size-12 shrink-0 items-center justify-center rounded-xl">
|
||||
<ConnectorIcon icon={connector.icon} className="size-7" />
|
||||
</span>
|
||||
@@ -489,14 +520,14 @@ export default function ConnectionDrawer({
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<SheetDescription>
|
||||
{connectorDescription(t, connector)}
|
||||
</p>
|
||||
</SheetDescription>
|
||||
<CapabilityBadges capabilities={connector.capabilities} />
|
||||
|
||||
{connector.publisher === 'custom' && (
|
||||
<Alert variant="warning" role="note">
|
||||
<CircleAlert />
|
||||
<TriangleAlert />
|
||||
<AlertDescription>
|
||||
{t('settings.connectors.unverified')}
|
||||
</AlertDescription>
|
||||
@@ -504,43 +535,7 @@ export default function ConnectionDrawer({
|
||||
)}
|
||||
|
||||
{connector.needs_setup && (
|
||||
<Alert variant="warning">
|
||||
<CircleAlert />
|
||||
<AlertTitle>
|
||||
{t('settings.connectors.status.needsAdminSetup')}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<div className="flex flex-col gap-2">
|
||||
{connector.missing_settings.length > 0 ? (
|
||||
<>
|
||||
<span>{t('settings.connectors.setupSettings')}</span>
|
||||
<code className="font-mono text-xs wrap-anywhere">
|
||||
{connector.missing_settings.join(', ')}
|
||||
</code>
|
||||
</>
|
||||
) : (
|
||||
<span>{t('settings.connectors.askAdmin')}</span>
|
||||
)}
|
||||
{connector.docs_url && (
|
||||
<Button
|
||||
variant="link"
|
||||
size="inline"
|
||||
asChild
|
||||
className="w-fit"
|
||||
>
|
||||
<a
|
||||
href={connector.docs_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('settings.connectors.setupGuide')}
|
||||
<ExternalLink />
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<ConnectorSetupNotice connector={connector} />
|
||||
)}
|
||||
|
||||
<section className="flex flex-col gap-3">
|
||||
@@ -575,6 +570,7 @@ export default function ConnectionDrawer({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
shape="pill"
|
||||
onClick={() => setReloadKey((key) => key + 1)}
|
||||
>
|
||||
{t('retry')}
|
||||
@@ -599,7 +595,7 @@ export default function ConnectionDrawer({
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-6">
|
||||
{details.map((detail) => (
|
||||
<AccountSection
|
||||
key={detail.id}
|
||||
@@ -630,6 +626,7 @@ export default function ConnectionDrawer({
|
||||
})}
|
||||
description={t('settings.connectors.disconnect.body', {
|
||||
count: toDisconnect?.source_count ?? 0,
|
||||
formatted: formatCount(toDisconnect?.source_count ?? 0),
|
||||
})}
|
||||
modalState={toDisconnect ? 'ACTIVE' : 'INACTIVE'}
|
||||
setModalState={(state) => state === 'INACTIVE' && setToDisconnect(null)}
|
||||
|
||||
@@ -8,20 +8,11 @@ import {
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
} from '../components/ui/card';
|
||||
import { formatCount } from '../utils/dateTimeUtils';
|
||||
import ConnectorIcon from './ConnectorIcon';
|
||||
import { connectorDescription, connectorName } from './i18n';
|
||||
import type { Capability, ConnectorDefinition } from './types';
|
||||
|
||||
// Sync is blue, Read green, Write red: what the connection does to your data.
|
||||
const CAPABILITY_VARIANT: Record<
|
||||
Capability,
|
||||
'info' | 'success' | 'destructive'
|
||||
> = {
|
||||
sync: 'info',
|
||||
read: 'success',
|
||||
write: 'destructive',
|
||||
};
|
||||
|
||||
export function CapabilityBadges({
|
||||
capabilities,
|
||||
}: {
|
||||
@@ -31,7 +22,9 @@ export function CapabilityBadges({
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{capabilities.map((capability) => (
|
||||
<Badge key={capability} variant={CAPABILITY_VARIANT[capability]}>
|
||||
// Capabilities are categories, not states: neutral, so the status
|
||||
// hues stay free for the state badge (DESIGN.md "Colour tokens").
|
||||
<Badge key={capability} variant="neutral">
|
||||
{t(`settings.connectors.capability.${capability}`)}
|
||||
</Badge>
|
||||
))}
|
||||
@@ -53,6 +46,7 @@ export function ConnectorStateBadge({
|
||||
{connector.connected_count > 1
|
||||
? t('settings.connectors.status.connectedCount', {
|
||||
count: connector.connected_count,
|
||||
formatted: formatCount(connector.connected_count),
|
||||
})
|
||||
: t('settings.connectors.status.connected')}
|
||||
</Badge>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { ExternalLink, TriangleAlert } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '../components/ui/alert';
|
||||
import { Button } from '../components/ui/button';
|
||||
import type { ConnectorDefinition } from './types';
|
||||
|
||||
/**
|
||||
* "Needs admin setup" for a connector that still lacks server settings:
|
||||
* the missing setting names for an admin, "ask your admin" for everyone
|
||||
* else, and the setup guide when there is one.
|
||||
*/
|
||||
export default function ConnectorSetupNotice({
|
||||
connector,
|
||||
}: {
|
||||
connector: Pick<ConnectorDefinition, 'missing_settings' | 'docs_url'>;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Alert variant="warning">
|
||||
<TriangleAlert />
|
||||
<AlertTitle>{t('settings.connectors.status.needsAdminSetup')}</AlertTitle>
|
||||
<AlertDescription>
|
||||
<div className="flex flex-col gap-2">
|
||||
{connector.missing_settings.length > 0 ? (
|
||||
<>
|
||||
<span>{t('settings.connectors.setupSettings')}</span>
|
||||
<code className="font-mono text-xs wrap-anywhere">
|
||||
{connector.missing_settings.join(', ')}
|
||||
</code>
|
||||
</>
|
||||
) : (
|
||||
<span>{t('settings.connectors.askAdmin')}</span>
|
||||
)}
|
||||
{connector.docs_url && (
|
||||
<Button
|
||||
variant="link"
|
||||
size="inline"
|
||||
asChild
|
||||
// eslint-disable-next-line shadcn/no-restyle -- the link inherits its Alert's status colour
|
||||
className="w-fit text-current"
|
||||
>
|
||||
<a
|
||||
href={connector.docs_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('settings.connectors.setupGuide')}
|
||||
<ExternalLink />
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import connectorsService from '../api/services/connectorsService';
|
||||
import { Card } from '../components/ui/card';
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '../components/ui/select';
|
||||
import { showActionToast } from '../notifications/actionToastSlice';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import type {
|
||||
ActionPermission,
|
||||
@@ -24,20 +25,24 @@ const PERMISSIONS: ActionPermission[] = ['always', 'ask', 'off'];
|
||||
/**
|
||||
* One tool's actions, grouped Read and Write, each with Always allow /
|
||||
* Needs approval / Off. Changes save immediately; a failed save puts the
|
||||
* previous choice back.
|
||||
* previous choice back and says so.
|
||||
*/
|
||||
export default function ToolPermissions({
|
||||
connectionId,
|
||||
tool,
|
||||
onChange,
|
||||
readOnly = false,
|
||||
variant = 'outline',
|
||||
}: {
|
||||
connectionId: string;
|
||||
tool: ConnectionTool;
|
||||
onChange?: (tool: ConnectionTool) => void;
|
||||
readOnly?: boolean;
|
||||
/** The panel surface: `outline` in a modal, `subtle` in the drawer. */
|
||||
variant?: 'outline' | 'subtle';
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const token = useSelector(selectToken);
|
||||
const [actions, setActions] = useState<ConnectionToolAction[]>(tool.actions);
|
||||
|
||||
@@ -53,7 +58,15 @@ export default function ToolPermissions({
|
||||
if (!data?.success) throw new Error('save failed');
|
||||
onChange?.(data.tool);
|
||||
})
|
||||
.catch(() => setActions(previous));
|
||||
.catch(() => {
|
||||
setActions(previous);
|
||||
dispatch(
|
||||
showActionToast({
|
||||
variant: 'destructive',
|
||||
message: t('settings.connectors.permission.saveFailed'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const groups = (['read', 'write'] as const)
|
||||
@@ -64,7 +77,7 @@ export default function ToolPermissions({
|
||||
.filter((group) => group.actions.length > 0);
|
||||
|
||||
return (
|
||||
<Card padding="sm" className="gap-4">
|
||||
<Card variant={variant} padding="sm" className="gap-4">
|
||||
{groups.map((group) => (
|
||||
<div key={group.access} className="flex flex-col gap-2">
|
||||
<SectionHeader
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useState, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
@@ -7,6 +8,7 @@ import userService from '../api/services/userService';
|
||||
import MCPServerModal from '../modals/MCPServerModal';
|
||||
import type { AvailableToolType } from '../modals/types';
|
||||
import type { ActiveState } from '../models/misc';
|
||||
import { showActionToast } from '../notifications/actionToastSlice';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import type { AppDispatch } from '../store';
|
||||
import ConnectWizard, { type WizardMode } from './ConnectWizard';
|
||||
@@ -46,6 +48,7 @@ const isMcp = (connector: ConnectorDefinition) =>
|
||||
export default function useConnectorLauncher({
|
||||
onConnected,
|
||||
}: { onConnected?: () => void } = {}) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const navigate = useNavigate();
|
||||
const token = useSelector(selectToken);
|
||||
@@ -80,26 +83,34 @@ export default function useConnectorLauncher({
|
||||
if (connector.key === 'custom_openapi') {
|
||||
// An OpenAPI tool starts empty; its spec import is today's API Tool
|
||||
// screen on the Tools page.
|
||||
const response = await userService.getAvailableTools(token);
|
||||
const data = await response.json();
|
||||
const tool = (data.data as AvailableToolType[] | undefined)?.find(
|
||||
(candidate) => candidate.name === 'api_tool',
|
||||
);
|
||||
if (!tool) return;
|
||||
const created = await userService.createTool(
|
||||
{
|
||||
name: tool.name,
|
||||
displayName: tool.displayName,
|
||||
description: tool.description,
|
||||
config: {},
|
||||
actions: tool.actions,
|
||||
status: true,
|
||||
},
|
||||
token,
|
||||
);
|
||||
const body = await created.json();
|
||||
if (body?.id) {
|
||||
try {
|
||||
const response = await userService.getAvailableTools(token);
|
||||
const data = await response.json();
|
||||
const tool = (data.data as AvailableToolType[] | undefined)?.find(
|
||||
(candidate) => candidate.name === 'api_tool',
|
||||
);
|
||||
if (!tool) throw new Error('api_tool is not available');
|
||||
const created = await userService.createTool(
|
||||
{
|
||||
name: tool.name,
|
||||
displayName: tool.displayName,
|
||||
description: tool.description,
|
||||
config: {},
|
||||
actions: tool.actions,
|
||||
status: true,
|
||||
},
|
||||
token,
|
||||
);
|
||||
const body = await created.json();
|
||||
if (!body?.id) throw new Error('create failed');
|
||||
navigate('/settings/tools', { state: { openToolId: body.id } });
|
||||
} catch {
|
||||
dispatch(
|
||||
showActionToast({
|
||||
variant: 'destructive',
|
||||
message: t('settings.connectors.createFailed'),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -110,7 +121,7 @@ export default function useConnectorLauncher({
|
||||
connectionId: options.connectionId,
|
||||
});
|
||||
},
|
||||
[navigate, token],
|
||||
[dispatch, navigate, t, token],
|
||||
);
|
||||
|
||||
const afterMcpSave = async () => {
|
||||
|
||||
+18
-10
@@ -1120,7 +1120,7 @@
|
||||
"status": {
|
||||
"connect": "Verbinden",
|
||||
"connected": "Verbunden",
|
||||
"connectedCount": "{{count}} verbunden",
|
||||
"connectedCount": "{{formatted}} verbunden",
|
||||
"reconnect": "Neu verbinden",
|
||||
"needsAdminSetup": "Admin-Einrichtung nötig",
|
||||
"disabledByAdmin": "Vom Admin deaktiviert",
|
||||
@@ -1190,13 +1190,15 @@
|
||||
"always": "Immer erlauben",
|
||||
"ask": "Freigabe nötig",
|
||||
"off": "Aus",
|
||||
"label": "Berechtigung für {{action}}"
|
||||
"label": "Berechtigung für {{action}}",
|
||||
"saveFailed": "Die Berechtigung konnte nicht gespeichert werden. Versuche es erneut."
|
||||
},
|
||||
"disconnect": {
|
||||
"title": "{{name}} trennen?",
|
||||
"body_zero": "Werkzeuge aus dieser Verbindung funktionieren erst wieder, wenn du neu verbindest.",
|
||||
"body_one": "Deine Quelle behält ihren Inhalt, wird aber nicht mehr synchronisiert. Werkzeuge aus dieser Verbindung funktionieren erst wieder, wenn du neu verbindest.",
|
||||
"body_other": "Deine {{count}} Quellen behalten ihren Inhalt, werden aber nicht mehr synchronisiert. Werkzeuge aus dieser Verbindung funktionieren erst wieder, wenn du neu verbindest."
|
||||
"body_other": "Deine {{formatted}} Quellen behalten ihren Inhalt, werden aber nicht mehr synchronisiert. Werkzeuge aus dieser Verbindung funktionieren erst wieder, wenn du neu verbindest.",
|
||||
"failed": "Trennen fehlgeschlagen. Versuche es erneut."
|
||||
},
|
||||
"fields": {
|
||||
"token": "API-Schlüssel",
|
||||
@@ -1226,11 +1228,11 @@
|
||||
"doneTitle": "{{name}} ist verbunden",
|
||||
"doneSummary": "Verbunden als {{account}}. {{sources}} werden synchronisiert, {{tools}} bereit.",
|
||||
"doneSummaryNone": "Verbunden als {{account}}.",
|
||||
"sourcesCount_one": "{{count}} Quelle",
|
||||
"sourcesCount_other": "{{count}} Quellen",
|
||||
"toolsCount_one": "{{count}} Werkzeug",
|
||||
"toolsCount_other": "{{count}} Werkzeuge",
|
||||
"toolsHeading": "Werkzeuge ({{count}})",
|
||||
"sourcesCount_one": "{{formatted}} Quelle",
|
||||
"sourcesCount_other": "{{formatted}} Quellen",
|
||||
"toolsCount_one": "{{formatted}} Werkzeug",
|
||||
"toolsCount_other": "{{formatted}} Werkzeuge",
|
||||
"toolsHeading": "Werkzeuge ({{formatted}})",
|
||||
"tryInChat": "Im Chat ausprobieren",
|
||||
"done": "Fertig",
|
||||
"connectFailed": "Verbindung fehlgeschlagen. Prüfe die Angaben und versuche es erneut.",
|
||||
@@ -1244,11 +1246,11 @@
|
||||
"title": "{{name}} entfernen?",
|
||||
"description": "Die gespeicherte Anmeldung wird gelöscht. Wähle, was mit den Inhalten dieser Verbindung passiert.",
|
||||
"sourcesLabel_one": "Ihre Quelle",
|
||||
"sourcesLabel_other": "Ihre {{count}} Quellen",
|
||||
"sourcesLabel_other": "Ihre {{formatted}} Quellen",
|
||||
"keepSources": "Inhalt behalten (Sync stoppen)",
|
||||
"deleteSources": "Löschen",
|
||||
"toolsLabel_one": "Ihr Werkzeug",
|
||||
"toolsLabel_other": "Ihre {{count}} Werkzeuge",
|
||||
"toolsLabel_other": "Ihre {{formatted}} Werkzeuge",
|
||||
"deleteTools": "Löschen",
|
||||
"keepTools": "Behalten, ausgeschaltet",
|
||||
"failed": "Die Verbindung konnte nicht entfernt werden. Versuche es erneut."
|
||||
@@ -1271,6 +1273,12 @@
|
||||
"memberNote": "Jedes Mitglied verbindet sein eigenes {{name}}-Konto, bevor es dieses Werkzeug nutzen kann.",
|
||||
"forced": "Ein Admin hat das für alle Freigaben dieses Konnektors festgelegt.",
|
||||
"saveFailed": "Die Änderung konnte nicht gespeichert werden. Versuche es erneut."
|
||||
},
|
||||
"createFailed": "Der Konnektor konnte nicht hinzugefügt werden. Versuche es erneut.",
|
||||
"capabilityFilter": {
|
||||
"sync": "Angezeigt werden Konnektoren, die Inhalte in Quellen synchronisieren.",
|
||||
"tools": "Angezeigt werden Konnektoren, die Agenten Werkzeuge geben.",
|
||||
"showAll": "Alle anzeigen"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+18
-13
@@ -1126,7 +1126,7 @@
|
||||
"status": {
|
||||
"connect": "Connect",
|
||||
"connected": "Connected",
|
||||
"connectedCount": "{{count}} connected",
|
||||
"connectedCount": "{{formatted}} connected",
|
||||
"reconnect": "Reconnect",
|
||||
"needsAdminSetup": "Needs admin setup",
|
||||
"disabledByAdmin": "Disabled by admin",
|
||||
@@ -1196,13 +1196,15 @@
|
||||
"always": "Always allow",
|
||||
"ask": "Needs approval",
|
||||
"off": "Off",
|
||||
"label": "Permission for {{action}}"
|
||||
"label": "Permission for {{action}}",
|
||||
"saveFailed": "Could not save the permission. Try again."
|
||||
},
|
||||
"disconnect": {
|
||||
"title": "Disconnect {{name}}?",
|
||||
"body_zero": "Tools from this connection stop working until you reconnect.",
|
||||
"body_one": "Your source keeps its content but stops syncing. Tools from this connection stop working until you reconnect.",
|
||||
"body_other": "Your {{count}} sources keep their content but stop syncing. Tools from this connection stop working until you reconnect."
|
||||
"body_other": "Your {{formatted}} sources keep their content but stop syncing. Tools from this connection stop working until you reconnect.",
|
||||
"failed": "Could not disconnect. Try again."
|
||||
},
|
||||
"fields": {
|
||||
"token": "API key",
|
||||
@@ -1232,11 +1234,11 @@
|
||||
"doneTitle": "{{name}} is connected",
|
||||
"doneSummary": "Connected as {{account}}. {{sources}} syncing, {{tools}} ready.",
|
||||
"doneSummaryNone": "Connected as {{account}}.",
|
||||
"sourcesCount_one": "{{count}} source",
|
||||
"sourcesCount_other": "{{count}} sources",
|
||||
"toolsCount_one": "{{count}} tool",
|
||||
"toolsCount_other": "{{count}} tools",
|
||||
"toolsHeading": "Tools ({{count}})",
|
||||
"sourcesCount_one": "{{formatted}} source",
|
||||
"sourcesCount_other": "{{formatted}} sources",
|
||||
"toolsCount_one": "{{formatted}} tool",
|
||||
"toolsCount_other": "{{formatted}} tools",
|
||||
"toolsHeading": "Tools ({{formatted}})",
|
||||
"tryInChat": "Try it in chat",
|
||||
"done": "Done",
|
||||
"connectFailed": "Could not connect. Check the details and try again.",
|
||||
@@ -1250,11 +1252,11 @@
|
||||
"title": "Remove {{name}}?",
|
||||
"description": "The stored sign-in is deleted. Choose what happens to what this connection feeds.",
|
||||
"sourcesLabel_one": "Its source",
|
||||
"sourcesLabel_other": "Its {{count}} sources",
|
||||
"sourcesLabel_other": "Its {{formatted}} sources",
|
||||
"keepSources": "Keep content (stop syncing)",
|
||||
"deleteSources": "Delete",
|
||||
"toolsLabel_one": "Its tool",
|
||||
"toolsLabel_other": "Its {{count}} tools",
|
||||
"toolsLabel_other": "Its {{formatted}} tools",
|
||||
"deleteTools": "Delete",
|
||||
"keepTools": "Keep, turned off",
|
||||
"failed": "Could not remove the connection. Try again."
|
||||
@@ -1277,6 +1279,12 @@
|
||||
"memberNote": "Each member connects their own {{name}} account before they can use this tool.",
|
||||
"forced": "An admin chose this for every share of this connector.",
|
||||
"saveFailed": "Could not save the change. Try again."
|
||||
},
|
||||
"createFailed": "Could not add the connector. Try again.",
|
||||
"capabilityFilter": {
|
||||
"sync": "Showing connectors that sync content into Sources.",
|
||||
"tools": "Showing connectors that give agents tools.",
|
||||
"showAll": "Show all"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2616,9 +2624,6 @@
|
||||
"body": "These were used this {{period}} and recorded at $0: {{models}}. Use a token limit for them, or declare their rates in the model catalog.",
|
||||
"model": "{{model}} ({{tokens}} tokens)"
|
||||
}
|
||||
},
|
||||
"connectors": {
|
||||
"defaultKey": "Set ENCRYPTION_SECRET_KEY before connecting services."
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-10
@@ -1120,7 +1120,7 @@
|
||||
"status": {
|
||||
"connect": "Conectar",
|
||||
"connected": "Conectado",
|
||||
"connectedCount": "{{count}} conectadas",
|
||||
"connectedCount": "{{formatted}} conectadas",
|
||||
"reconnect": "Reconectar",
|
||||
"needsAdminSetup": "Requiere configuración del administrador",
|
||||
"disabledByAdmin": "Desactivado por el administrador",
|
||||
@@ -1190,13 +1190,15 @@
|
||||
"always": "Permitir siempre",
|
||||
"ask": "Requiere aprobación",
|
||||
"off": "Desactivada",
|
||||
"label": "Permiso para {{action}}"
|
||||
"label": "Permiso para {{action}}",
|
||||
"saveFailed": "No se pudo guardar el permiso. Inténtalo de nuevo."
|
||||
},
|
||||
"disconnect": {
|
||||
"title": "¿Desconectar {{name}}?",
|
||||
"body_zero": "Las herramientas de esta conexión dejarán de funcionar hasta que vuelvas a conectarla.",
|
||||
"body_one": "Tu fuente conserva su contenido pero deja de sincronizarse. Las herramientas de esta conexión dejarán de funcionar hasta que vuelvas a conectarla.",
|
||||
"body_other": "Tus {{count}} fuentes conservan su contenido pero dejan de sincronizarse. Las herramientas de esta conexión dejarán de funcionar hasta que vuelvas a conectarla."
|
||||
"body_other": "Tus {{formatted}} fuentes conservan su contenido pero dejan de sincronizarse. Las herramientas de esta conexión dejarán de funcionar hasta que vuelvas a conectarla.",
|
||||
"failed": "No se pudo desconectar. Inténtalo de nuevo."
|
||||
},
|
||||
"fields": {
|
||||
"token": "Clave de API",
|
||||
@@ -1226,11 +1228,11 @@
|
||||
"doneTitle": "{{name}} está conectado",
|
||||
"doneSummary": "Conectado como {{account}}. {{sources}} sincronizando, {{tools}} listas.",
|
||||
"doneSummaryNone": "Conectado como {{account}}.",
|
||||
"sourcesCount_one": "{{count}} fuente",
|
||||
"sourcesCount_other": "{{count}} fuentes",
|
||||
"toolsCount_one": "{{count}} herramienta",
|
||||
"toolsCount_other": "{{count}} herramientas",
|
||||
"toolsHeading": "Herramientas ({{count}})",
|
||||
"sourcesCount_one": "{{formatted}} fuente",
|
||||
"sourcesCount_other": "{{formatted}} fuentes",
|
||||
"toolsCount_one": "{{formatted}} herramienta",
|
||||
"toolsCount_other": "{{formatted}} herramientas",
|
||||
"toolsHeading": "Herramientas ({{formatted}})",
|
||||
"tryInChat": "Probar en el chat",
|
||||
"done": "Listo",
|
||||
"connectFailed": "No se pudo conectar. Revisa los datos e inténtalo de nuevo.",
|
||||
@@ -1244,11 +1246,11 @@
|
||||
"title": "¿Eliminar {{name}}?",
|
||||
"description": "Se borra el inicio de sesión guardado. Elige qué pasa con lo que alimenta esta conexión.",
|
||||
"sourcesLabel_one": "Su fuente",
|
||||
"sourcesLabel_other": "Sus {{count}} fuentes",
|
||||
"sourcesLabel_other": "Sus {{formatted}} fuentes",
|
||||
"keepSources": "Conservar el contenido (dejar de sincronizar)",
|
||||
"deleteSources": "Eliminar",
|
||||
"toolsLabel_one": "Su herramienta",
|
||||
"toolsLabel_other": "Sus {{count}} herramientas",
|
||||
"toolsLabel_other": "Sus {{formatted}} herramientas",
|
||||
"deleteTools": "Eliminar",
|
||||
"keepTools": "Conservar, desactivadas",
|
||||
"failed": "No se pudo eliminar la conexión. Inténtalo de nuevo."
|
||||
@@ -1271,6 +1273,12 @@
|
||||
"memberNote": "Cada miembro conecta su propia cuenta de {{name}} antes de poder usar esta herramienta.",
|
||||
"forced": "Un administrador eligió esto para todos los recursos compartidos de este conector.",
|
||||
"saveFailed": "No se pudo guardar el cambio. Inténtalo de nuevo."
|
||||
},
|
||||
"createFailed": "No se pudo añadir el conector. Inténtalo de nuevo.",
|
||||
"capabilityFilter": {
|
||||
"sync": "Se muestran los conectores que sincronizan contenido en Fuentes.",
|
||||
"tools": "Se muestran los conectores que dan herramientas a los agentes.",
|
||||
"showAll": "Mostrar todos"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1111,7 +1111,7 @@
|
||||
"status": {
|
||||
"connect": "接続",
|
||||
"connected": "接続済み",
|
||||
"connectedCount": "{{count}} 件接続済み",
|
||||
"connectedCount": "{{formatted}} 件接続済み",
|
||||
"reconnect": "再接続",
|
||||
"needsAdminSetup": "管理者の設定が必要",
|
||||
"disabledByAdmin": "管理者により無効",
|
||||
@@ -1181,12 +1181,14 @@
|
||||
"always": "常に許可",
|
||||
"ask": "承認が必要",
|
||||
"off": "オフ",
|
||||
"label": "{{action}} の権限"
|
||||
"label": "{{action}} の権限",
|
||||
"saveFailed": "権限を保存できませんでした。もう一度お試しください。"
|
||||
},
|
||||
"disconnect": {
|
||||
"title": "{{name}} を切断しますか?",
|
||||
"body_zero": "この接続のツールは、再接続するまで使えなくなります。",
|
||||
"body_other": "{{count}} 件のソースはコンテンツを保持しますが、同期は停止します。この接続のツールは、再接続するまで使えなくなります。"
|
||||
"body_other": "{{formatted}} 件のソースはコンテンツを保持しますが、同期は停止します。この接続のツールは、再接続するまで使えなくなります。",
|
||||
"failed": "切断できませんでした。もう一度お試しください。"
|
||||
},
|
||||
"fields": {
|
||||
"token": "API キー",
|
||||
@@ -1216,9 +1218,9 @@
|
||||
"doneTitle": "{{name}} に接続しました",
|
||||
"doneSummary": "{{account}} として接続しました。{{sources}}を同期中、{{tools}}を利用できます。",
|
||||
"doneSummaryNone": "{{account}} として接続しました。",
|
||||
"sourcesCount_other": "{{count}} 件のソース",
|
||||
"toolsCount_other": "{{count}} 個のツール",
|
||||
"toolsHeading": "ツール ({{count}})",
|
||||
"sourcesCount_other": "{{formatted}} 件のソース",
|
||||
"toolsCount_other": "{{formatted}} 個のツール",
|
||||
"toolsHeading": "ツール ({{formatted}})",
|
||||
"tryInChat": "チャットで試す",
|
||||
"done": "完了",
|
||||
"connectFailed": "接続できませんでした。入力内容を確認して、もう一度お試しください。",
|
||||
@@ -1231,10 +1233,10 @@
|
||||
"remove": {
|
||||
"title": "{{name}} を削除しますか?",
|
||||
"description": "保存されたサインイン情報は削除されます。この接続から提供されているものをどうするか選んでください。",
|
||||
"sourcesLabel_other": "{{count}} 件のソース",
|
||||
"sourcesLabel_other": "{{formatted}} 件のソース",
|
||||
"keepSources": "コンテンツを保持(同期を停止)",
|
||||
"deleteSources": "削除",
|
||||
"toolsLabel_other": "{{count}} 個のツール",
|
||||
"toolsLabel_other": "{{formatted}} 個のツール",
|
||||
"deleteTools": "削除",
|
||||
"keepTools": "保持してオフにする",
|
||||
"failed": "接続を削除できませんでした。もう一度お試しください。"
|
||||
@@ -1257,6 +1259,12 @@
|
||||
"memberNote": "各メンバーは、このツールを使う前に自分の {{name}} アカウントを接続します。",
|
||||
"forced": "このコネクタのすべての共有に対して管理者が設定しています。",
|
||||
"saveFailed": "変更を保存できませんでした。もう一度お試しください。"
|
||||
},
|
||||
"createFailed": "コネクタを追加できませんでした。もう一度お試しください。",
|
||||
"capabilityFilter": {
|
||||
"sync": "ソースにコンテンツを同期するコネクタを表示しています。",
|
||||
"tools": "エージェントにツールを提供するコネクタを表示しています。",
|
||||
"showAll": "すべて表示"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+29
-21
@@ -1176,7 +1176,7 @@
|
||||
"status": {
|
||||
"connect": "Подключить",
|
||||
"connected": "Подключено",
|
||||
"connectedCount": "Подключено: {{count}}",
|
||||
"connectedCount": "Подключено: {{formatted}}",
|
||||
"reconnect": "Переподключить",
|
||||
"needsAdminSetup": "Нужна настройка администратором",
|
||||
"disabledByAdmin": "Отключено администратором",
|
||||
@@ -1246,15 +1246,17 @@
|
||||
"always": "Всегда разрешать",
|
||||
"ask": "Нужно одобрение",
|
||||
"off": "Выкл.",
|
||||
"label": "Разрешение для {{action}}"
|
||||
"label": "Разрешение для {{action}}",
|
||||
"saveFailed": "Не удалось сохранить разрешение. Попробуйте ещё раз."
|
||||
},
|
||||
"disconnect": {
|
||||
"title": "Отключить {{name}}?",
|
||||
"body_zero": "Инструменты из этого подключения перестанут работать, пока вы не переподключитесь.",
|
||||
"body_one": "Ваш {{count}} источник сохранит контент, но перестанет синхронизироваться. Инструменты из этого подключения перестанут работать, пока вы не переподключитесь.",
|
||||
"body_few": "Ваши {{count}} источника сохранят контент, но перестанут синхронизироваться. Инструменты из этого подключения перестанут работать, пока вы не переподключитесь.",
|
||||
"body_many": "Ваши {{count}} источников сохранят контент, но перестанут синхронизироваться. Инструменты из этого подключения перестанут работать, пока вы не переподключитесь.",
|
||||
"body_other": "Ваши {{count}} источника сохранят контент, но перестанут синхронизироваться. Инструменты из этого подключения перестанут работать, пока вы не переподключитесь."
|
||||
"body_one": "Ваш {{formatted}} источник сохранит контент, но перестанет синхронизироваться. Инструменты из этого подключения перестанут работать, пока вы не переподключитесь.",
|
||||
"body_few": "Ваши {{formatted}} источника сохранят контент, но перестанут синхронизироваться. Инструменты из этого подключения перестанут работать, пока вы не переподключитесь.",
|
||||
"body_many": "Ваши {{formatted}} источников сохранят контент, но перестанут синхронизироваться. Инструменты из этого подключения перестанут работать, пока вы не переподключитесь.",
|
||||
"body_other": "Ваши {{formatted}} источника сохранят контент, но перестанут синхронизироваться. Инструменты из этого подключения перестанут работать, пока вы не переподключитесь.",
|
||||
"failed": "Не удалось отключить. Попробуйте ещё раз."
|
||||
},
|
||||
"fields": {
|
||||
"token": "API-ключ",
|
||||
@@ -1284,15 +1286,15 @@
|
||||
"doneTitle": "{{name}} подключён",
|
||||
"doneSummary": "Подключено как {{account}}. Синхронизируется: {{sources}}, готово: {{tools}}.",
|
||||
"doneSummaryNone": "Подключено как {{account}}.",
|
||||
"sourcesCount_one": "{{count}} источник",
|
||||
"sourcesCount_few": "{{count}} источника",
|
||||
"sourcesCount_many": "{{count}} источников",
|
||||
"sourcesCount_other": "{{count}} источника",
|
||||
"toolsCount_one": "{{count}} инструмент",
|
||||
"toolsCount_few": "{{count}} инструмента",
|
||||
"toolsCount_many": "{{count}} инструментов",
|
||||
"toolsCount_other": "{{count}} инструмента",
|
||||
"toolsHeading": "Инструменты ({{count}})",
|
||||
"sourcesCount_one": "{{formatted}} источник",
|
||||
"sourcesCount_few": "{{formatted}} источника",
|
||||
"sourcesCount_many": "{{formatted}} источников",
|
||||
"sourcesCount_other": "{{formatted}} источника",
|
||||
"toolsCount_one": "{{formatted}} инструмент",
|
||||
"toolsCount_few": "{{formatted}} инструмента",
|
||||
"toolsCount_many": "{{formatted}} инструментов",
|
||||
"toolsCount_other": "{{formatted}} инструмента",
|
||||
"toolsHeading": "Инструменты ({{formatted}})",
|
||||
"tryInChat": "Попробовать в чате",
|
||||
"done": "Готово",
|
||||
"connectFailed": "Не удалось подключиться. Проверьте данные и попробуйте ещё раз.",
|
||||
@@ -1306,15 +1308,15 @@
|
||||
"title": "Удалить {{name}}?",
|
||||
"description": "Сохранённый вход будет удалён. Выберите, что станет с тем, что даёт это подключение.",
|
||||
"sourcesLabel_one": "Его источник",
|
||||
"sourcesLabel_few": "Его {{count}} источника",
|
||||
"sourcesLabel_many": "Его {{count}} источников",
|
||||
"sourcesLabel_other": "Его {{count}} источника",
|
||||
"sourcesLabel_few": "Его {{formatted}} источника",
|
||||
"sourcesLabel_many": "Его {{formatted}} источников",
|
||||
"sourcesLabel_other": "Его {{formatted}} источника",
|
||||
"keepSources": "Оставить контент (остановить синхронизацию)",
|
||||
"deleteSources": "Удалить",
|
||||
"toolsLabel_one": "Его инструмент",
|
||||
"toolsLabel_few": "Его {{count}} инструмента",
|
||||
"toolsLabel_many": "Его {{count}} инструментов",
|
||||
"toolsLabel_other": "Его {{count}} инструмента",
|
||||
"toolsLabel_few": "Его {{formatted}} инструмента",
|
||||
"toolsLabel_many": "Его {{formatted}} инструментов",
|
||||
"toolsLabel_other": "Его {{formatted}} инструмента",
|
||||
"deleteTools": "Удалить",
|
||||
"keepTools": "Оставить выключенными",
|
||||
"failed": "Не удалось удалить подключение. Попробуйте ещё раз."
|
||||
@@ -1337,6 +1339,12 @@
|
||||
"memberNote": "Каждый участник подключает свой аккаунт {{name}}, прежде чем пользоваться этим инструментом.",
|
||||
"forced": "Администратор задал это для всех общих доступов к этому коннектору.",
|
||||
"saveFailed": "Не удалось сохранить изменение. Попробуйте ещё раз."
|
||||
},
|
||||
"createFailed": "Не удалось добавить коннектор. Попробуйте ещё раз.",
|
||||
"capabilityFilter": {
|
||||
"sync": "Показаны коннекторы, которые синхронизируют содержимое в источники.",
|
||||
"tools": "Показаны коннекторы, которые дают агентам инструменты.",
|
||||
"showAll": "Показать все"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1111,7 +1111,7 @@
|
||||
"status": {
|
||||
"connect": "連線",
|
||||
"connected": "已連線",
|
||||
"connectedCount": "已連線 {{count}} 個",
|
||||
"connectedCount": "已連線 {{formatted}} 個",
|
||||
"reconnect": "重新連線",
|
||||
"needsAdminSetup": "需要管理員設定",
|
||||
"disabledByAdmin": "已被管理員停用",
|
||||
@@ -1181,12 +1181,14 @@
|
||||
"always": "一律允許",
|
||||
"ask": "需要核准",
|
||||
"off": "關閉",
|
||||
"label": "{{action}} 的權限"
|
||||
"label": "{{action}} 的權限",
|
||||
"saveFailed": "無法儲存權限。請再試一次。"
|
||||
},
|
||||
"disconnect": {
|
||||
"title": "要中斷 {{name}} 的連線嗎?",
|
||||
"body_zero": "在重新連線之前,此連線的工具將無法使用。",
|
||||
"body_other": "你的 {{count}} 個來源會保留內容,但會停止同步。在重新連線之前,此連線的工具將無法使用。"
|
||||
"body_other": "你的 {{formatted}} 個來源會保留內容,但會停止同步。在重新連線之前,此連線的工具將無法使用。",
|
||||
"failed": "無法中斷連線。請再試一次。"
|
||||
},
|
||||
"fields": {
|
||||
"token": "API 金鑰",
|
||||
@@ -1216,9 +1218,9 @@
|
||||
"doneTitle": "{{name}} 已連線",
|
||||
"doneSummary": "已以 {{account}} 連線。{{sources}}同步中,{{tools}}已就緒。",
|
||||
"doneSummaryNone": "已以 {{account}} 連線。",
|
||||
"sourcesCount_other": "{{count}} 個來源",
|
||||
"toolsCount_other": "{{count}} 個工具",
|
||||
"toolsHeading": "工具({{count}})",
|
||||
"sourcesCount_other": "{{formatted}} 個來源",
|
||||
"toolsCount_other": "{{formatted}} 個工具",
|
||||
"toolsHeading": "工具({{formatted}})",
|
||||
"tryInChat": "在聊天中試試",
|
||||
"done": "完成",
|
||||
"connectFailed": "無法連線。請檢查資料後再試一次。",
|
||||
@@ -1231,10 +1233,10 @@
|
||||
"remove": {
|
||||
"title": "要移除 {{name}} 嗎?",
|
||||
"description": "儲存的登入資訊將被刪除。請選擇如何處理此連線提供的內容。",
|
||||
"sourcesLabel_other": "它的 {{count}} 個來源",
|
||||
"sourcesLabel_other": "它的 {{formatted}} 個來源",
|
||||
"keepSources": "保留內容(停止同步)",
|
||||
"deleteSources": "刪除",
|
||||
"toolsLabel_other": "它的 {{count}} 個工具",
|
||||
"toolsLabel_other": "它的 {{formatted}} 個工具",
|
||||
"deleteTools": "刪除",
|
||||
"keepTools": "保留並關閉",
|
||||
"failed": "無法移除此連線。請再試一次。"
|
||||
@@ -1257,6 +1259,12 @@
|
||||
"memberNote": "每位成員須先連線自己的 {{name}} 帳號,才能使用此工具。",
|
||||
"forced": "管理員已為此連接器的所有共用設定此項。",
|
||||
"saveFailed": "無法儲存變更。請再試一次。"
|
||||
},
|
||||
"createFailed": "無法新增連接器。請再試一次。",
|
||||
"capabilityFilter": {
|
||||
"sync": "正在顯示將內容同步到來源的連接器。",
|
||||
"tools": "正在顯示為代理提供工具的連接器。",
|
||||
"showAll": "顯示全部"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1111,7 +1111,7 @@
|
||||
"status": {
|
||||
"connect": "连接",
|
||||
"connected": "已连接",
|
||||
"connectedCount": "已连接 {{count}} 个",
|
||||
"connectedCount": "已连接 {{formatted}} 个",
|
||||
"reconnect": "重新连接",
|
||||
"needsAdminSetup": "需要管理员设置",
|
||||
"disabledByAdmin": "已被管理员停用",
|
||||
@@ -1181,12 +1181,14 @@
|
||||
"always": "始终允许",
|
||||
"ask": "需要批准",
|
||||
"off": "关闭",
|
||||
"label": "{{action}} 的权限"
|
||||
"label": "{{action}} 的权限",
|
||||
"saveFailed": "无法保存权限。请重试。"
|
||||
},
|
||||
"disconnect": {
|
||||
"title": "断开 {{name}}?",
|
||||
"body_zero": "在重新连接之前,此连接的工具将无法使用。",
|
||||
"body_other": "你的 {{count}} 个来源会保留内容,但将停止同步。在重新连接之前,此连接的工具将无法使用。"
|
||||
"body_other": "你的 {{formatted}} 个来源会保留内容,但将停止同步。在重新连接之前,此连接的工具将无法使用。",
|
||||
"failed": "无法断开连接。请重试。"
|
||||
},
|
||||
"fields": {
|
||||
"token": "API 密钥",
|
||||
@@ -1216,9 +1218,9 @@
|
||||
"doneTitle": "{{name}} 已连接",
|
||||
"doneSummary": "已作为 {{account}} 连接。{{sources}}正在同步,{{tools}}已就绪。",
|
||||
"doneSummaryNone": "已作为 {{account}} 连接。",
|
||||
"sourcesCount_other": "{{count}} 个来源",
|
||||
"toolsCount_other": "{{count}} 个工具",
|
||||
"toolsHeading": "工具({{count}})",
|
||||
"sourcesCount_other": "{{formatted}} 个来源",
|
||||
"toolsCount_other": "{{formatted}} 个工具",
|
||||
"toolsHeading": "工具({{formatted}})",
|
||||
"tryInChat": "在聊天中试试",
|
||||
"done": "完成",
|
||||
"connectFailed": "无法连接。请检查信息后重试。",
|
||||
@@ -1231,10 +1233,10 @@
|
||||
"remove": {
|
||||
"title": "移除 {{name}}?",
|
||||
"description": "保存的登录信息将被删除。请选择如何处理此连接提供的内容。",
|
||||
"sourcesLabel_other": "它的 {{count}} 个来源",
|
||||
"sourcesLabel_other": "它的 {{formatted}} 个来源",
|
||||
"keepSources": "保留内容(停止同步)",
|
||||
"deleteSources": "删除",
|
||||
"toolsLabel_other": "它的 {{count}} 个工具",
|
||||
"toolsLabel_other": "它的 {{formatted}} 个工具",
|
||||
"deleteTools": "删除",
|
||||
"keepTools": "保留并关闭",
|
||||
"failed": "无法移除此连接。请重试。"
|
||||
@@ -1257,6 +1259,12 @@
|
||||
"memberNote": "每位成员需先连接自己的 {{name}} 账号才能使用此工具。",
|
||||
"forced": "管理员已为此连接器的所有共享设定了此项。",
|
||||
"saveFailed": "无法保存更改。请重试。"
|
||||
},
|
||||
"createFailed": "无法添加连接器。请重试。",
|
||||
"capabilityFilter": {
|
||||
"sync": "正在显示将内容同步到来源的连接器。",
|
||||
"tools": "正在显示为智能体提供工具的连接器。",
|
||||
"showAll": "显示全部"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { ArrowRight, Plus } from 'lucide-react';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import SkeletonLoader from '../components/SkeletonLoader';
|
||||
import ToolIcon from '../components/ToolIcon';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardDescription, CardTitle } from '../components/ui/card';
|
||||
import { Modal } from '../components/ui/modal';
|
||||
import { Modal, ModalActions } from '../components/ui/modal';
|
||||
import { SectionHeader } from '../components/ui/section-header';
|
||||
import { useLoaderState } from '../hooks';
|
||||
import {
|
||||
@@ -40,6 +40,7 @@ export default function AddToolModal({
|
||||
onDevicePaired?: (deviceId: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const token = useSelector(selectToken);
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const catalog = useSelector(selectConnectorCatalog);
|
||||
@@ -141,6 +142,28 @@ export default function AddToolModal({
|
||||
onOpenChange={(o) => !o && setModalState('INACTIVE')}
|
||||
title={t('settings.tools.selectToolSetup')}
|
||||
size="xl"
|
||||
footer={
|
||||
<ModalActions
|
||||
footerStart={
|
||||
<Button variant="link" size="inline" asChild>
|
||||
<Link
|
||||
to="/settings/connectors"
|
||||
onClick={() => setModalState('INACTIVE')}
|
||||
>
|
||||
{t('settings.tools.browseConnectors')}
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
cancelLabel={t('cancel')}
|
||||
onCancel={() => setModalState('INACTIVE')}
|
||||
submitLabel={t('settings.connectors.addCustom')}
|
||||
onSubmit={() => {
|
||||
setModalState('INACTIVE');
|
||||
navigate('/settings/connectors?filter=custom');
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-6">
|
||||
{loading ? (
|
||||
@@ -205,26 +228,6 @@ export default function AddToolModal({
|
||||
);
|
||||
})
|
||||
)}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<Button variant="link" size="inline" asChild>
|
||||
<Link
|
||||
to="/settings/connectors"
|
||||
onClick={() => setModalState('INACTIVE')}
|
||||
>
|
||||
{t('settings.tools.browseConnectors')}
|
||||
<ArrowRight className="size-3" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline-primary" shape="pill" size="sm" asChild>
|
||||
<Link
|
||||
to="/settings/connectors?filter=custom"
|
||||
onClick={() => setModalState('INACTIVE')}
|
||||
>
|
||||
<Plus />
|
||||
{t('settings.connectors.addCustom')}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<ConfigToolModal
|
||||
|
||||
@@ -209,6 +209,33 @@ describe('Connectors page', () => {
|
||||
expect(container.textContent).toContain('settings.connectors.empty');
|
||||
});
|
||||
|
||||
it('says when the composer narrowed the list, and clears it', async () => {
|
||||
await render('/settings/connectors?capability=sync');
|
||||
expect(container.textContent).toContain(
|
||||
'settings.connectors.capabilityFilter.sync',
|
||||
);
|
||||
expect(card('telegram')).toBeNull();
|
||||
const showAll = Array.from(container.querySelectorAll('button')).find(
|
||||
(button) =>
|
||||
button.textContent === 'settings.connectors.capabilityFilter.showAll',
|
||||
)!;
|
||||
await act(async () => showAll.click());
|
||||
expect(container.textContent).not.toContain(
|
||||
'settings.connectors.capabilityFilter.sync',
|
||||
);
|
||||
expect(card('telegram')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('only offers categories that have connectors', async () => {
|
||||
await render();
|
||||
const pills = Array.from(
|
||||
container.querySelectorAll('[data-slot="toggle-group-item"]'),
|
||||
).map((item) => item.textContent);
|
||||
expect(pills).toContain('settings.connectors.categories.all');
|
||||
expect(pills).toContain('settings.connectors.categories.files');
|
||||
expect(pills).not.toContain('settings.connectors.categories.database');
|
||||
});
|
||||
|
||||
it('shows a retry when the catalog fails to load', async () => {
|
||||
service.getCatalog.mockResolvedValue({ success: false });
|
||||
await render();
|
||||
|
||||
@@ -15,6 +15,13 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '../components/ui/dropdown-menu';
|
||||
import { EmptyState } from '../components/ui/empty-state';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '../components/ui/select';
|
||||
import { ToggleGroup, ToggleGroupItem } from '../components/ui/toggle-group';
|
||||
import ConnectionDrawer from '../connectors/ConnectionDrawer';
|
||||
import ConnectorCard from '../connectors/ConnectorCard';
|
||||
@@ -86,6 +93,21 @@ export default function Connectors() {
|
||||
// "Connect more" in the composer opens the connectors that can do what the
|
||||
// picker is for: sync content, or give tools.
|
||||
const capability = searchParams.get('capability');
|
||||
const clearCapability = () => {
|
||||
searchParams.delete('capability');
|
||||
setSearchParams(searchParams, { replace: true });
|
||||
};
|
||||
|
||||
// Only categories that have something in them (connectors that need admin
|
||||
// setup are not listed, which can empty one).
|
||||
const filters = FILTERS.filter(
|
||||
(key) =>
|
||||
key === 'all' ||
|
||||
key === filter ||
|
||||
(key === 'connected'
|
||||
? catalog.some(isConnected)
|
||||
: catalog.some((connector) => connector.category === key)),
|
||||
);
|
||||
|
||||
const visible = useMemo(() => {
|
||||
const query = search.trim().toLowerCase();
|
||||
@@ -163,19 +185,57 @@ export default function Connectors() {
|
||||
</DropdownMenu>
|
||||
}
|
||||
>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={filter}
|
||||
onValueChange={(value) => value && setFilter(value as Filter)}
|
||||
aria-label={t('settings.connectors.categoriesLabel')}
|
||||
className="mb-6"
|
||||
>
|
||||
{FILTERS.map((key) => (
|
||||
<ToggleGroupItem key={key} value={key}>
|
||||
{t(`settings.connectors.categories.${key}`)}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
<div className="mb-6 flex flex-col gap-3">
|
||||
{/* Nine pills wrap to four lines on a phone: a Select there. */}
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={filter}
|
||||
onValueChange={(value) => value && setFilter(value as Filter)}
|
||||
aria-label={t('settings.connectors.categoriesLabel')}
|
||||
className="hidden sm:flex"
|
||||
>
|
||||
{filters.map((key) => (
|
||||
<ToggleGroupItem key={key} value={key}>
|
||||
{t(`settings.connectors.categories.${key}`)}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
<div className="sm:hidden">
|
||||
<Select
|
||||
value={filter}
|
||||
onValueChange={(value) => setFilter(value as Filter)}
|
||||
>
|
||||
<SelectTrigger
|
||||
size="field"
|
||||
shape="pill"
|
||||
className="w-full"
|
||||
aria-label={t('settings.connectors.categoriesLabel')}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{filters.map((key) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{t(`settings.connectors.categories.${key}`)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{(capability === 'sync' || capability === 'tools') && (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t(`settings.connectors.capabilityFilter.${capability}`)}{' '}
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
size="inline"
|
||||
onClick={clearCapability}
|
||||
>
|
||||
{t('settings.connectors.capabilityFilter.showAll')}
|
||||
</Button>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</PageToolbar>
|
||||
|
||||
{!loaded && !failed ? (
|
||||
@@ -191,6 +251,7 @@ export default function Connectors() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
shape="pill"
|
||||
onClick={() => dispatch(loadConnectors({ token }))}
|
||||
>
|
||||
{t('retry')}
|
||||
@@ -198,13 +259,15 @@ export default function Connectors() {
|
||||
}
|
||||
/>
|
||||
) : visible.length === 0 ? (
|
||||
<EmptyState
|
||||
title={
|
||||
filter === 'connected' && !search
|
||||
? t('settings.connectors.empty')
|
||||
: t('settings.connectors.noMatches')
|
||||
}
|
||||
/>
|
||||
filter === 'connected' && !search ? (
|
||||
<EmptyState title={t('settings.connectors.empty')} />
|
||||
) : (
|
||||
<EmptyState
|
||||
size="xs"
|
||||
illustration="none"
|
||||
title={t('settings.connectors.noMatches')}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{visible.map((connector) => (
|
||||
|
||||
@@ -27,6 +27,11 @@ import { Card, CardFooter, CardTitle } from '../components/ui/card';
|
||||
import { ActionMenu, type MenuOption } from '../components/ui/dropdown-menu';
|
||||
import { EmptyState } from '../components/ui/empty-state';
|
||||
import { Pagination } from '../components/ui/pagination';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '../components/ui/tooltip';
|
||||
import { useDebouncedValue, useLoaderState } from '../hooks';
|
||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||
import { ActiveState, Doc, DocumentsProps } from '../models/misc';
|
||||
@@ -662,15 +667,19 @@ export default function Sources({
|
||||
</Badge>
|
||||
)}
|
||||
{connection && paused && (
|
||||
<Badge
|
||||
variant="warning"
|
||||
title={t('settings.sources.paused', {
|
||||
name: connection.name,
|
||||
interpolation: { escapeValue: false },
|
||||
})}
|
||||
>
|
||||
{t('settings.connectors.detail.paused')}
|
||||
</Badge>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge variant="warning" tabIndex={0}>
|
||||
{t('settings.connectors.detail.paused')}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t('settings.sources.paused', {
|
||||
name: connection.name,
|
||||
interpolation: { escapeValue: false },
|
||||
})}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{document.ingestStatus === 'failed' && (
|
||||
<Badge variant="destructive">
|
||||
@@ -726,7 +735,7 @@ export default function Sources({
|
||||
title={connection.account_label}
|
||||
>
|
||||
{t('settings.tools.viaConnection', {
|
||||
name: connection.account_label,
|
||||
name: connection.name,
|
||||
interpolation: { escapeValue: false },
|
||||
})}
|
||||
</span>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Pencil, RefreshCw, Trash2, Users } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import devicesService from '../api/services/devicesService';
|
||||
import userService from '../api/services/userService';
|
||||
@@ -12,7 +12,12 @@ import SkeletonLoader from '../components/SkeletonLoader';
|
||||
import ToolIcon from '../components/ToolIcon';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { Card, CardDescription, CardTitle } from '../components/ui/card';
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
} from '../components/ui/card';
|
||||
import { Switch } from '../components/ui/switch';
|
||||
import { ActionMenu, type MenuOption } from '../components/ui/dropdown-menu';
|
||||
import { EmptyState } from '../components/ui/empty-state';
|
||||
@@ -413,37 +418,6 @@ export default function Tools() {
|
||||
>
|
||||
{tool.customName || tool.displayName}
|
||||
</CardTitle>
|
||||
{(() => {
|
||||
const connection = tool.connection_id
|
||||
? connections.find(
|
||||
(c) => c.id === tool.connection_id,
|
||||
)
|
||||
: undefined;
|
||||
if (!connection) return null;
|
||||
return (
|
||||
<Button
|
||||
variant="link"
|
||||
size="inline"
|
||||
asChild
|
||||
className="mt-1 max-w-full justify-start"
|
||||
>
|
||||
<Link
|
||||
to={`/settings/connectors?connector=${encodeURIComponent(connection.connector_key)}`}
|
||||
>
|
||||
<ConnectorIcon
|
||||
icon={connection.icon}
|
||||
className="size-3.5 shrink-0"
|
||||
/>
|
||||
<span className="truncate">
|
||||
{t('settings.tools.viaConnection', {
|
||||
name: connection.name,
|
||||
interpolation: { escapeValue: false },
|
||||
})}
|
||||
</span>
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
})()}
|
||||
<CardDescription
|
||||
size="xs"
|
||||
className="mt-1 line-clamp-4 max-h-24 overflow-hidden break-words"
|
||||
@@ -453,6 +427,35 @@ export default function Tools() {
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
{(() => {
|
||||
const connection = tool.connection_id
|
||||
? connections.find(
|
||||
(c) => c.id === tool.connection_id,
|
||||
)
|
||||
: undefined;
|
||||
if (!connection) return null;
|
||||
// The same meta line as a Sources tile; mr-12 keeps
|
||||
// it clear of the status switch.
|
||||
return (
|
||||
<CardFooter>
|
||||
<span className="mr-12 flex min-w-0 items-center gap-2">
|
||||
<ConnectorIcon
|
||||
icon={connection.icon}
|
||||
className="size-3.5 shrink-0"
|
||||
/>
|
||||
<span
|
||||
className="truncate"
|
||||
title={connection.account_label}
|
||||
>
|
||||
{t('settings.tools.viaConnection', {
|
||||
name: connection.name,
|
||||
interpolation: { escapeValue: false },
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
</CardFooter>
|
||||
);
|
||||
})()}
|
||||
<div className="absolute right-4 bottom-4">
|
||||
<Switch
|
||||
checked={tool.status}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { CircleAlert, Trash2, UserRound, UsersRound } from 'lucide-react';
|
||||
import {
|
||||
CircleAlert,
|
||||
Trash2,
|
||||
TriangleAlert,
|
||||
UserRound,
|
||||
UsersRound,
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@@ -580,7 +586,7 @@ export default function ShareToTeamModal({
|
||||
)}
|
||||
{credentialMode === 'owner' ? (
|
||||
<Alert variant="warning" role="note">
|
||||
<CircleAlert aria-hidden="true" />
|
||||
<TriangleAlert aria-hidden="true" />
|
||||
<AlertDescription>
|
||||
{t('settings.connectors.share.ownerWarning', {
|
||||
account: credentials.account,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChevronLeft, CircleAlert, FileText } from 'lucide-react';
|
||||
import { ChevronLeft, FileText } from 'lucide-react';
|
||||
import { envVar } from '@/env';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
@@ -28,8 +28,9 @@ import { Modal } from '../components/ui/modal';
|
||||
import { Separator } from '../components/ui/separator';
|
||||
import { OptionCard } from '../components/ui/option-card';
|
||||
import { SectionHeader } from '../components/ui/section-header';
|
||||
import { Alert, AlertDescription, AlertTitle } from '../components/ui/alert';
|
||||
import ConnectorIcon from '../connectors/ConnectorIcon';
|
||||
import ConnectorSetupNotice from '../connectors/ConnectorSetupNotice';
|
||||
import { formatCount } from '../utils/dateTimeUtils';
|
||||
import {
|
||||
loadConnectors,
|
||||
selectConnections,
|
||||
@@ -1194,6 +1195,7 @@ function Upload({
|
||||
if (accounts.length > 1)
|
||||
return t('settings.connectors.status.connectedCount', {
|
||||
count: accounts.length,
|
||||
formatted: formatCount(accounts.length),
|
||||
});
|
||||
return t('settings.connectors.status.connect');
|
||||
};
|
||||
@@ -1274,26 +1276,7 @@ function Upload({
|
||||
|
||||
const renderSetupNotice = () =>
|
||||
selectedConnector && needsSetup ? (
|
||||
<Alert variant="warning">
|
||||
<CircleAlert />
|
||||
<AlertTitle>
|
||||
{t('settings.connectors.status.needsAdminSetup')}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<div className="flex flex-col gap-2">
|
||||
{selectedConnector.missing_settings.length > 0 ? (
|
||||
<>
|
||||
<span>{t('settings.connectors.setupSettings')}</span>
|
||||
<code className="font-mono text-xs wrap-anywhere">
|
||||
{selectedConnector.missing_settings.join(', ')}
|
||||
</code>
|
||||
</>
|
||||
) : (
|
||||
<span>{t('settings.connectors.askAdmin')}</span>
|
||||
)}
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<ConnectorSetupNotice connector={selectedConnector} />
|
||||
) : null;
|
||||
return (
|
||||
<Modal
|
||||
|
||||
Reference in new issue
Block a user