mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 08:03:09 +00:00
@@ -84,9 +84,9 @@ Done means: scheduled or manual automations behave predictably when a project ex
|
||||
|
||||
## 6. Settings, Accounts, And Host Ownership
|
||||
|
||||
- [~] Clarify which settings are client-owned versus host-owned.
|
||||
- [~] Add host/account selectors to provider settings where credentials may differ by host.
|
||||
- [~] Make provider rate limits, diagnostics, and “current account” views show their host/source context.
|
||||
- [x] Clarify which settings are client-owned versus host-owned.
|
||||
- [x] Add host/account selectors to provider settings where credentials may differ by host.
|
||||
- [x] Make provider rate limits, diagnostics, and “current account” views show their host/source context.
|
||||
- [x] Audit source-control AI, agent defaults, command defaults, and model selections for whether they should be client-global or host-scoped.
|
||||
- [x] Preserve existing Windows/WSL-style setting patterns where they already solve host-specific settings.
|
||||
- [x] Ensure remote server version/capability information is visible enough to explain unavailable controls.
|
||||
@@ -258,3 +258,4 @@ Done means: reviewers and future agents can understand the vision, implementatio
|
||||
- [x] 2026-06-12: Extracted and tested external automation source availability copy so local missing tooling, disconnected SSH sources, connecting SSH sources, and connected SSH hosts with missing remote tooling get distinct states; verified with focused automation availability tests, targeted oxlint, and full `pnpm run typecheck`.
|
||||
- [x] 2026-06-12: Marked repo-id/path compatibility adapters complete after hardening the renderer project-host selector to merge repo-derived compatibility rows when hydrated project/setup arrays are empty or partial, alongside existing shared projection, persistence backfill/sync, automation context migration, and pending worktree context coverage. Verified with selector/projection tests, persistence tests, targeted oxlint, full `pnpm run typecheck`, and `git diff --check`.
|
||||
- [x] 2026-06-12: Marked the Host copy audit complete after replacing remaining visible stale SSH/project wording in paired-web project creation errors, Settings SSH search entries, AI Vault SSH-host workspace copy, and SSH shutdown errors. Verified with a stale-copy scan showing only comments/tests/technical internals, focused SSH IPC tests, targeted oxlint, full `pnpm run typecheck`, and `git diff --check`.
|
||||
- [x] 2026-06-12: Completed Settings ownership/account rows by adding a shared provider Host scope control with a Change Host action that opens Settings > Active Server from GitHub/GitLab/Linear/Jira account surfaces and GitHub/GitLab API budget panels. Existing ownership diagnostics still distinguish desktop-client versus remote-server credentials, and rate-limit panels show the budget Host scope. Verified with settings ownership, RuntimeEnvironmentsPane, provider account, source-control card, task-tracker card, Jira card, and rate-limit panel tests, targeted oxlint, full `pnpm run typecheck`, and `git diff --check`.
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useAppStore } from '@/store'
|
||||
import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
|
||||
import type { GetRateLimitResult, GitHubRateLimitSnapshot } from '../../../../shared/types'
|
||||
import { getProviderRateLimitScope } from '@/components/settings/provider-account-scope'
|
||||
import { ProviderHostScopeControl } from '@/components/settings/ProviderHostScopeControl'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
const REFRESH_INTERVAL_MS = 60_000
|
||||
@@ -190,16 +191,14 @@ export function GitHubRateLimitPanel({ className }: { className?: string }): Rea
|
||||
'Orca uses REST, Search, and GraphQL through the GitHub CLI.'
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<span className="font-medium text-foreground">
|
||||
{translate(
|
||||
'auto.components.github.github.rate.limit.display.budget_scope',
|
||||
'Budget scope: {{value0}}',
|
||||
{ value0: budgetScope.label }
|
||||
)}
|
||||
</span>{' '}
|
||||
{budgetScope.description}
|
||||
</p>
|
||||
<ProviderHostScopeControl
|
||||
labelPrefix={translate(
|
||||
'auto.components.github.github.rate.limit.display.budget_scope_prefix',
|
||||
'Budget scope'
|
||||
)}
|
||||
scope={budgetScope}
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useAppStore } from '@/store'
|
||||
import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
|
||||
import type { GetGitLabRateLimitResult, GitLabRateLimitSnapshot } from '../../../../shared/types'
|
||||
import { getProviderRateLimitScope } from '@/components/settings/provider-account-scope'
|
||||
import { ProviderHostScopeControl } from '@/components/settings/ProviderHostScopeControl'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
const REFRESH_INTERVAL_MS = 60_000
|
||||
@@ -167,16 +168,14 @@ export function GitLabRateLimitPanel({ className }: { className?: string }): Rea
|
||||
'Orca uses REST through the GitLab CLI.'
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<span className="font-medium text-foreground">
|
||||
{translate(
|
||||
'auto.components.gitlab.gitlab.rate.limit.display.budget_scope',
|
||||
'Budget scope: {{value0}}',
|
||||
{ value0: budgetScope.label }
|
||||
)}
|
||||
</span>{' '}
|
||||
{budgetScope.description}
|
||||
</p>
|
||||
<ProviderHostScopeControl
|
||||
labelPrefix={translate(
|
||||
'auto.components.gitlab.gitlab.rate.limit.display.budget_scope_prefix',
|
||||
'Budget scope'
|
||||
)}
|
||||
scope={budgetScope}
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ServerCog } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useAppStore } from '@/store'
|
||||
import type { ProviderAccountScope, ProviderRateLimitScope } from './provider-account-scope'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
type ProviderHostScopeControlProps = {
|
||||
labelPrefix: string
|
||||
scope: ProviderAccountScope | ProviderRateLimitScope
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function ProviderHostScopeControl({
|
||||
labelPrefix,
|
||||
scope,
|
||||
className
|
||||
}: ProviderHostScopeControlProps): React.JSX.Element {
|
||||
const openSettingsPage = useAppStore((state) => state.openSettingsPage)
|
||||
const openSettingsTarget = useAppStore((state) => state.openSettingsTarget)
|
||||
|
||||
const openActiveServer = (): void => {
|
||||
openSettingsPage()
|
||||
openSettingsTarget({ pane: 'servers', repoId: null })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<span className="font-medium text-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.ProviderHostScopeControl.scope_label',
|
||||
'{{value0}}: {{value1}}',
|
||||
{ value0: labelPrefix, value1: scope.label }
|
||||
)}
|
||||
</span>
|
||||
<div className="mt-0.5 text-muted-foreground">{scope.description}</div>
|
||||
</div>
|
||||
<Button type="button" variant="ghost" size="sm" onClick={openActiveServer}>
|
||||
<ServerCog className="size-3.5" />
|
||||
{translate(
|
||||
'auto.components.settings.ProviderHostScopeControl.change_host',
|
||||
'Change Host'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
|
||||
type StoreState = {
|
||||
settings: { activeRuntimeEnvironmentId: string | null }
|
||||
openSettingsPage: () => void
|
||||
openSettingsTarget: (target: { pane: string; repoId: string | null }) => void
|
||||
}
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
@@ -74,7 +76,13 @@ describe('CLI source-control integration card account scope', () => {
|
||||
})
|
||||
|
||||
it('shows local-client ownership for connected GitHub CLI credentials', async () => {
|
||||
mocks.store.current = { settings: { activeRuntimeEnvironmentId: null } }
|
||||
const openSettingsPage = vi.fn()
|
||||
const openSettingsTarget = vi.fn()
|
||||
mocks.store.current = {
|
||||
settings: { activeRuntimeEnvironmentId: null },
|
||||
openSettingsPage,
|
||||
openSettingsTarget
|
||||
}
|
||||
|
||||
const rendered = await renderCard(<GitHubIntegrationCard />)
|
||||
|
||||
@@ -84,10 +92,22 @@ describe('CLI source-control integration card account scope', () => {
|
||||
expect(rendered.textContent).toContain(
|
||||
'Credentials and account checks for this provider are owned by this desktop client. Choose a remote Host from Settings > Active Server to edit server-owned credentials.'
|
||||
)
|
||||
await act(async () => {
|
||||
Array.from(rendered.querySelectorAll('button'))
|
||||
.find((button) => button.textContent === 'Change Host')
|
||||
?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
})
|
||||
|
||||
expect(openSettingsPage).toHaveBeenCalledTimes(1)
|
||||
expect(openSettingsTarget).toHaveBeenCalledWith({ pane: 'servers', repoId: null })
|
||||
})
|
||||
|
||||
it('shows remote-server ownership for GitLab CLI credential checks', async () => {
|
||||
mocks.store.current = { settings: { activeRuntimeEnvironmentId: 'runtime-1' } }
|
||||
mocks.store.current = {
|
||||
settings: { activeRuntimeEnvironmentId: 'runtime-1' },
|
||||
openSettingsPage: vi.fn(),
|
||||
openSettingsTarget: vi.fn()
|
||||
}
|
||||
mocks.preflight.statuses.glabStatus = 'not-authenticated'
|
||||
|
||||
const rendered = await renderCard(<GitLabIntegrationCard />)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { useAppStore } from '@/store'
|
||||
import { IntegrationCardDetails, IntegrationCardShell } from './integration-card-shell'
|
||||
import { getProviderAccountScope } from './provider-account-scope'
|
||||
import { ProviderHostScopeControl } from './ProviderHostScopeControl'
|
||||
import { usePreflightCardStatuses } from './source-control-preflight-card-status'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
@@ -16,16 +17,14 @@ function ProviderAccountScopeDetails({
|
||||
|
||||
return (
|
||||
<IntegrationCardDetails>
|
||||
<div className="text-xs">
|
||||
<span className="font-medium text-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.cli.source.control.integration.cards.account_scope',
|
||||
'Account scope: {{value0}}',
|
||||
{ value0: accountScope.label }
|
||||
)}
|
||||
</span>
|
||||
<div className="mt-0.5 text-muted-foreground">{accountScope.description}</div>
|
||||
</div>
|
||||
<ProviderHostScopeControl
|
||||
labelPrefix={translate(
|
||||
'auto.components.settings.cli.source.control.integration.cards.account_scope_prefix',
|
||||
'Account scope'
|
||||
)}
|
||||
scope={accountScope}
|
||||
className="text-xs"
|
||||
/>
|
||||
{children}
|
||||
</IntegrationCardDetails>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getProviderRuntimeContextKey } from '@/lib/provider-runtime-context'
|
||||
import { JiraIntegrationCard } from './jira-integration-card'
|
||||
|
||||
type StoreState = {
|
||||
jiraStatus: {
|
||||
connected: boolean
|
||||
sites?: { id: string; displayName: string; siteUrl: string; email?: string }[]
|
||||
}
|
||||
jiraStatusChecked: boolean
|
||||
jiraStatusContextKey: string | null
|
||||
checkJiraConnection: () => Promise<void>
|
||||
disconnectJira: (siteId?: string) => Promise<void>
|
||||
testJiraConnection: (siteId: string) => Promise<{ ok: boolean; error?: string }>
|
||||
settings: { activeRuntimeEnvironmentId: string | null }
|
||||
openSettingsPage: () => void
|
||||
openSettingsTarget: (target: { pane: string; repoId: string | null }) => void
|
||||
}
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
store: { current: null as StoreState | null }
|
||||
}))
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: (selector: (state: StoreState) => unknown) => {
|
||||
if (!mocks.store.current) {
|
||||
throw new Error('Store state was not installed')
|
||||
}
|
||||
return selector(mocks.store.current)
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/components/jira-connect-dialog', () => ({
|
||||
JiraConnectDialog: ({ onConnected }: { onConnected?: () => void }) => (
|
||||
<button type="button" data-testid="simulate-jira-connected" onClick={onConnected}>
|
||||
Simulate Jira connected
|
||||
</button>
|
||||
)
|
||||
}))
|
||||
|
||||
let root: Root | null = null
|
||||
let container: HTMLDivElement | null = null
|
||||
|
||||
function installStore(settings: StoreState['settings']): StoreState {
|
||||
const state: StoreState = {
|
||||
jiraStatus: {
|
||||
connected: true,
|
||||
sites: [
|
||||
{
|
||||
id: 'site-1',
|
||||
displayName: 'Acme Jira',
|
||||
siteUrl: 'https://acme.atlassian.net',
|
||||
email: 'jira@example.test'
|
||||
}
|
||||
]
|
||||
},
|
||||
jiraStatusChecked: true,
|
||||
jiraStatusContextKey: getProviderRuntimeContextKey(settings),
|
||||
checkJiraConnection: vi.fn(async () => {}),
|
||||
disconnectJira: vi.fn(async () => {}),
|
||||
testJiraConnection: vi.fn(async () => ({ ok: true })),
|
||||
settings,
|
||||
openSettingsPage: vi.fn(),
|
||||
openSettingsTarget: vi.fn()
|
||||
}
|
||||
mocks.store.current = state
|
||||
return state
|
||||
}
|
||||
|
||||
async function renderCard(): Promise<HTMLDivElement> {
|
||||
container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
root = createRoot(container)
|
||||
await act(async () => {
|
||||
root?.render(<JiraIntegrationCard />)
|
||||
})
|
||||
return container
|
||||
}
|
||||
|
||||
describe('JiraIntegrationCard account scope', () => {
|
||||
afterEach(async () => {
|
||||
if (root) {
|
||||
await act(async () => {
|
||||
root?.unmount()
|
||||
})
|
||||
}
|
||||
root = null
|
||||
container?.remove()
|
||||
container = null
|
||||
mocks.store.current = null
|
||||
})
|
||||
|
||||
it('shows remote-server account ownership and opens Active Server host selection', async () => {
|
||||
const state = installStore({ activeRuntimeEnvironmentId: 'runtime-1' })
|
||||
|
||||
const rendered = await renderCard()
|
||||
|
||||
expect(rendered.textContent).toContain('Account scope: Remote server: runtime-1')
|
||||
expect(rendered.textContent).toContain('Acme Jira')
|
||||
expect(rendered.textContent).toContain('https://acme.atlassian.net · jira@example.test')
|
||||
|
||||
await act(async () => {
|
||||
Array.from(rendered.querySelectorAll('button'))
|
||||
.find((button) => button.textContent === 'Change Host')
|
||||
?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
})
|
||||
|
||||
expect(state.openSettingsPage).toHaveBeenCalledTimes(1)
|
||||
expect(state.openSettingsTarget).toHaveBeenCalledWith({ pane: 'servers', repoId: null })
|
||||
})
|
||||
})
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { useAppStore } from '@/store'
|
||||
import { IntegrationCardDetails, IntegrationCardShell } from './integration-card-shell'
|
||||
import { getProviderAccountScope } from './provider-account-scope'
|
||||
import { ProviderHostScopeControl } from './ProviderHostScopeControl'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
type VerificationResult = { state: 'ok' | 'error'; error?: string }
|
||||
@@ -108,16 +109,14 @@ export function JiraIntegrationCard(): React.JSX.Element {
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<div className="mt-3 rounded-md border border-border/40 bg-background/50 px-3 py-2 text-xs">
|
||||
<div className="font-medium text-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.task.tracker.integration.cards.account_scope',
|
||||
'Account scope: {{value0}}',
|
||||
{ value0: accountScope.label }
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-0.5 text-muted-foreground">{accountScope.description}</div>
|
||||
</div>
|
||||
<ProviderHostScopeControl
|
||||
labelPrefix={translate(
|
||||
'auto.components.settings.task.tracker.integration.cards.account_scope_prefix',
|
||||
'Account scope'
|
||||
)}
|
||||
scope={accountScope}
|
||||
className="mt-3 rounded-md border border-border/40 bg-background/50 px-3 py-2 text-xs"
|
||||
/>
|
||||
{connected && sites.length > 0 ? (
|
||||
<div className="mt-3 space-y-2">
|
||||
{sites.map((site) => {
|
||||
|
||||
@@ -5,10 +5,18 @@ import { GitLabRateLimitPanel } from '@/components/gitlab/gitlab-rate-limit-disp
|
||||
|
||||
type StoreState = {
|
||||
settings: { activeRuntimeEnvironmentId: string | null }
|
||||
openSettingsPage: () => void
|
||||
openSettingsTarget: (target: { pane: string; repoId: string | null }) => void
|
||||
}
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
store: { current: { settings: { activeRuntimeEnvironmentId: null } } as StoreState }
|
||||
store: {
|
||||
current: {
|
||||
settings: { activeRuntimeEnvironmentId: null },
|
||||
openSettingsPage: vi.fn(),
|
||||
openSettingsTarget: vi.fn()
|
||||
} as StoreState
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
@@ -17,7 +25,11 @@ vi.mock('@/store', () => ({
|
||||
|
||||
describe('provider rate-limit panels account scope', () => {
|
||||
it('shows the local host scope for GitHub API budget', () => {
|
||||
mocks.store.current = { settings: { activeRuntimeEnvironmentId: null } }
|
||||
mocks.store.current = {
|
||||
settings: { activeRuntimeEnvironmentId: null },
|
||||
openSettingsPage: vi.fn(),
|
||||
openSettingsTarget: vi.fn()
|
||||
}
|
||||
|
||||
const markup = renderToStaticMarkup(<GitHubRateLimitPanel />)
|
||||
|
||||
@@ -25,10 +37,15 @@ describe('provider rate-limit panels account scope', () => {
|
||||
expect(markup).toContain(
|
||||
'GitHub API budget is fetched from the CLI on this desktop client. Choose a remote Host from Settings > Active Server to view server-owned budgets.'
|
||||
)
|
||||
expect(markup).toContain('Change Host')
|
||||
})
|
||||
|
||||
it('shows the remote server scope for GitLab API budget', () => {
|
||||
mocks.store.current = { settings: { activeRuntimeEnvironmentId: 'runtime-1' } }
|
||||
mocks.store.current = {
|
||||
settings: { activeRuntimeEnvironmentId: 'runtime-1' },
|
||||
openSettingsPage: vi.fn(),
|
||||
openSettingsTarget: vi.fn()
|
||||
}
|
||||
|
||||
const markup = renderToStaticMarkup(<GitLabRateLimitPanel />)
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ type StoreState = {
|
||||
checkLinearConnection: (force?: boolean) => Promise<void>
|
||||
testLinearConnection: (workspaceId: string) => Promise<{ ok: boolean; error?: string }>
|
||||
settings: { activeRuntimeEnvironmentId: string | null }
|
||||
openSettingsPage: () => void
|
||||
openSettingsTarget: (target: { pane: string; repoId: string | null }) => void
|
||||
}
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
@@ -68,7 +70,9 @@ function installStore(
|
||||
disconnectLinearWorkspace: vi.fn(async () => {}),
|
||||
checkLinearConnection: vi.fn(async () => {}),
|
||||
testLinearConnection: vi.fn(async () => ({ ok: true })),
|
||||
settings
|
||||
settings,
|
||||
openSettingsPage: vi.fn(),
|
||||
openSettingsTarget: vi.fn()
|
||||
}
|
||||
mocks.store.current = state
|
||||
return state
|
||||
@@ -106,6 +110,7 @@ describe('LinearIntegrationCard account scope', () => {
|
||||
expect(rendered.textContent).toContain(
|
||||
'Credentials and account checks for this provider are owned by this desktop client. Choose a remote Host from Settings > Active Server to edit server-owned credentials.'
|
||||
)
|
||||
expect(rendered.textContent).toContain('Change Host')
|
||||
expect(rendered.textContent).toContain('Add access with a Personal API key')
|
||||
|
||||
await act(async () => {
|
||||
@@ -126,6 +131,13 @@ describe('LinearIntegrationCard account scope', () => {
|
||||
expect(rendered.textContent).toContain(
|
||||
'Credentials and account checks for this provider are owned by this remote server. Choose a different Host from Settings > Active Server to edit another account scope.'
|
||||
)
|
||||
await act(async () => {
|
||||
Array.from(rendered.querySelectorAll('button'))
|
||||
.find((button) => button.textContent === 'Change Host')
|
||||
?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
})
|
||||
expect(state.openSettingsPage).toHaveBeenCalledTimes(1)
|
||||
expect(state.openSettingsTarget).toHaveBeenCalledWith({ pane: 'servers', repoId: null })
|
||||
expect(rendered.textContent).toContain('Acme')
|
||||
expect(rendered.textContent).toContain('Acme workspace · linear@example.test')
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getProviderRuntimeContextKey } from '@/lib/provider-runtime-context'
|
||||
import { useAppStore } from '@/store'
|
||||
import { IntegrationCardDetails, IntegrationCardShell } from './integration-card-shell'
|
||||
import { getProviderAccountScope } from './provider-account-scope'
|
||||
import { ProviderHostScopeControl } from './ProviderHostScopeControl'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
type VerificationResult = { state: 'ok' | 'error'; error?: string }
|
||||
@@ -214,16 +215,14 @@ export function LinearIntegrationCard(): React.JSX.Element {
|
||||
|
||||
function ProviderAccountScopeRow({ scope }: { scope: ReturnType<typeof getProviderAccountScope> }) {
|
||||
return (
|
||||
<div className="mt-3 rounded-md border border-border/40 bg-background/50 px-3 py-2 text-xs">
|
||||
<div className="font-medium text-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.task.tracker.integration.cards.account_scope',
|
||||
'Account scope: {{value0}}',
|
||||
{ value0: scope.label }
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-0.5 text-muted-foreground">{scope.description}</div>
|
||||
</div>
|
||||
<ProviderHostScopeControl
|
||||
labelPrefix={translate(
|
||||
'auto.components.settings.task.tracker.integration.cards.account_scope_prefix',
|
||||
'Account scope'
|
||||
)}
|
||||
scope={scope}
|
||||
className="mt-3 rounded-md border border-border/40 bg-background/50 px-3 py-2 text-xs"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user