Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions apps/web/src/components/settings/AddProviderInstanceDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { Radio as RadioPrimitive } from "@base-ui/react/radio";
import { CheckIcon } from "lucide-react";
import { useMemo, useState } from "react";
import {
type EnvironmentId,
ProviderInstanceId,
ProviderDriverKind,
type ProviderInstanceConfig,
} from "@t3tools/contracts";

import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings";
import { useEnvironmentSettings, useUpdateEnvironmentSettings } from "../../hooks/useSettings";
import { cn } from "../../lib/utils";
import { normalizeProviderAccentColor } from "../../providerInstances";
import { Button } from "../ui/button";
Expand Down Expand Up @@ -115,13 +116,20 @@ function validateInstanceId(id: string, existing: ReadonlySet<string>): string |
}

interface AddProviderInstanceDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
readonly open: boolean;
readonly environmentId: EnvironmentId;
readonly environmentLabel: string;
readonly onOpenChange: (open: boolean) => void;
}

export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderInstanceDialogProps) {
const settings = usePrimarySettings();
const updateSettings = useUpdatePrimarySettings();
export function AddProviderInstanceDialog({
open,
environmentId,
environmentLabel,
onOpenChange,
}: AddProviderInstanceDialogProps) {
const settings = useEnvironmentSettings(environmentId);
const updateSettings = useUpdateEnvironmentSettings(environmentId);

const [wizardStep, setWizardStep] = useState(0);
const [driver, setDriver] = useState<ProviderDriverKind>(DEFAULT_DRIVER_KIND);
Expand Down Expand Up @@ -208,7 +216,7 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns
toastManager.add({
type: "success",
title: "Provider instance added",
description: `${driverOption.label} instance '${instanceId}' was added.`,
description: `${driverOption.label} instance '${instanceId}' was added to ${environmentLabel}.`,
});
onOpenChange(false);
} catch (error) {
Expand All @@ -227,8 +235,7 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns
<DialogHeader>
<DialogTitle>Add provider instance</DialogTitle>
<DialogDescription>
Configure an additional provider instance — for example, a second Codex install
pointed at a different workspace.
Configure an additional provider instance on {environmentLabel}.
</DialogDescription>
<AddProviderInstanceWizardSteps
currentStep={wizardStep}
Expand Down
77 changes: 77 additions & 0 deletions apps/web/src/components/settings/ProviderEnvironmentSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { EnvironmentId } from "@t3tools/contracts";
import { CloudIcon, MonitorIcon } from "lucide-react";
import { useMemo } from "react";

import type { EnvironmentPresentation } from "../../state/environments";
import {
Select,
SelectGroup,
SelectGroupLabel,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "../ui/select";

interface ProviderEnvironmentSelectorProps {
readonly environmentId: EnvironmentId;
readonly environments: ReadonlyArray<EnvironmentPresentation>;
readonly primaryEnvironmentId: EnvironmentId | null;
readonly onEnvironmentChange: (environmentId: EnvironmentId) => void;
}

export function ProviderEnvironmentSelector({
environmentId,
environments,
primaryEnvironmentId,
onEnvironmentChange,
}: ProviderEnvironmentSelectorProps) {
const selectedEnvironment =
environments.find((environment) => environment.environmentId === environmentId) ?? null;
const items = useMemo(
() =>
environments.map((environment) => ({
value: environment.environmentId,
label: environment.label,
})),
[environments],
);

return (
<Select
value={environmentId}
items={items}
onValueChange={(value) => onEnvironmentChange(value as EnvironmentId)}
>
<SelectTrigger
size="sm"
className="w-36 sm:w-52"
aria-label="Configure providers on environment"
>
{environmentId === primaryEnvironmentId ? (
<MonitorIcon className="size-3.5" />
) : (
<CloudIcon className="size-3.5" />
)}
<SelectValue>{selectedEnvironment?.label ?? "Select environment"}</SelectValue>
</SelectTrigger>
<SelectPopup align="end" alignItemWithTrigger={false}>
<SelectGroup>
<SelectGroupLabel>Configure providers on</SelectGroupLabel>
{environments.map((environment) => (
<SelectItem key={environment.environmentId} value={environment.environmentId}>
<span className="inline-flex items-center gap-1.5">
{environment.environmentId === primaryEnvironmentId ? (
<MonitorIcon className="size-3.5" />
) : (
<CloudIcon className="size-3.5" />
)}
{environment.label}
</span>
</SelectItem>
))}
</SelectGroup>
</SelectPopup>
</Select>
);
}
62 changes: 62 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.logic.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
DEFAULT_SERVER_SETTINGS,
EnvironmentId,
ProviderDriverKind,
ProviderInstanceId,
type ProviderInstanceConfig,
Expand All @@ -10,8 +11,69 @@ import {
formatDiagnosticsDescription,
isProjectGroupingEnabled,
projectGroupingModeFromToggle,
resolveProviderSettingsEnvironmentId,
} from "./SettingsPanels.logic";

const LOCAL_ENVIRONMENT_ID = EnvironmentId.make("00000000-0000-4000-8000-000000000001");
const REMOTE_ENVIRONMENT_ID = EnvironmentId.make("00000000-0000-4000-8000-000000000002");

describe("provider settings environment selection", () => {
it("preserves an explicit selected environment", () => {
expect(
resolveProviderSettingsEnvironmentId({
availableEnvironmentIds: [LOCAL_ENVIRONMENT_ID, REMOTE_ENVIRONMENT_ID],
selectedEnvironmentId: REMOTE_ENVIRONMENT_ID,
primaryEnvironmentId: LOCAL_ENVIRONMENT_ID,
activeEnvironmentId: LOCAL_ENVIRONMENT_ID,
}),
).toBe(REMOTE_ENVIRONMENT_ID);
});

it("defaults to the primary environment in managed mode", () => {
expect(
resolveProviderSettingsEnvironmentId({
availableEnvironmentIds: [REMOTE_ENVIRONMENT_ID, LOCAL_ENVIRONMENT_ID],
selectedEnvironmentId: null,
primaryEnvironmentId: LOCAL_ENVIRONMENT_ID,
activeEnvironmentId: REMOTE_ENVIRONMENT_ID,
}),
).toBe(LOCAL_ENVIRONMENT_ID);
});

it("defaults to the active remote environment in client-only mode", () => {
expect(
resolveProviderSettingsEnvironmentId({
availableEnvironmentIds: [LOCAL_ENVIRONMENT_ID, REMOTE_ENVIRONMENT_ID],
selectedEnvironmentId: null,
primaryEnvironmentId: null,
activeEnvironmentId: REMOTE_ENVIRONMENT_ID,
}),
).toBe(REMOTE_ENVIRONMENT_ID);
});

it("falls back when the selected environment is no longer available", () => {
expect(
resolveProviderSettingsEnvironmentId({
availableEnvironmentIds: [LOCAL_ENVIRONMENT_ID],
selectedEnvironmentId: REMOTE_ENVIRONMENT_ID,
primaryEnvironmentId: LOCAL_ENVIRONMENT_ID,
activeEnvironmentId: REMOTE_ENVIRONMENT_ID,
}),
).toBe(LOCAL_ENVIRONMENT_ID);
});

it("returns null when no environments are available", () => {
expect(
resolveProviderSettingsEnvironmentId({
availableEnvironmentIds: [],
selectedEnvironmentId: null,
primaryEnvironmentId: null,
activeEnvironmentId: null,
}),
).toBeNull();
});
});

describe("project grouping toggle", () => {
it("enables repository grouping and disables into separate projects", () => {
expect(isProjectGroupingEnabled("repository")).toBe(true);
Expand Down
24 changes: 24 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.logic.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
EnvironmentId,
ProviderDriverKind,
ProviderInstanceConfig,
ProviderInstanceId,
Expand All @@ -8,6 +9,29 @@ import type {
} from "@t3tools/contracts";
import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";

export function resolveProviderSettingsEnvironmentId(input: {
readonly availableEnvironmentIds: ReadonlyArray<EnvironmentId>;
readonly selectedEnvironmentId: EnvironmentId | null;
readonly primaryEnvironmentId: EnvironmentId | null;
readonly activeEnvironmentId: EnvironmentId | null;
}): EnvironmentId | null {
const availableIds = new Set(input.availableEnvironmentIds);
const candidates = [
input.selectedEnvironmentId,
input.primaryEnvironmentId,
input.activeEnvironmentId,
input.availableEnvironmentIds[0] ?? null,
];

for (const candidate of candidates) {
if (candidate !== null && availableIds.has(candidate)) {
return candidate;
}
}

return null;
}

export function isProjectGroupingEnabled(mode: SidebarProjectGroupingMode): boolean {
return mode !== "separate";
}
Expand Down
Loading
Loading