@echothink-ui/project 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/components/ProjectActivityTimeline.d.ts +5 -0
- package/dist/components/ProjectAppDomainPanel.d.ts +8 -0
- package/dist/components/ProjectCard.d.ts +8 -0
- package/dist/components/ProjectCreateForm.d.ts +7 -0
- package/dist/components/ProjectDashboardTemplate.d.ts +11 -0
- package/dist/components/ProjectManagementPage.d.ts +17 -0
- package/dist/components/ProjectMembersPanel.d.ts +9 -0
- package/dist/components/ProjectModelConfigPanel.d.ts +9 -0
- package/dist/components/ProjectPermissionPanel.d.ts +9 -0
- package/dist/components/ProjectResourcePanel.d.ts +6 -0
- package/dist/components/ProjectScopeSelector.d.ts +7 -0
- package/dist/components/ProjectSettingsPanel.d.ts +6 -0
- package/dist/components/ProjectStatusSummary.d.ts +6 -0
- package/dist/components/ProjectSummaryPanel.d.ts +5 -0
- package/dist/components/ProjectTab.d.ts +3 -0
- package/dist/components/ProjectTabGroup.d.ts +12 -0
- package/dist/components/ProjectTable.d.ts +9 -0
- package/dist/index.cjs +2112 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +2059 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +2098 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +99 -0
- package/dist/utils.d.ts +288 -0
- package/package.json +45 -0
- package/src/components/ProjectActivityTimeline.test.tsx +43 -0
- package/src/components/ProjectActivityTimeline.tsx +118 -0
- package/src/components/ProjectAppDomainPanel.tsx +147 -0
- package/src/components/ProjectCard.tsx +117 -0
- package/src/components/ProjectCreateForm.test.tsx +45 -0
- package/src/components/ProjectCreateForm.tsx +176 -0
- package/src/components/ProjectDashboardTemplate.tsx +107 -0
- package/src/components/ProjectManagementPage.tsx +112 -0
- package/src/components/ProjectMembersPanel.tsx +181 -0
- package/src/components/ProjectModelConfigPanel.tsx +294 -0
- package/src/components/ProjectPermissionPanel.tsx +174 -0
- package/src/components/ProjectResourcePanel.tsx +154 -0
- package/src/components/ProjectScopeSelector.test.tsx +50 -0
- package/src/components/ProjectScopeSelector.tsx +92 -0
- package/src/components/ProjectSettingsPanel.test.tsx +25 -0
- package/src/components/ProjectSettingsPanel.tsx +244 -0
- package/src/components/ProjectStatusSummary.tsx +165 -0
- package/src/components/ProjectSummaryPanel.test.tsx +37 -0
- package/src/components/ProjectSummaryPanel.tsx +85 -0
- package/src/components/ProjectTab.tsx +8 -0
- package/src/components/ProjectTabGroup.tsx +38 -0
- package/src/components/ProjectTable.tsx +138 -0
- package/src/index.test.tsx +337 -0
- package/src/index.tsx +41 -0
- package/src/styles.css +2431 -0
- package/src/types.ts +111 -0
- package/src/utils.ts +96 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Badge,
|
|
4
|
+
EmptyState,
|
|
5
|
+
StatusDot,
|
|
6
|
+
statusLabel,
|
|
7
|
+
type EthAction
|
|
8
|
+
} from "@echothink-ui/core";
|
|
9
|
+
import { DataTable, type DataColumn } from "@echothink-ui/data";
|
|
10
|
+
import type { ProjectBaseProps, ProjectResource } from "../types";
|
|
11
|
+
import { classNames, formatNumber, projectHtmlProps } from "../utils";
|
|
12
|
+
|
|
13
|
+
const syncedStatuses = new Set(["completed", "succeeded", "synced", "active"]);
|
|
14
|
+
const attentionStatuses = new Set([
|
|
15
|
+
"blocked",
|
|
16
|
+
"failed",
|
|
17
|
+
"stale",
|
|
18
|
+
"warning",
|
|
19
|
+
"pending-approval",
|
|
20
|
+
"approval-required"
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
export interface ProjectResourcePanelProps extends ProjectBaseProps {
|
|
24
|
+
resources?: ProjectResource[];
|
|
25
|
+
onSelect?: (resourceId: string) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function ProjectResourcePanel({
|
|
29
|
+
resources = [],
|
|
30
|
+
onSelect,
|
|
31
|
+
className,
|
|
32
|
+
title = "Project resources",
|
|
33
|
+
...props
|
|
34
|
+
}: ProjectResourcePanelProps) {
|
|
35
|
+
const syncedCount = resources.filter(
|
|
36
|
+
(resource) => resource.status && syncedStatuses.has(resource.status)
|
|
37
|
+
).length;
|
|
38
|
+
const attentionCount = resources.filter(
|
|
39
|
+
(resource) => resource.status && attentionStatuses.has(resource.status)
|
|
40
|
+
).length;
|
|
41
|
+
const hasRowActions = Boolean(onSelect);
|
|
42
|
+
|
|
43
|
+
const columns = React.useMemo<DataColumn<ProjectResource>[]>(
|
|
44
|
+
() => [
|
|
45
|
+
{
|
|
46
|
+
key: "label",
|
|
47
|
+
header: "Resource",
|
|
48
|
+
width: "40%",
|
|
49
|
+
render: (resource) => (
|
|
50
|
+
<div className="eth-project-resource-panel__resource">
|
|
51
|
+
<span className="eth-project-resource-panel__resource-icon" aria-hidden="true">
|
|
52
|
+
{resourceKindInitial(resource.kind)}
|
|
53
|
+
</span>
|
|
54
|
+
<span className="eth-project-resource-panel__resource-copy">
|
|
55
|
+
<strong>{resource.label}</strong>
|
|
56
|
+
<span>{resource.id}</span>
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: "kind",
|
|
63
|
+
header: "Kind",
|
|
64
|
+
width: "14%",
|
|
65
|
+
render: (resource) => <Badge severity="neutral">{resourceKindLabel(resource.kind)}</Badge>
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
key: "status",
|
|
69
|
+
header: "Status",
|
|
70
|
+
width: "20%",
|
|
71
|
+
render: (resource) =>
|
|
72
|
+
resource.status ? (
|
|
73
|
+
<StatusDot status={resource.status} label={statusLabel(resource.status)} />
|
|
74
|
+
) : (
|
|
75
|
+
<span className="eth-project-resource-panel__muted">Not tracked</span>
|
|
76
|
+
)
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: "updatedAt",
|
|
80
|
+
header: "Updated",
|
|
81
|
+
width: "16%",
|
|
82
|
+
render: (resource) => (
|
|
83
|
+
<span className="eth-project-resource-panel__updated">
|
|
84
|
+
{resource.updatedAt ?? "Not updated"}
|
|
85
|
+
</span>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
[]
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const rowActions = React.useCallback(
|
|
93
|
+
(resource: ProjectResource): EthAction[] =>
|
|
94
|
+
onSelect ? [{ id: "open", label: "Open", onSelect: () => onSelect(resource.id) }] : [],
|
|
95
|
+
[onSelect]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<section
|
|
100
|
+
{...projectHtmlProps(props)}
|
|
101
|
+
className={classNames("eth-project-resource-panel", className)}
|
|
102
|
+
data-eth-component="ProjectResourcePanel"
|
|
103
|
+
role="region"
|
|
104
|
+
aria-label={typeof title === "string" ? title : "Project resources"}
|
|
105
|
+
tabIndex={-1}
|
|
106
|
+
>
|
|
107
|
+
{title ? (
|
|
108
|
+
<header className="eth-project-resource-panel__header">
|
|
109
|
+
<h3>{title}</h3>
|
|
110
|
+
<dl className="eth-project-resource-panel__summary" aria-label="Resource summary">
|
|
111
|
+
<div>
|
|
112
|
+
<dt>Resources</dt>
|
|
113
|
+
<dd>{formatNumber(resources.length)}</dd>
|
|
114
|
+
</div>
|
|
115
|
+
<div>
|
|
116
|
+
<dt>Synced</dt>
|
|
117
|
+
<dd>{formatNumber(syncedCount)}</dd>
|
|
118
|
+
</div>
|
|
119
|
+
<div>
|
|
120
|
+
<dt>Attention</dt>
|
|
121
|
+
<dd>{formatNumber(attentionCount)}</dd>
|
|
122
|
+
</div>
|
|
123
|
+
</dl>
|
|
124
|
+
</header>
|
|
125
|
+
) : null}
|
|
126
|
+
<DataTable
|
|
127
|
+
rows={resources}
|
|
128
|
+
columns={columns}
|
|
129
|
+
rowKey="id"
|
|
130
|
+
density="compact"
|
|
131
|
+
className="eth-project-resource-panel__table"
|
|
132
|
+
rowActions={hasRowActions ? rowActions : undefined}
|
|
133
|
+
emptyState={
|
|
134
|
+
<EmptyState
|
|
135
|
+
title="No resources"
|
|
136
|
+
description="Linked files, documents, tables, and services appear here."
|
|
137
|
+
/>
|
|
138
|
+
}
|
|
139
|
+
/>
|
|
140
|
+
</section>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resourceKindLabel(kind: string) {
|
|
145
|
+
return kind
|
|
146
|
+
.split(/[-_\s]+/)
|
|
147
|
+
.filter(Boolean)
|
|
148
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
149
|
+
.join(" ");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function resourceKindInitial(kind: string) {
|
|
153
|
+
return resourceKindLabel(kind).charAt(0) || "R";
|
|
154
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { ProjectScopeSelector } from "./ProjectScopeSelector";
|
|
4
|
+
|
|
5
|
+
const scopes = [
|
|
6
|
+
{
|
|
7
|
+
id: "campaign",
|
|
8
|
+
label: "Campaign scope",
|
|
9
|
+
kind: "project" as const,
|
|
10
|
+
status: "active" as const
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: "mailbox",
|
|
14
|
+
label: "Mailbox",
|
|
15
|
+
kind: "app-domain" as const,
|
|
16
|
+
status: "running" as const
|
|
17
|
+
},
|
|
18
|
+
{ id: "brief", label: "Brief.md", kind: "resource" as const }
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
describe("@echothink-ui/project ProjectScopeSelector", () => {
|
|
22
|
+
it("renders scopes as a single-select radio group with status metadata", () => {
|
|
23
|
+
const onSelect = vi.fn();
|
|
24
|
+
|
|
25
|
+
render(<ProjectScopeSelector scopes={scopes} activeScopeId="campaign" onSelect={onSelect} />);
|
|
26
|
+
|
|
27
|
+
expect(screen.getByRole("radiogroup", { name: "Target scope" })).toBeTruthy();
|
|
28
|
+
expect(
|
|
29
|
+
(screen.getByRole("radio", { name: /Campaign scope/ }) as HTMLInputElement).checked
|
|
30
|
+
).toBe(true);
|
|
31
|
+
expect((screen.getByRole("radio", { name: /Mailbox/ }) as HTMLInputElement).checked).toBe(
|
|
32
|
+
false
|
|
33
|
+
);
|
|
34
|
+
expect(screen.getByText("Project")).toBeTruthy();
|
|
35
|
+
expect(screen.getByText("App domain")).toBeTruthy();
|
|
36
|
+
expect(screen.getByText("Active")).toBeTruthy();
|
|
37
|
+
expect(screen.getByText("Running")).toBeTruthy();
|
|
38
|
+
|
|
39
|
+
fireEvent.click(screen.getByRole("radio", { name: /Mailbox/ }));
|
|
40
|
+
|
|
41
|
+
expect(onSelect).toHaveBeenCalledWith("mailbox");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("shows an empty state when no scopes are available", () => {
|
|
45
|
+
render(<ProjectScopeSelector scopes={[]} />);
|
|
46
|
+
|
|
47
|
+
expect(screen.getByText("No scopes")).toBeTruthy();
|
|
48
|
+
expect(screen.getByText("Project, app-domain, and resource targets appear here.")).toBeTruthy();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { EmptyState, StatusDot, statusLabel } from "@echothink-ui/core";
|
|
3
|
+
import type { ProjectBaseProps, ProjectScope } from "../types";
|
|
4
|
+
import { classNames, projectHtmlProps } from "../utils";
|
|
5
|
+
|
|
6
|
+
export interface ProjectScopeSelectorProps extends ProjectBaseProps {
|
|
7
|
+
scopes?: ProjectScope[];
|
|
8
|
+
activeScopeId?: string;
|
|
9
|
+
onSelect?: (scopeId: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const kindLabels: Record<ProjectScope["kind"], string> = {
|
|
13
|
+
project: "Project",
|
|
14
|
+
"app-domain": "App domain",
|
|
15
|
+
resource: "Resource"
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function ProjectScopeSelector({
|
|
19
|
+
scopes = [],
|
|
20
|
+
activeScopeId,
|
|
21
|
+
onSelect,
|
|
22
|
+
className,
|
|
23
|
+
...props
|
|
24
|
+
}: ProjectScopeSelectorProps) {
|
|
25
|
+
const groupName = React.useId();
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<section
|
|
29
|
+
{...projectHtmlProps(props)}
|
|
30
|
+
className={classNames("eth-project-scope-selector", className)}
|
|
31
|
+
data-eth-component="ProjectScopeSelector"
|
|
32
|
+
role="region"
|
|
33
|
+
aria-label="Project scopes"
|
|
34
|
+
tabIndex={-1}
|
|
35
|
+
>
|
|
36
|
+
{scopes.length ? (
|
|
37
|
+
<div
|
|
38
|
+
className="eth-project-scope-selector__group"
|
|
39
|
+
role="radiogroup"
|
|
40
|
+
aria-label="Target scope"
|
|
41
|
+
>
|
|
42
|
+
{scopes.map((scope, index) => {
|
|
43
|
+
const active = activeScopeId === scope.id;
|
|
44
|
+
const inputId = `${groupName}-${index}`;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<label
|
|
48
|
+
key={scope.id}
|
|
49
|
+
className={classNames(
|
|
50
|
+
"eth-project-scope-selector__item",
|
|
51
|
+
active && "eth-project-scope-selector__item--active",
|
|
52
|
+
!onSelect && "eth-project-scope-selector__item--readonly"
|
|
53
|
+
)}
|
|
54
|
+
htmlFor={inputId}
|
|
55
|
+
>
|
|
56
|
+
<input
|
|
57
|
+
id={inputId}
|
|
58
|
+
className="eth-project-scope-selector__control"
|
|
59
|
+
type="radio"
|
|
60
|
+
name={groupName}
|
|
61
|
+
value={scope.id}
|
|
62
|
+
checked={active}
|
|
63
|
+
readOnly={!onSelect}
|
|
64
|
+
onChange={() => onSelect?.(scope.id)}
|
|
65
|
+
/>
|
|
66
|
+
<span className="eth-project-scope-selector__content">
|
|
67
|
+
<span className="eth-project-scope-selector__title-row">
|
|
68
|
+
<span className="eth-project-scope-selector__label">{scope.label}</span>
|
|
69
|
+
{scope.status ? (
|
|
70
|
+
<span className="eth-project-scope-selector__status">
|
|
71
|
+
<StatusDot status={scope.status} label={statusLabel(scope.status)} />
|
|
72
|
+
</span>
|
|
73
|
+
) : null}
|
|
74
|
+
</span>
|
|
75
|
+
<span className="eth-project-scope-selector__kind">
|
|
76
|
+
{kindLabels[scope.kind]}
|
|
77
|
+
</span>
|
|
78
|
+
</span>
|
|
79
|
+
</label>
|
|
80
|
+
);
|
|
81
|
+
})}
|
|
82
|
+
</div>
|
|
83
|
+
) : (
|
|
84
|
+
<EmptyState
|
|
85
|
+
className="eth-project-scope-selector__empty"
|
|
86
|
+
title="No scopes"
|
|
87
|
+
description="Project, app-domain, and resource targets appear here."
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
</section>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { ProjectSettingsPanel } from "./ProjectSettingsPanel";
|
|
4
|
+
|
|
5
|
+
describe("ProjectSettingsPanel", () => {
|
|
6
|
+
it("renders project members as editable collaborators inside the settings surface", () => {
|
|
7
|
+
render(
|
|
8
|
+
<ProjectSettingsPanel
|
|
9
|
+
settings={{
|
|
10
|
+
name: "Marketing Campaign",
|
|
11
|
+
members: [{ id: "alex", label: "Alex Chen", role: "owner" }],
|
|
12
|
+
defaultRole: "editor"
|
|
13
|
+
}}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
expect(screen.getByRole("heading", { name: "Project settings" })).toBeTruthy();
|
|
18
|
+
expect(screen.getByText("Alex Chen")).toBeTruthy();
|
|
19
|
+
const roleSelect = screen.getByRole("combobox", {
|
|
20
|
+
name: "Role for Alex Chen"
|
|
21
|
+
}) as HTMLSelectElement;
|
|
22
|
+
|
|
23
|
+
expect(roleSelect.value).toBe("owner");
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
FormField,
|
|
4
|
+
FormSection,
|
|
5
|
+
Select,
|
|
6
|
+
TextInput,
|
|
7
|
+
Textarea
|
|
8
|
+
} from "@echothink-ui/core";
|
|
9
|
+
import type { ProjectBaseProps, ProjectSettings } from "../types";
|
|
10
|
+
import { classNames, projectHtmlProps } from "../utils";
|
|
11
|
+
|
|
12
|
+
export interface ProjectSettingsPanelProps extends ProjectBaseProps {
|
|
13
|
+
settings?: ProjectSettings;
|
|
14
|
+
onChange?: (settings: ProjectSettings) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const permissionOptions = [
|
|
18
|
+
{ value: "private", label: "Private" },
|
|
19
|
+
{ value: "team", label: "Team" },
|
|
20
|
+
{ value: "organization", label: "Organization" }
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const roleOptions = [
|
|
24
|
+
{ value: "viewer", label: "Viewer" },
|
|
25
|
+
{ value: "editor", label: "Editor" },
|
|
26
|
+
{ value: "owner", label: "Owner" }
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const providerOptions = [
|
|
30
|
+
{ value: "openai", label: "OpenAI" },
|
|
31
|
+
{ value: "azure-openai", label: "Azure OpenAI" },
|
|
32
|
+
{ value: "custom", label: "Custom provider" }
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const modelOptions = [
|
|
36
|
+
{ value: "gpt-4o", label: "GPT-4o" },
|
|
37
|
+
{ value: "gpt-5.4", label: "GPT-5.4" },
|
|
38
|
+
{ value: "gpt-5.4-mini", label: "GPT-5.4 mini" },
|
|
39
|
+
{ value: "custom", label: "Custom model" }
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function optionLabel(options: Array<{ value: string; label: string }>, value: string) {
|
|
43
|
+
return options.find((option) => option.value === value)?.label ?? value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function nodeText(node: React.ReactNode) {
|
|
47
|
+
if (typeof node === "string" || typeof node === "number") return String(node);
|
|
48
|
+
return "member";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function memberInitials(label: React.ReactNode) {
|
|
52
|
+
const text = nodeText(label);
|
|
53
|
+
const words = text.trim().split(/\s+/).filter(Boolean);
|
|
54
|
+
if (words.length === 0 || text === "member") return "M";
|
|
55
|
+
return words
|
|
56
|
+
.slice(0, 2)
|
|
57
|
+
.map((word) => word.charAt(0).toUpperCase())
|
|
58
|
+
.join("");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function countLabel(count: number, singular: string, plural = `${singular}s`) {
|
|
62
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function ProjectSettingsPanel({
|
|
66
|
+
settings = {},
|
|
67
|
+
onChange,
|
|
68
|
+
className,
|
|
69
|
+
...props
|
|
70
|
+
}: ProjectSettingsPanelProps) {
|
|
71
|
+
const titleId = React.useId();
|
|
72
|
+
const members = settings.members ?? [];
|
|
73
|
+
const permissionMode = settings.permissionMode ?? "private";
|
|
74
|
+
const defaultRole = settings.defaultRole ?? "viewer";
|
|
75
|
+
const modelProvider = settings.modelProvider ?? "openai";
|
|
76
|
+
const model = settings.model ?? "gpt-5.4";
|
|
77
|
+
const update = (patch: Partial<ProjectSettings>) => {
|
|
78
|
+
onChange?.({ ...settings, ...patch });
|
|
79
|
+
};
|
|
80
|
+
const updateMemberRole = (memberId: string, role: string) => {
|
|
81
|
+
update({
|
|
82
|
+
members: members.map((member) => (member.id === memberId ? { ...member, role } : member))
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<section
|
|
88
|
+
{...projectHtmlProps(props)}
|
|
89
|
+
className={classNames("eth-project-settings-panel", className)}
|
|
90
|
+
data-eth-component="ProjectSettingsPanel"
|
|
91
|
+
role="region"
|
|
92
|
+
aria-labelledby={titleId}
|
|
93
|
+
tabIndex={-1}
|
|
94
|
+
>
|
|
95
|
+
<header className="eth-project-settings-panel__header">
|
|
96
|
+
<div className="eth-project-settings-panel__heading">
|
|
97
|
+
<p className="eth-project-settings-panel__eyebrow">Project management</p>
|
|
98
|
+
<h2 id={titleId}>Project settings</h2>
|
|
99
|
+
<p>Manage identity, access defaults, members, and the project agent model.</p>
|
|
100
|
+
</div>
|
|
101
|
+
<dl className="eth-project-settings-panel__summary" aria-label="Project settings summary">
|
|
102
|
+
<div>
|
|
103
|
+
<dt>Visibility</dt>
|
|
104
|
+
<dd>{optionLabel(permissionOptions, permissionMode)}</dd>
|
|
105
|
+
</div>
|
|
106
|
+
<div>
|
|
107
|
+
<dt>Members</dt>
|
|
108
|
+
<dd>{countLabel(members.length, "member")}</dd>
|
|
109
|
+
</div>
|
|
110
|
+
<div>
|
|
111
|
+
<dt>Model</dt>
|
|
112
|
+
<dd>{optionLabel(modelOptions, model)}</dd>
|
|
113
|
+
</div>
|
|
114
|
+
</dl>
|
|
115
|
+
</header>
|
|
116
|
+
|
|
117
|
+
<div className="eth-project-settings-panel__content">
|
|
118
|
+
<FormSection title="General" description="Core project identity and description.">
|
|
119
|
+
<div className="eth-project-settings-panel__stack">
|
|
120
|
+
<FormField
|
|
121
|
+
id="project-settings-name"
|
|
122
|
+
label="Project name"
|
|
123
|
+
helperText="Shown in project lists, audit records, and app-domain routing."
|
|
124
|
+
>
|
|
125
|
+
<TextInput
|
|
126
|
+
value={settings.name ?? ""}
|
|
127
|
+
onChange={(event) => update({ name: event.currentTarget.value })}
|
|
128
|
+
/>
|
|
129
|
+
</FormField>
|
|
130
|
+
<FormField
|
|
131
|
+
id="project-settings-description"
|
|
132
|
+
label="Description"
|
|
133
|
+
helperText="Brief context for collaborators and project agents."
|
|
134
|
+
>
|
|
135
|
+
<Textarea
|
|
136
|
+
value={settings.description ?? ""}
|
|
137
|
+
rows={4}
|
|
138
|
+
onChange={(event) => update({ description: event.currentTarget.value })}
|
|
139
|
+
/>
|
|
140
|
+
</FormField>
|
|
141
|
+
</div>
|
|
142
|
+
</FormSection>
|
|
143
|
+
|
|
144
|
+
<FormSection title="Members" description="People who can work in this project.">
|
|
145
|
+
<div className="eth-project-settings-panel__members">
|
|
146
|
+
<div className="eth-project-settings-panel__member-meta">
|
|
147
|
+
<span>{countLabel(members.length, "active member")}</span>
|
|
148
|
+
<span>Default role: {optionLabel(roleOptions, defaultRole)}</span>
|
|
149
|
+
</div>
|
|
150
|
+
{members.length ? (
|
|
151
|
+
<div className="eth-project-settings-panel__member-list" role="list">
|
|
152
|
+
{members.map((member) => {
|
|
153
|
+
const label = nodeText(member.label);
|
|
154
|
+
return (
|
|
155
|
+
<article
|
|
156
|
+
key={member.id}
|
|
157
|
+
className="eth-project-settings-panel__member-row"
|
|
158
|
+
role="listitem"
|
|
159
|
+
>
|
|
160
|
+
<span
|
|
161
|
+
className="eth-project-settings-panel__member-avatar"
|
|
162
|
+
aria-hidden="true"
|
|
163
|
+
>
|
|
164
|
+
{memberInitials(member.label)}
|
|
165
|
+
</span>
|
|
166
|
+
<div className="eth-project-settings-panel__member-main">
|
|
167
|
+
<strong>{member.label}</strong>
|
|
168
|
+
<span>Project collaborator</span>
|
|
169
|
+
</div>
|
|
170
|
+
<Select
|
|
171
|
+
aria-label={`Role for ${label}`}
|
|
172
|
+
options={roleOptions}
|
|
173
|
+
value={member.role ?? defaultRole}
|
|
174
|
+
onChange={(event) => updateMemberRole(member.id, event.currentTarget.value)}
|
|
175
|
+
/>
|
|
176
|
+
</article>
|
|
177
|
+
);
|
|
178
|
+
})}
|
|
179
|
+
</div>
|
|
180
|
+
) : (
|
|
181
|
+
<p className="eth-project-settings-panel__empty">
|
|
182
|
+
Members added to this project will appear here with their assigned role.
|
|
183
|
+
</p>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
</FormSection>
|
|
187
|
+
|
|
188
|
+
<FormSection title="Permissions" description="Default access policy for project resources.">
|
|
189
|
+
<div className="eth-project-settings-panel__field-grid">
|
|
190
|
+
<FormField
|
|
191
|
+
id="project-settings-permission-mode"
|
|
192
|
+
label="Visibility"
|
|
193
|
+
helperText="Controls the default project audience."
|
|
194
|
+
>
|
|
195
|
+
<Select
|
|
196
|
+
options={permissionOptions}
|
|
197
|
+
value={permissionMode}
|
|
198
|
+
onChange={(event) => update({ permissionMode: event.currentTarget.value })}
|
|
199
|
+
/>
|
|
200
|
+
</FormField>
|
|
201
|
+
<FormField
|
|
202
|
+
id="project-settings-default-role"
|
|
203
|
+
label="Default member role"
|
|
204
|
+
helperText="Applied when a member has no explicit role."
|
|
205
|
+
>
|
|
206
|
+
<Select
|
|
207
|
+
options={roleOptions}
|
|
208
|
+
value={defaultRole}
|
|
209
|
+
onChange={(event) => update({ defaultRole: event.currentTarget.value })}
|
|
210
|
+
/>
|
|
211
|
+
</FormField>
|
|
212
|
+
</div>
|
|
213
|
+
</FormSection>
|
|
214
|
+
|
|
215
|
+
<FormSection title="Model config" description="Default provider and model for project agents.">
|
|
216
|
+
<div className="eth-project-settings-panel__field-grid">
|
|
217
|
+
<FormField
|
|
218
|
+
id="project-settings-provider"
|
|
219
|
+
label="Provider"
|
|
220
|
+
helperText="Runtime provider for project-level agents."
|
|
221
|
+
>
|
|
222
|
+
<Select
|
|
223
|
+
options={providerOptions}
|
|
224
|
+
value={modelProvider}
|
|
225
|
+
onChange={(event) => update({ modelProvider: event.currentTarget.value })}
|
|
226
|
+
/>
|
|
227
|
+
</FormField>
|
|
228
|
+
<FormField
|
|
229
|
+
id="project-settings-model"
|
|
230
|
+
label="Model"
|
|
231
|
+
helperText="Used when an agent has no override."
|
|
232
|
+
>
|
|
233
|
+
<Select
|
|
234
|
+
options={modelOptions}
|
|
235
|
+
value={model}
|
|
236
|
+
onChange={(event) => update({ model: event.currentTarget.value })}
|
|
237
|
+
/>
|
|
238
|
+
</FormField>
|
|
239
|
+
</div>
|
|
240
|
+
</FormSection>
|
|
241
|
+
</div>
|
|
242
|
+
</section>
|
|
243
|
+
);
|
|
244
|
+
}
|