mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
Merge remote-tracking branch 'private/cloud/integration' into cloud/multiplayer-gaps
This commit is contained in:
@@ -132,6 +132,12 @@ export type Env = {
|
||||
|
||||
/** Operator routes (/v1/internal/*): kill switch, drain, bindings. Secret, >= 32 chars; unset = no routes. */
|
||||
OPERATOR_TOKEN?: string
|
||||
/**
|
||||
* Auth's bearer for POST /v1/internal/org-members/removed only (PropelAuth offboarding and
|
||||
* Orca-side removals). Secret, >= 32 chars; lets auth push removals without holding the
|
||||
* operator token. The operator token is still accepted on that route.
|
||||
*/
|
||||
MEMBER_REMOVED_TOKEN?: string
|
||||
/** `true` refuses every create/resume/rollover (static kill switch; the D1 flag is the runtime one). */
|
||||
SANDBOX_CREATES_DISABLED?: string
|
||||
/** Warm pool size (pool.ts); unset or 0 = off. */
|
||||
|
||||
@@ -1150,7 +1150,12 @@ export async function handleRequest(request: Request, env: Env, deps: Deps = {})
|
||||
return await internalInstallUrl(env, deps, url)
|
||||
}
|
||||
if (url.pathname.startsWith('/v1/internal/')) {
|
||||
if (!(await isOperator(request, env.OPERATOR_TOKEN))) {
|
||||
// Auth's member-removed push holds a token good for that one route, not the operator token.
|
||||
const memberRemovedOnly =
|
||||
url.pathname === '/v1/internal/org-members/removed' &&
|
||||
request.method === 'POST' &&
|
||||
(await isOperator(request, env.MEMBER_REMOVED_TOKEN))
|
||||
if (!memberRemovedOnly && !(await isOperator(request, env.OPERATOR_TOKEN))) {
|
||||
return fail('not_found', 404)
|
||||
}
|
||||
const life = await lifecycleDeps(env, deps)
|
||||
|
||||
@@ -180,3 +180,18 @@ describe('pokes', () => {
|
||||
expect((await t.req(solo, 'GET', '/v1/team/pokes')).body).toEqual({ enabled: false, pokes: [] })
|
||||
})
|
||||
})
|
||||
|
||||
describe('member-removed token', () => {
|
||||
it('authorizes only POST /v1/internal/org-members/removed, never other operator routes', async () => {
|
||||
const MR = 'm'.repeat(40)
|
||||
const body = { accountId: 'org:acme', userId: 'bob' }
|
||||
const h = await createHarness({ OPERATOR_TOKEN: 'o'.repeat(40), MEMBER_REMOVED_TOKEN: MR })
|
||||
expect((await h.call('POST', '/v1/internal/org-members/removed', { bearer: MR, body })).status).toBe(200)
|
||||
expect((await h.call('GET', '/v1/internal/status', { bearer: MR })).status).toBe(404)
|
||||
expect((await h.call('POST', '/v1/internal/members/removed', { bearer: MR, body })).status).toBe(404)
|
||||
// The operator token still works on the route; a short token is never accepted.
|
||||
expect((await h.call('POST', '/v1/internal/org-members/removed', { bearer: 'o'.repeat(40), body })).status).toBe(200)
|
||||
const weak = await createHarness({ OPERATOR_TOKEN: 'o'.repeat(40), MEMBER_REMOVED_TOKEN: 'short' })
|
||||
expect((await weak.call('POST', '/v1/internal/org-members/removed', { bearer: 'short', body })).status).toBe(404)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useTranslate, type Translate } from '@/i18n/use-translate'
|
||||
import { useAppStore } from '@/store/index'
|
||||
import { OrcaProfileSignOutConfirmDialog } from '../../../orca-profiles/OrcaProfileSignOutConfirmDialog'
|
||||
import { OrcaSignInButton, OrcaSignInWaitNotice } from '../../../OrcaSignInProgress'
|
||||
import { OrcaCloudSettingsLinks } from '../../orca-cloud/OrcaCloudSettingsLinks'
|
||||
import { useOrcaCloudApiAvailable } from '@/hooks/useOrcaCloudApiAvailability'
|
||||
|
||||
function accountStatusCopy(
|
||||
@@ -68,7 +67,6 @@ export function OrcaAccountSettingsPane(): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const authStatus = useAppStore((state) => state.orcaProfileAuthStatus)
|
||||
const signOut = useAppStore((state) => state.signOutCurrentOrcaProfile)
|
||||
const openSettingsTarget = useAppStore((state) => state.openSettingsTarget)
|
||||
const cloudAvailable = useOrcaCloudApiAvailable()
|
||||
const [signOutOpen, setSignOutOpen] = useState(false)
|
||||
const [signingOut, setSigningOut] = useState(false)
|
||||
@@ -133,51 +131,54 @@ export function OrcaAccountSettingsPane(): React.JSX.Element {
|
||||
{connected ? null : <OrcaSignInWaitNotice className="basis-full" />}
|
||||
</div>
|
||||
|
||||
{connected && cloudAvailable ? (
|
||||
<OrcaCloudSettingsLinks onOpen={(pane) => openSettingsTarget({ pane, repoId: null })} />
|
||||
) : null}
|
||||
|
||||
<div className="space-y-4 border-t border-border/60 pt-5">
|
||||
<SettingsGroupHeader
|
||||
className="px-0"
|
||||
title={t(
|
||||
'auto.components.settings.orcaAccount.benefitsTitle',
|
||||
'Included with your account'
|
||||
)}
|
||||
/>
|
||||
<div className="space-y-5">
|
||||
<div className="grid gap-5 md:grid-cols-2 md:gap-0 md:divide-x md:divide-border/60">
|
||||
{/* JA-16: with Orca Cloud the page is identity + Sign out only; plan, team and agents have
|
||||
their own entries in the Orca Cloud group right beside it. */}
|
||||
{cloudAvailable ? null : (
|
||||
<div className="space-y-4 border-t border-border/60 pt-5">
|
||||
<SettingsGroupHeader
|
||||
className="px-0"
|
||||
title={t(
|
||||
'auto.components.settings.orcaAccount.benefitsTitle',
|
||||
'Included with your account'
|
||||
)}
|
||||
/>
|
||||
<div className="space-y-5">
|
||||
<div className="grid gap-5 md:grid-cols-2 md:gap-0 md:divide-x md:divide-border/60">
|
||||
<AccountBenefit
|
||||
icon={Files}
|
||||
className="md:pr-6"
|
||||
title={t(
|
||||
'auto.components.settings.orcaAccount.artifactsTitle',
|
||||
'Artifact sharing'
|
||||
)}
|
||||
description={t(
|
||||
'auto.components.settings.orcaAccount.artifactsDescription',
|
||||
'Publish HTML and Markdown files, then manage every shared link from Orca.'
|
||||
)}
|
||||
/>
|
||||
<AccountBenefit
|
||||
icon={Smartphone}
|
||||
className="md:pl-6"
|
||||
title={t('auto.components.settings.orcaAccount.relayTitle', 'Orca Relay')}
|
||||
description={t(
|
||||
'auto.components.settings.orcaAccount.relayDescription',
|
||||
'Connect Orca Mobile to this desktop across cellular or any Wi-Fi.'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* Put the third benefit below at full width so it cannot squeeze the existing pair. */}
|
||||
<AccountBenefit
|
||||
icon={Files}
|
||||
className="md:pr-6"
|
||||
title={t('auto.components.settings.orcaAccount.artifactsTitle', 'Artifact sharing')}
|
||||
icon={BookOpen}
|
||||
className="border-t border-border/60 pt-5"
|
||||
title={t('auto.components.settings.orcaAccount.skillsTitle', 'Skill sharing')}
|
||||
description={t(
|
||||
'auto.components.settings.orcaAccount.artifactsDescription',
|
||||
'Publish HTML and Markdown files, then manage every shared link from Orca.'
|
||||
)}
|
||||
/>
|
||||
<AccountBenefit
|
||||
icon={Smartphone}
|
||||
className="md:pl-6"
|
||||
title={t('auto.components.settings.orcaAccount.relayTitle', 'Orca Relay')}
|
||||
description={t(
|
||||
'auto.components.settings.orcaAccount.relayDescription',
|
||||
'Connect Orca Mobile to this desktop across cellular or any Wi-Fi.'
|
||||
'auto.components.settings.orcaAccount.skillsDescription',
|
||||
'Share one skill or a whole set behind an unlisted link, and install them on any machine you use.'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* Put the third benefit below at full width so it cannot squeeze the existing pair. */}
|
||||
<AccountBenefit
|
||||
icon={BookOpen}
|
||||
className="border-t border-border/60 pt-5"
|
||||
title={t('auto.components.settings.orcaAccount.skillsTitle', 'Skill sharing')}
|
||||
description={t(
|
||||
'auto.components.settings.orcaAccount.skillsDescription',
|
||||
'Share one skill or a whole set behind an unlisted link, and install them on any machine you use.'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<OrcaProfileSignOutConfirmDialog
|
||||
|
||||
@@ -21,14 +21,13 @@ import {
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { useIntlLocale, useTranslate } from '@/i18n/use-translate'
|
||||
import { useIntlLocale, useTranslate, type Translate } from '@/i18n/use-translate'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type {
|
||||
CloudApiProject,
|
||||
Routine,
|
||||
RoutineRun
|
||||
} from '../../../../../shared/cloud-api/cloud-api-types'
|
||||
import { PersonSentence } from './PersonName'
|
||||
import { RoutineRunHistory, type LoadRoutineRuns } from './RoutineRunHistory'
|
||||
import {
|
||||
describeRoutineTrigger,
|
||||
@@ -62,7 +61,8 @@ export function RoutineRow({
|
||||
onLatestRun,
|
||||
onOpenWorkspace,
|
||||
fixedNow,
|
||||
viewerUserId
|
||||
viewerUserId,
|
||||
memberNames
|
||||
}: {
|
||||
routine: Routine
|
||||
projects: readonly CloudApiProject[]
|
||||
@@ -79,6 +79,7 @@ export function RoutineRow({
|
||||
onOpenWorkspace?: (workspaceId: string) => void
|
||||
fixedNow?: number
|
||||
viewerUserId: string | null
|
||||
memberNames?: ReadonlyMap<string, string>
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const locale = useIntlLocale()
|
||||
@@ -164,14 +165,9 @@ export function RoutineRow({
|
||||
{viewerUserId && routine.createdBy !== viewerUserId ? (
|
||||
<span className="text-muted-foreground">
|
||||
{' · '}
|
||||
<PersonSentence
|
||||
sentence={t(
|
||||
'auto.components.settings.cloudRoutines.createdByPerson',
|
||||
'by <person></person>'
|
||||
)}
|
||||
name={routine.createdByName}
|
||||
id={routine.createdBy}
|
||||
/>
|
||||
{t('auto.components.settings.cloudRoutines.createdByName', 'by {{name}}', {
|
||||
name: creatorName(routine, memberNames, t)
|
||||
})}
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
@@ -321,3 +317,16 @@ export function RoutineRow({
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
/** Display name only, never a raw id: the server's name, then the team roster, then "Former member". */
|
||||
export function creatorName(
|
||||
routine: Pick<Routine, 'createdBy' | 'createdByName'>,
|
||||
memberNames: ReadonlyMap<string, string> | undefined,
|
||||
t: Translate
|
||||
): string {
|
||||
return (
|
||||
routine.createdByName?.trim() ||
|
||||
memberNames?.get(routine.createdBy)?.trim() ||
|
||||
t('auto.components.settings.cloudRoutines.formerMember', 'Former member')
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { localTimeZone } from './cloud-api-format'
|
||||
import { toCloudLoadState, type CloudApiFailure, type CloudLoadState } from './cloud-load-state'
|
||||
import { useOrcaCloudApiAvailability } from '@/hooks/useOrcaCloudApiAvailability'
|
||||
import { useRequestGeneration } from './use-request-generation'
|
||||
import { useOrgRoster } from '../../billing/team/use-org-roster'
|
||||
|
||||
const RUNS_PAGE_SIZE = 20
|
||||
|
||||
@@ -48,6 +49,20 @@ export function CloudRoutinesPane(): React.JSX.Element | null {
|
||||
const [installations, setInstallations] = useState<CloudApiInstallation[] | null>(null)
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
const timezone = useMemo(() => localTimeZone(), [])
|
||||
// Creators the server can't name come from the team roster, the same directory Home uses.
|
||||
const orgId = useAppStore((s) => s.orcaProfileAuthStatus?.cloud?.activeOrgId ?? null)
|
||||
const roster = useOrgRoster(orgId)
|
||||
const memberNames = useMemo(() => {
|
||||
const names = new Map<string, string>()
|
||||
if (roster.result?.status === 'ok') {
|
||||
for (const m of roster.result.roster.members) {
|
||||
if (m.userId && (m.displayName || m.email)) {
|
||||
names.set(m.userId, m.displayName || m.email)
|
||||
}
|
||||
}
|
||||
}
|
||||
return names
|
||||
}, [roster.result])
|
||||
const word = useCallback(
|
||||
(failure: CloudApiFailure) => cloudApiErrorCopy(failure.error, t, failure),
|
||||
[t]
|
||||
@@ -126,6 +141,7 @@ export function CloudRoutinesPane(): React.JSX.Element | null {
|
||||
onRefresh={() => void load()}
|
||||
refreshing={refreshing}
|
||||
viewerUserId={viewerUserId}
|
||||
memberNames={memberNames}
|
||||
onOpenWorkspace={(sandboxId) => void openRunWorkspace(sandboxId)}
|
||||
onLatestRun={(routine, run) => {
|
||||
// Keep the row's pill in step with the newest run its open history just fetched.
|
||||
|
||||
@@ -48,6 +48,8 @@ export type CloudRoutinesViewProps = {
|
||||
fixtureNow?: number
|
||||
/** Routines someone else created name their creator; null while unknown. */
|
||||
viewerUserId?: string | null
|
||||
/** Team roster names by user id (the same directory Home uses), for creators the server can't name. */
|
||||
memberNames?: ReadonlyMap<string, string>
|
||||
/** A refresh is running while the current list stays on screen. */
|
||||
refreshing?: boolean
|
||||
/** Fixtures: start with the editor, history or delete confirmation open. */
|
||||
@@ -183,6 +185,7 @@ export function CloudRoutinesView(props: CloudRoutinesViewProps): React.JSX.Elem
|
||||
onOpenWorkspace={props.onOpenWorkspace}
|
||||
fixedNow={props.fixtureNow}
|
||||
viewerUserId={props.viewerUserId ?? null}
|
||||
memberNames={props.memberNames}
|
||||
onRunNow={async () => {
|
||||
const failure = await props.onRunNow(routine)
|
||||
if (!failure) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { CLOUD_API_SCOPES } from '../../../../../shared/cloud-api/cloud-api-types'
|
||||
import { creatorName } from './CloudRoutineRow'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import {
|
||||
cloudApiErrorCopy,
|
||||
describeRoutineRunError,
|
||||
@@ -137,3 +139,21 @@ describe('run failure tone', () => {
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('routine creator', () => {
|
||||
it('shows a display name only, never a raw id', () => {
|
||||
const roster = new Map([['u_lin', 'Lin Chen']])
|
||||
expect(
|
||||
creatorName({ createdBy: 'u_grace', createdByName: 'Grace Hopper' }, roster, translate)
|
||||
).toBe('Grace Hopper')
|
||||
expect(creatorName({ createdBy: 'u_lin', createdByName: null }, roster, translate)).toBe(
|
||||
'Lin Chen'
|
||||
)
|
||||
expect(creatorName({ createdBy: 'u_7f3k', createdByName: ' ' }, roster, translate)).toBe(
|
||||
'Former member'
|
||||
)
|
||||
expect(creatorName({ createdBy: 'u_7f3k', createdByName: null }, undefined, translate)).toBe(
|
||||
'Former member'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,7 +9,6 @@ import { FIXTURE_NAMES, fixtureCloudSecretsApi } from '../cloud-secrets/cloud-se
|
||||
import { CloudMcpSyncPanel } from '../runtime-environments/cloud-mcp/CloudMcpSyncPanel'
|
||||
import { SettingsSection } from '../section-layout/SettingsSection'
|
||||
import { OrcaCloudMcpServersView, type OrcaCloudMcpWorkspace } from './OrcaCloudMcpServersPane'
|
||||
import { OrcaCloudSettingsLinks } from './OrcaCloudSettingsLinks'
|
||||
import { OrcaCloudSignedOutNotice } from './OrcaCloudSettingsPanes'
|
||||
|
||||
const noop = (): void => {}
|
||||
@@ -197,16 +196,6 @@ export const sessionExpired = defineFixture({
|
||||
)
|
||||
})
|
||||
|
||||
/** Settings › Orca account, signed in: the Orca Cloud panes are one click away. */
|
||||
export const accountLinks = defineFixture({
|
||||
width: 720,
|
||||
render: () => (
|
||||
<div className="bg-background p-6">
|
||||
<OrcaCloudSettingsLinks onOpen={noop} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
/** Settings › Agent API keys header: desktop keys are not for Orca Cloud; link to Cloud secrets. */
|
||||
export const agentApiKeysHeader = defineFixture({
|
||||
width: 784,
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { Boxes, ChevronRight, CreditCard, LockKeyhole, Plug, Users } from 'lucide-react'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import { SettingsGroupHeader } from '@/components/settings/section-layout/SettingsGroup'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import type { SettingsNavTarget } from '@/lib/settings-navigation-types'
|
||||
|
||||
type OrcaCloudLink = { pane: SettingsNavTarget; icon: LucideIcon; label: string }
|
||||
|
||||
/** Settings › Orca account: the Orca Cloud panes live in their own group; this points at them. */
|
||||
export function OrcaCloudSettingsLinks({
|
||||
onOpen
|
||||
}: {
|
||||
onOpen: (pane: SettingsNavTarget) => void
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
const links: OrcaCloudLink[] = [
|
||||
{
|
||||
pane: 'cloud-billing',
|
||||
icon: CreditCard,
|
||||
label: t('auto.components.billing.title', 'Plan & billing')
|
||||
},
|
||||
{ pane: 'cloud-team', icon: Users, label: t('auto.components.billing.team.title', 'Team') },
|
||||
{ pane: 'cloud-secrets', icon: LockKeyhole, label: t('cloudSecrets.title', 'Cloud secrets') },
|
||||
{
|
||||
pane: 'cloud-environments',
|
||||
icon: Boxes,
|
||||
label: t('settings.orcaCloud.environments.title', 'Cloud environments')
|
||||
},
|
||||
{ pane: 'cloud-mcp', icon: Plug, label: t('cloudMcpSync.title', 'MCP servers') }
|
||||
]
|
||||
return (
|
||||
<nav
|
||||
className="space-y-3 border-t border-border/60 pt-5"
|
||||
aria-label={t('settings.groups.orcaCloud', 'Orca Cloud')}
|
||||
>
|
||||
<SettingsGroupHeader className="px-0" title={t('settings.groups.orcaCloud', 'Orca Cloud')} />
|
||||
<ul className="divide-y divide-border/60 overflow-hidden rounded-lg border border-border bg-card shadow-xs">
|
||||
{links.map(({ pane, icon: Icon, label }) => (
|
||||
<li key={pane}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpen(pane)}
|
||||
className="flex w-full items-center gap-3 px-3 py-2.5 text-left text-sm transition-colors hover:bg-accent focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus-ring"
|
||||
>
|
||||
<Icon aria-hidden className="size-4 shrink-0 text-muted-foreground" />
|
||||
<span className="min-w-0 flex-1 truncate">{label}</span>
|
||||
<ChevronRight aria-hidden className="size-4 text-muted-foreground" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
+1
-4
@@ -24,10 +24,7 @@ export function renderOrcaAccountSettingsSection(
|
||||
title={t('auto.components.settings.orcaAccount.title', 'Orca account')}
|
||||
description={
|
||||
model.orcaCloudApiAvailable
|
||||
? t(
|
||||
'settings.orcaAccount.descriptionWithCloud',
|
||||
'Sign-in, plan, team and Orca Cloud settings.'
|
||||
)
|
||||
? t('settings.orcaAccount.descriptionIdentity', 'The Orca account you’re signed in with.')
|
||||
: t(
|
||||
'auto.components.settings.orcaAccount.description',
|
||||
'Share work instantly and reach your desktop from Orca Mobile wherever you are.'
|
||||
|
||||
@@ -170,8 +170,8 @@ export function buildSetupSettingsSections(
|
||||
title: t('auto.components.settings.orcaAccount.title', 'Orca account'),
|
||||
description: isOrcaCloudApiAvailable
|
||||
? t(
|
||||
'settings.orcaAccount.descriptionWithCloud',
|
||||
'Sign-in, plan, team and Orca Cloud settings.'
|
||||
'settings.orcaAccount.descriptionIdentity',
|
||||
'The Orca account you’re signed in with.'
|
||||
)
|
||||
: t(
|
||||
'auto.components.settings.orcaAccount.description',
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Bot } from 'lucide-react'
|
||||
import type { SettingsNavSection } from '@/lib/settings-navigation-types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildSetupSettingsSections } from './settings-navigation-capability-sections'
|
||||
import type { SettingsNavigationBuildOptions } from './settings-navigation-build-options'
|
||||
import { buildOrcaCloudSettingsSections } from './settings-navigation-orca-cloud-sections'
|
||||
import {
|
||||
buildOrcaCloudSettingsSections,
|
||||
gatherOrcaCloudSettingsGroup
|
||||
} from './settings-navigation-orca-cloud-sections'
|
||||
|
||||
function options(patch: Partial<SettingsNavigationBuildOptions>): SettingsNavigationBuildOptions {
|
||||
return {
|
||||
@@ -45,7 +50,7 @@ describe('Orca Cloud settings group', () => {
|
||||
const setup = buildSetupSettingsSections(options({}))
|
||||
expect(setup.some((section) => section.id.startsWith('cloud-'))).toBe(false)
|
||||
expect(setup.find((section) => section.id === 'orca-account')?.description).toBe(
|
||||
'Sign-in, plan, team and Orca Cloud settings.'
|
||||
'The Orca account you’re signed in with.'
|
||||
)
|
||||
const local = buildSetupSettingsSections(options({ isOrcaCloudApiAvailable: false }))
|
||||
expect(local.find((section) => section.id === 'orca-account')?.description).not.toMatch(
|
||||
@@ -53,3 +58,44 @@ describe('Orca Cloud settings group', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('one Orca Cloud group (JA-16)', () => {
|
||||
const nav = (id: string, group: SettingsNavSection['group']): SettingsNavSection => ({
|
||||
id,
|
||||
title: id,
|
||||
description: '',
|
||||
icon: Bot,
|
||||
searchEntries: [],
|
||||
group
|
||||
})
|
||||
|
||||
const all = [
|
||||
nav('agents', 'capabilities'),
|
||||
nav('accounts', 'capabilities'),
|
||||
nav('orca-account', 'setup'),
|
||||
nav('general', 'setup'),
|
||||
nav('cloud-billing', 'orca-cloud'),
|
||||
nav('cloud-team', 'orca-cloud'),
|
||||
nav('automations', 'workflows')
|
||||
]
|
||||
|
||||
it('leads the Orca Cloud group with the Orca account; Agent accounts stays in AI capabilities', () => {
|
||||
const out = gatherOrcaCloudSettingsGroup(all, options({}))
|
||||
expect(out.map((section) => `${section.group}:${section.id}`)).toEqual([
|
||||
'capabilities:agents',
|
||||
'capabilities:accounts',
|
||||
'setup:general',
|
||||
'orca-cloud:orca-account',
|
||||
'orca-cloud:cloud-billing',
|
||||
'orca-cloud:cloud-team',
|
||||
'workflows:automations'
|
||||
])
|
||||
})
|
||||
|
||||
it('leaves the upstream grouping alone without Orca Cloud', () => {
|
||||
expect(gatherOrcaCloudSettingsGroup(all, options({ isOrcaCloudApiAvailable: false }))).toEqual(
|
||||
all
|
||||
)
|
||||
expect(gatherOrcaCloudSettingsGroup(all, options({ isWebClient: true }))).toEqual(all)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -104,3 +104,26 @@ export function buildOrcaCloudSettingsSections(
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* JA-16: in Orca Cloud builds the Orca account leads the one Orca Cloud group instead of sitting in
|
||||
* Set up. Agent accounts stays in AI capabilities: it covers this computer and hosts as much as
|
||||
* Orca Cloud, and its Orca Cloud part is deep-linked from every cloud entry point.
|
||||
*/
|
||||
export function gatherOrcaCloudSettingsGroup(
|
||||
sections: readonly SettingsNavSection[],
|
||||
{ isWebClient, isOrcaCloudApiAvailable }: SettingsNavigationBuildOptions
|
||||
): SettingsNavSection[] {
|
||||
if (isWebClient || !isOrcaCloudApiAvailable) {
|
||||
return [...sections]
|
||||
}
|
||||
const leadIds = ['orca-account']
|
||||
const leads = leadIds
|
||||
.map((id) => sections.find((section) => section.id === id))
|
||||
.filter((section): section is SettingsNavSection => section !== undefined)
|
||||
.map((section) => ({ ...section, group: 'orca-cloud' as const }))
|
||||
const rest = sections.filter((section) => !leadIds.includes(section.id))
|
||||
const firstCloud = rest.findIndex((section) => section.group === 'orca-cloud')
|
||||
const at = firstCloud === -1 ? rest.length : firstCloud
|
||||
return [...rest.slice(0, at), ...leads, ...rest.slice(at)]
|
||||
}
|
||||
|
||||
@@ -31,7 +31,10 @@ import {
|
||||
buildSetupSettingsSections
|
||||
} from './settings-navigation-capability-sections'
|
||||
import type { SettingsNavigationBuildOptions } from './settings-navigation-build-options'
|
||||
import { buildOrcaCloudSettingsSections } from './settings-navigation-orca-cloud-sections'
|
||||
import {
|
||||
buildOrcaCloudSettingsSections,
|
||||
gatherOrcaCloudSettingsGroup
|
||||
} from './settings-navigation-orca-cloud-sections'
|
||||
import { buildInterfaceSettingsSections } from './settings-navigation-interface-sections'
|
||||
import { buildRemoteSettingsSections } from './settings-navigation-remote-sections'
|
||||
import { buildWorkflowSettingsSections } from './settings-navigation-workflow-sections'
|
||||
@@ -110,14 +113,17 @@ export function buildSettingsNavigationMetadata(
|
||||
}
|
||||
|
||||
// Match SETTINGS_NAV_GROUPS order so Settings and Cmd+J keep siblings together.
|
||||
return [
|
||||
...buildCapabilitySettingsSections(options, t),
|
||||
...buildSetupSettingsSections(options, t),
|
||||
...buildOrcaCloudSettingsSections(options, t),
|
||||
...buildWorkflowSettingsSections(options, terminalPaneSearchEntries, t),
|
||||
...buildInterfaceSettingsSections(options, t),
|
||||
...buildRemoteSettingsSections(options, runtimeEnvironmentsSearchEntry, reposById, t)
|
||||
]
|
||||
return gatherOrcaCloudSettingsGroup(
|
||||
[
|
||||
...buildCapabilitySettingsSections(options, t),
|
||||
...buildSetupSettingsSections(options, t),
|
||||
...buildOrcaCloudSettingsSections(options, t),
|
||||
...buildWorkflowSettingsSections(options, terminalPaneSearchEntries, t),
|
||||
...buildInterfaceSettingsSections(options, t),
|
||||
...buildRemoteSettingsSections(options, runtimeEnvironmentsSearchEntry, reposById, t)
|
||||
],
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
export function useSettingsNavigationMetadata(): SettingsNavSection[] {
|
||||
|
||||
+3
@@ -2478,6 +2478,9 @@
|
||||
"sendingNotConfigured": "sending diagnostics is not configured for this build"
|
||||
}
|
||||
},
|
||||
"groups": {
|
||||
"orcaCloud": "Orca Cloud"
|
||||
},
|
||||
"mcp": {
|
||||
"serverCount_one": "{{count}} server",
|
||||
"serverCount_other": "{{count}} servers"
|
||||
|
||||
@@ -1397,7 +1397,7 @@
|
||||
"orcaCloud": "Orca Cloud"
|
||||
},
|
||||
"orcaAccount": {
|
||||
"descriptionWithCloud": "Sign-in, plan, team and Orca Cloud settings."
|
||||
"descriptionIdentity": "The Orca account you’re signed in with."
|
||||
},
|
||||
"agentAccounts": {
|
||||
"searchTitle": "Agent accounts",
|
||||
@@ -14603,7 +14603,8 @@
|
||||
},
|
||||
"cloudRoutines": {
|
||||
"pausedSr": "(paused)",
|
||||
"createdByPerson": "by <person></person>",
|
||||
"createdByName": "by {{name}}",
|
||||
"formerMember": "Former member",
|
||||
"lastRun": "Last run:",
|
||||
"neverRun": "Never run",
|
||||
"nextRun": "Next run {{time}}",
|
||||
|
||||
Reference in New Issue
Block a user