@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,117 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
ActionGroup,
|
|
4
|
+
Badge,
|
|
5
|
+
Button,
|
|
6
|
+
StatusDot,
|
|
7
|
+
statusLabel,
|
|
8
|
+
type EthAction
|
|
9
|
+
} from "@echothink-ui/core";
|
|
10
|
+
import type { ProjectBaseProps, ProjectSummary } from "../types";
|
|
11
|
+
import {
|
|
12
|
+
classNames,
|
|
13
|
+
formatNumber,
|
|
14
|
+
healthLabel,
|
|
15
|
+
healthToStatus,
|
|
16
|
+
projectHtmlProps,
|
|
17
|
+
statusToSeverity
|
|
18
|
+
} from "../utils";
|
|
19
|
+
|
|
20
|
+
export interface ProjectCardProps extends ProjectBaseProps {
|
|
21
|
+
project?: ProjectSummary;
|
|
22
|
+
onSelect?: (projectId: string) => void;
|
|
23
|
+
actions?: EthAction[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ProjectCard({
|
|
27
|
+
project,
|
|
28
|
+
onSelect,
|
|
29
|
+
actions,
|
|
30
|
+
className,
|
|
31
|
+
...props
|
|
32
|
+
}: ProjectCardProps) {
|
|
33
|
+
const resolvedProject: ProjectSummary = project ?? {
|
|
34
|
+
id: "project",
|
|
35
|
+
name: "Project",
|
|
36
|
+
status: "not-started",
|
|
37
|
+
description: "Project summary"
|
|
38
|
+
};
|
|
39
|
+
const titleId = `${resolvedProject.id}-title`;
|
|
40
|
+
const descriptionId = resolvedProject.description
|
|
41
|
+
? `${resolvedProject.id}-description`
|
|
42
|
+
: undefined;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<article
|
|
46
|
+
{...projectHtmlProps(props)}
|
|
47
|
+
className={classNames(
|
|
48
|
+
"eth-project-card",
|
|
49
|
+
`eth-project-card--${resolvedProject.status}`,
|
|
50
|
+
className
|
|
51
|
+
)}
|
|
52
|
+
data-eth-component="ProjectCard"
|
|
53
|
+
data-status={resolvedProject.status}
|
|
54
|
+
role="region"
|
|
55
|
+
aria-labelledby={titleId}
|
|
56
|
+
aria-describedby={descriptionId}
|
|
57
|
+
tabIndex={-1}
|
|
58
|
+
>
|
|
59
|
+
<header className="eth-project-card__header">
|
|
60
|
+
<div className="eth-project-card__heading">
|
|
61
|
+
<h3 id={titleId}>{resolvedProject.name}</h3>
|
|
62
|
+
{resolvedProject.description ? (
|
|
63
|
+
<p id={descriptionId}>{resolvedProject.description}</p>
|
|
64
|
+
) : null}
|
|
65
|
+
</div>
|
|
66
|
+
<div className="eth-project-card__status">
|
|
67
|
+
<Badge severity={statusToSeverity(resolvedProject.status)}>
|
|
68
|
+
{statusLabel(resolvedProject.status)}
|
|
69
|
+
</Badge>
|
|
70
|
+
</div>
|
|
71
|
+
</header>
|
|
72
|
+
|
|
73
|
+
<dl className="eth-project-card__metadata">
|
|
74
|
+
<div>
|
|
75
|
+
<dt>Owner</dt>
|
|
76
|
+
<dd>{resolvedProject.ownerLabel ?? "Unassigned"}</dd>
|
|
77
|
+
</div>
|
|
78
|
+
<div>
|
|
79
|
+
<dt>Updated</dt>
|
|
80
|
+
<dd>{resolvedProject.updatedAt ?? "Not updated"}</dd>
|
|
81
|
+
</div>
|
|
82
|
+
<div>
|
|
83
|
+
<dt>App domains</dt>
|
|
84
|
+
<dd>{formatNumber(resolvedProject.appDomainsCount)}</dd>
|
|
85
|
+
</div>
|
|
86
|
+
<div>
|
|
87
|
+
<dt>Open tasks</dt>
|
|
88
|
+
<dd>{formatNumber(resolvedProject.openTasksCount)}</dd>
|
|
89
|
+
</div>
|
|
90
|
+
<div>
|
|
91
|
+
<dt>Health</dt>
|
|
92
|
+
<dd>
|
|
93
|
+
<StatusDot
|
|
94
|
+
status={healthToStatus(resolvedProject.healthStatus)}
|
|
95
|
+
label={healthLabel(resolvedProject.healthStatus)}
|
|
96
|
+
/>
|
|
97
|
+
</dd>
|
|
98
|
+
</div>
|
|
99
|
+
</dl>
|
|
100
|
+
|
|
101
|
+
{onSelect || actions?.length ? (
|
|
102
|
+
<footer className="eth-project-card__actions">
|
|
103
|
+
{onSelect ? (
|
|
104
|
+
<Button
|
|
105
|
+
intent="secondary"
|
|
106
|
+
density="compact"
|
|
107
|
+
onClick={() => onSelect(resolvedProject.id)}
|
|
108
|
+
>
|
|
109
|
+
Open project
|
|
110
|
+
</Button>
|
|
111
|
+
) : null}
|
|
112
|
+
<ActionGroup actions={actions} />
|
|
113
|
+
</footer>
|
|
114
|
+
) : null}
|
|
115
|
+
</article>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { ProjectCreateForm } from "./ProjectCreateForm";
|
|
4
|
+
|
|
5
|
+
describe("ProjectCreateForm", () => {
|
|
6
|
+
it("renders a named create form with supporting description", () => {
|
|
7
|
+
render(<ProjectCreateForm />);
|
|
8
|
+
|
|
9
|
+
const form = screen.getByRole("form", { name: "Create project" });
|
|
10
|
+
const description = screen.getByText(
|
|
11
|
+
"Start a project workspace with an owner, template, and operating context."
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
expect(form.getAttribute("aria-describedby")).toBe(description.id);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("validates required name and submits trimmed project values", () => {
|
|
18
|
+
const onSubmit = vi.fn();
|
|
19
|
+
render(
|
|
20
|
+
<ProjectCreateForm
|
|
21
|
+
defaults={{ description: "Initial scope", ownerLabel: "Jane Doe" }}
|
|
22
|
+
onSubmit={onSubmit}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
fireEvent.click(screen.getByRole("button", { name: "Create project" }));
|
|
27
|
+
expect(screen.getAllByText("Project name is required.")).toHaveLength(2);
|
|
28
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
29
|
+
|
|
30
|
+
fireEvent.change(screen.getByLabelText("Name"), {
|
|
31
|
+
target: { value: " Q3 launch workspace " }
|
|
32
|
+
});
|
|
33
|
+
fireEvent.change(screen.getByLabelText("Template"), {
|
|
34
|
+
target: { value: "software-delivery" }
|
|
35
|
+
});
|
|
36
|
+
fireEvent.click(screen.getByRole("button", { name: "Create project" }));
|
|
37
|
+
|
|
38
|
+
expect(onSubmit).toHaveBeenCalledWith({
|
|
39
|
+
name: "Q3 launch workspace",
|
|
40
|
+
description: "Initial scope",
|
|
41
|
+
ownerLabel: "Jane Doe",
|
|
42
|
+
template: "software-delivery"
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
FormField,
|
|
5
|
+
FormSection,
|
|
6
|
+
FormValidationMessage,
|
|
7
|
+
Select,
|
|
8
|
+
TextInput,
|
|
9
|
+
Textarea
|
|
10
|
+
} from "@echothink-ui/core";
|
|
11
|
+
import type { ProjectBaseProps, ProjectCreateValues } from "../types";
|
|
12
|
+
import { classNames, projectHtmlProps } from "../utils";
|
|
13
|
+
|
|
14
|
+
export interface ProjectCreateFormProps extends ProjectBaseProps {
|
|
15
|
+
defaults?: Partial<ProjectCreateValues>;
|
|
16
|
+
onSubmit?: (values: ProjectCreateValues) => void;
|
|
17
|
+
onCancel?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const templateOptions = [
|
|
21
|
+
{ value: "blank", label: "Blank project" },
|
|
22
|
+
{ value: "software-delivery", label: "Software delivery" },
|
|
23
|
+
{ value: "research", label: "Research workspace" },
|
|
24
|
+
{ value: "operations", label: "Operations runbook" }
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function ProjectCreateForm({
|
|
28
|
+
defaults,
|
|
29
|
+
onSubmit,
|
|
30
|
+
onCancel,
|
|
31
|
+
title = "Create project",
|
|
32
|
+
description = "Start a project workspace with an owner, template, and operating context.",
|
|
33
|
+
className,
|
|
34
|
+
...props
|
|
35
|
+
}: ProjectCreateFormProps) {
|
|
36
|
+
const [values, setValues] = React.useState<ProjectCreateValues>({
|
|
37
|
+
name: defaults?.name ?? "",
|
|
38
|
+
description: defaults?.description ?? "",
|
|
39
|
+
ownerLabel: defaults?.ownerLabel ?? "",
|
|
40
|
+
template: defaults?.template ?? "blank"
|
|
41
|
+
});
|
|
42
|
+
const [nameError, setNameError] = React.useState<string>();
|
|
43
|
+
const formId = React.useId().replace(/:/g, "");
|
|
44
|
+
const titleId = `eth-project-create-form-${formId}-title`;
|
|
45
|
+
const descriptionId = `eth-project-create-form-${formId}-description`;
|
|
46
|
+
const htmlProps = projectHtmlProps(props);
|
|
47
|
+
const describedBy =
|
|
48
|
+
[htmlProps["aria-describedby"], description ? descriptionId : undefined]
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.join(" ") || undefined;
|
|
51
|
+
|
|
52
|
+
const update = (field: keyof ProjectCreateValues, value: string) => {
|
|
53
|
+
setValues((current) => ({ ...current, [field]: value }));
|
|
54
|
+
if (field === "name" && value.trim()) setNameError(undefined);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const submit = (event: React.FormEvent<HTMLFormElement>) => {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
if (!values.name.trim()) {
|
|
60
|
+
setNameError("Project name is required.");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
onSubmit?.({ ...values, name: values.name.trim() });
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<form
|
|
68
|
+
{...htmlProps}
|
|
69
|
+
className={classNames("eth-project-create-form", className)}
|
|
70
|
+
data-eth-component="ProjectCreateForm"
|
|
71
|
+
aria-labelledby={htmlProps["aria-labelledby"] ?? (title ? titleId : undefined)}
|
|
72
|
+
aria-describedby={describedBy}
|
|
73
|
+
onSubmit={submit}
|
|
74
|
+
noValidate
|
|
75
|
+
>
|
|
76
|
+
{title || description ? (
|
|
77
|
+
<header className="eth-project-create-form__header">
|
|
78
|
+
{title ? (
|
|
79
|
+
<h2 id={titleId} className="eth-project-create-form__title">
|
|
80
|
+
{title}
|
|
81
|
+
</h2>
|
|
82
|
+
) : null}
|
|
83
|
+
{description ? (
|
|
84
|
+
<p id={descriptionId} className="eth-project-create-form__description">
|
|
85
|
+
{description}
|
|
86
|
+
</p>
|
|
87
|
+
) : null}
|
|
88
|
+
</header>
|
|
89
|
+
) : null}
|
|
90
|
+
|
|
91
|
+
{nameError ? (
|
|
92
|
+
<FormValidationMessage className="eth-project-create-form__validation">
|
|
93
|
+
{nameError}
|
|
94
|
+
</FormValidationMessage>
|
|
95
|
+
) : null}
|
|
96
|
+
|
|
97
|
+
<FormSection
|
|
98
|
+
className="eth-project-create-form__section"
|
|
99
|
+
title="Project details"
|
|
100
|
+
description="Define the visible identity and initial scope for the workspace."
|
|
101
|
+
>
|
|
102
|
+
<div className="eth-project-create-form__grid">
|
|
103
|
+
<FormField
|
|
104
|
+
id="project-name"
|
|
105
|
+
label="Name"
|
|
106
|
+
helperText="Shown in project lists, tabs, and approval trails."
|
|
107
|
+
required
|
|
108
|
+
error={nameError}
|
|
109
|
+
className="eth-project-create-form__field"
|
|
110
|
+
>
|
|
111
|
+
<TextInput
|
|
112
|
+
value={values.name}
|
|
113
|
+
onChange={(event) => update("name", event.currentTarget.value)}
|
|
114
|
+
autoFocus
|
|
115
|
+
/>
|
|
116
|
+
</FormField>
|
|
117
|
+
<FormField
|
|
118
|
+
id="project-owner"
|
|
119
|
+
label="Owner"
|
|
120
|
+
helperText="Person or group accountable for project changes."
|
|
121
|
+
className="eth-project-create-form__field"
|
|
122
|
+
>
|
|
123
|
+
<TextInput
|
|
124
|
+
value={values.ownerLabel}
|
|
125
|
+
onChange={(event) => update("ownerLabel", event.currentTarget.value)}
|
|
126
|
+
/>
|
|
127
|
+
</FormField>
|
|
128
|
+
<FormField
|
|
129
|
+
id="project-description"
|
|
130
|
+
label="Description"
|
|
131
|
+
helperText="Summarize the purpose, key outputs, or operating window."
|
|
132
|
+
className="eth-project-create-form__field eth-project-create-form__field--wide"
|
|
133
|
+
>
|
|
134
|
+
<Textarea
|
|
135
|
+
value={values.description}
|
|
136
|
+
onChange={(event) => update("description", event.currentTarget.value)}
|
|
137
|
+
rows={4}
|
|
138
|
+
/>
|
|
139
|
+
</FormField>
|
|
140
|
+
</div>
|
|
141
|
+
</FormSection>
|
|
142
|
+
|
|
143
|
+
<FormSection
|
|
144
|
+
className="eth-project-create-form__section"
|
|
145
|
+
title="Setup"
|
|
146
|
+
description="Select the starting structure for tasks, resources, and app-domain links."
|
|
147
|
+
>
|
|
148
|
+
<div className="eth-project-create-form__grid eth-project-create-form__grid--setup">
|
|
149
|
+
<FormField
|
|
150
|
+
id="project-template"
|
|
151
|
+
label="Template"
|
|
152
|
+
helperText="Templates prefill common sections without locking future changes."
|
|
153
|
+
className="eth-project-create-form__field"
|
|
154
|
+
>
|
|
155
|
+
<Select
|
|
156
|
+
options={templateOptions}
|
|
157
|
+
value={values.template}
|
|
158
|
+
onChange={(event) => update("template", event.currentTarget.value)}
|
|
159
|
+
/>
|
|
160
|
+
</FormField>
|
|
161
|
+
</div>
|
|
162
|
+
</FormSection>
|
|
163
|
+
|
|
164
|
+
<footer className="eth-project-create-form__actions">
|
|
165
|
+
{onCancel ? (
|
|
166
|
+
<Button intent="secondary" onClick={onCancel}>
|
|
167
|
+
Cancel
|
|
168
|
+
</Button>
|
|
169
|
+
) : null}
|
|
170
|
+
<Button type="submit" intent="primary">
|
|
171
|
+
Create project
|
|
172
|
+
</Button>
|
|
173
|
+
</footer>
|
|
174
|
+
</form>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { StatusDot, type EthAction } from "@echothink-ui/core";
|
|
3
|
+
import { KPIBlock } from "@echothink-ui/charts";
|
|
4
|
+
import { PageHeader } from "@echothink-ui/layouts";
|
|
5
|
+
import type {
|
|
6
|
+
ProjectActivityEvent,
|
|
7
|
+
ProjectAppDomainInstance,
|
|
8
|
+
ProjectBaseProps,
|
|
9
|
+
ProjectResource,
|
|
10
|
+
ProjectSummary
|
|
11
|
+
} from "../types";
|
|
12
|
+
import { projectHtmlProps } from "../utils";
|
|
13
|
+
import { ProjectActivityTimeline } from "./ProjectActivityTimeline";
|
|
14
|
+
import { ProjectAppDomainPanel } from "./ProjectAppDomainPanel";
|
|
15
|
+
import { ProjectResourcePanel } from "./ProjectResourcePanel";
|
|
16
|
+
import { ProjectStatusSummary } from "./ProjectStatusSummary";
|
|
17
|
+
|
|
18
|
+
export interface ProjectDashboardTemplateProps extends ProjectBaseProps {
|
|
19
|
+
project?: ProjectSummary;
|
|
20
|
+
projects?: ProjectSummary[];
|
|
21
|
+
events?: ProjectActivityEvent[];
|
|
22
|
+
resources?: ProjectResource[];
|
|
23
|
+
instances?: ProjectAppDomainInstance[];
|
|
24
|
+
actions?: EthAction[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const demoProject: ProjectSummary = {
|
|
28
|
+
id: "project-demo",
|
|
29
|
+
name: "Project dashboard",
|
|
30
|
+
status: "in-progress",
|
|
31
|
+
description: "Project overview for resources, tasks, activity, and app domains.",
|
|
32
|
+
ownerLabel: "Operations",
|
|
33
|
+
updatedAt: "Today",
|
|
34
|
+
appDomainsCount: 2,
|
|
35
|
+
openTasksCount: 7,
|
|
36
|
+
healthStatus: "healthy"
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const demoEvents: ProjectActivityEvent[] = [
|
|
40
|
+
{
|
|
41
|
+
id: "event-1",
|
|
42
|
+
timestamp: "2026-05-27 09:20",
|
|
43
|
+
actor: "EchoThink",
|
|
44
|
+
kind: "resource",
|
|
45
|
+
summary: "Resource inventory refreshed"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "event-2",
|
|
49
|
+
timestamp: "2026-05-27 10:05",
|
|
50
|
+
actor: "Project lead",
|
|
51
|
+
kind: "app-domain",
|
|
52
|
+
summary: "Analytics domain configured"
|
|
53
|
+
}
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const demoResources: ProjectResource[] = [
|
|
57
|
+
{ id: "res-1", label: "Launch brief", kind: "document", status: "synced", updatedAt: "Today" },
|
|
58
|
+
{ id: "res-2", label: "Task backlog", kind: "table", status: "in-progress", updatedAt: "Today" }
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const demoInstances: ProjectAppDomainInstance[] = [
|
|
62
|
+
{ id: "inst-1", appDomainLabel: "Analytics", status: "running", health: "healthy" },
|
|
63
|
+
{ id: "inst-2", appDomainLabel: "Document review", status: "active", health: "warning" }
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
export function ProjectDashboardTemplate({
|
|
67
|
+
project = demoProject,
|
|
68
|
+
projects,
|
|
69
|
+
events = demoEvents,
|
|
70
|
+
resources = demoResources,
|
|
71
|
+
instances = demoInstances,
|
|
72
|
+
className,
|
|
73
|
+
actions,
|
|
74
|
+
...props
|
|
75
|
+
}: ProjectDashboardTemplateProps) {
|
|
76
|
+
const projectSet = projects ?? [project];
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<main
|
|
80
|
+
{...projectHtmlProps(props)}
|
|
81
|
+
className={`eth-project-dashboard-template ${className ?? ""}`}
|
|
82
|
+
data-eth-component="ProjectDashboardTemplate"
|
|
83
|
+
>
|
|
84
|
+
<PageHeader
|
|
85
|
+
title={project.name}
|
|
86
|
+
subtitle={project.description}
|
|
87
|
+
status={project.status}
|
|
88
|
+
actions={actions}
|
|
89
|
+
/>
|
|
90
|
+
<div className="eth-project-dashboard-template__kpis" role="region" aria-label="Project KPIs">
|
|
91
|
+
<KPIBlock title="Open tasks" value={project.openTasksCount ?? 0} />
|
|
92
|
+
<KPIBlock title="App domains" value={project.appDomainsCount ?? 0} />
|
|
93
|
+
<KPIBlock
|
|
94
|
+
title="Health"
|
|
95
|
+
value={project.healthStatus ?? "healthy"}
|
|
96
|
+
status={<StatusDot status="synced" label="Operational" />}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
<ProjectStatusSummary projects={projectSet} />
|
|
100
|
+
<div className="eth-project-dashboard-template__grid">
|
|
101
|
+
<ProjectActivityTimeline events={events} />
|
|
102
|
+
<ProjectResourcePanel resources={resources} />
|
|
103
|
+
<ProjectAppDomainPanel instances={instances} />
|
|
104
|
+
</div>
|
|
105
|
+
</main>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Button, EmptyState, type EthAction } from "@echothink-ui/core";
|
|
3
|
+
import { PageHeader } from "@echothink-ui/layouts";
|
|
4
|
+
import type { ProjectBaseProps, ProjectSummary } from "../types";
|
|
5
|
+
import { projectHtmlProps } from "../utils";
|
|
6
|
+
import { ProjectStatusSummary } from "./ProjectStatusSummary";
|
|
7
|
+
import { ProjectTable } from "./ProjectTable";
|
|
8
|
+
|
|
9
|
+
export interface ProjectManagementPageProps extends ProjectBaseProps {
|
|
10
|
+
projects?: ProjectSummary[];
|
|
11
|
+
onCreate?: () => void;
|
|
12
|
+
onSelect?: (projectId: string) => void;
|
|
13
|
+
onFilterSelect?: (filterId: string) => void;
|
|
14
|
+
filters?: Array<{ id: string; label: React.ReactNode; active?: boolean }>;
|
|
15
|
+
activeProjectId?: string;
|
|
16
|
+
actions?: EthAction[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ProjectManagementPage({
|
|
20
|
+
projects = [],
|
|
21
|
+
onCreate,
|
|
22
|
+
onSelect,
|
|
23
|
+
onFilterSelect,
|
|
24
|
+
filters = [],
|
|
25
|
+
activeProjectId,
|
|
26
|
+
title = "Projects",
|
|
27
|
+
subtitle = "Manage project workspaces, resources, and installed app domains.",
|
|
28
|
+
className,
|
|
29
|
+
actions,
|
|
30
|
+
...props
|
|
31
|
+
}: ProjectManagementPageProps) {
|
|
32
|
+
const headerActions = React.useMemo<EthAction[]>(() => {
|
|
33
|
+
const providedActions = actions ?? [];
|
|
34
|
+
const hasCreateAction = providedActions.some((action) => {
|
|
35
|
+
if (action.id === "new-project") return true;
|
|
36
|
+
return (
|
|
37
|
+
typeof action.label === "string" &&
|
|
38
|
+
action.label.trim().toLowerCase() === "new project"
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!onCreate || hasCreateAction) return providedActions;
|
|
43
|
+
|
|
44
|
+
return [
|
|
45
|
+
...providedActions,
|
|
46
|
+
{
|
|
47
|
+
id: "new-project",
|
|
48
|
+
label: "New project",
|
|
49
|
+
intent: "primary",
|
|
50
|
+
onSelect: onCreate
|
|
51
|
+
}
|
|
52
|
+
];
|
|
53
|
+
}, [actions, onCreate]);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<main
|
|
57
|
+
{...projectHtmlProps(props)}
|
|
58
|
+
className={`eth-project-management-page ${className ?? ""}`}
|
|
59
|
+
data-eth-component="ProjectManagementPage"
|
|
60
|
+
>
|
|
61
|
+
<PageHeader title={title} subtitle={subtitle} actions={headerActions} />
|
|
62
|
+
|
|
63
|
+
{filters.length ? (
|
|
64
|
+
<div
|
|
65
|
+
className="eth-project-management-page__filters"
|
|
66
|
+
role="group"
|
|
67
|
+
aria-label="Project filters"
|
|
68
|
+
>
|
|
69
|
+
{filters.map((filter) => (
|
|
70
|
+
<button
|
|
71
|
+
key={filter.id}
|
|
72
|
+
type="button"
|
|
73
|
+
className={`eth-project-management-page__filter ${
|
|
74
|
+
filter.active ? "eth-project-management-page__filter--active" : ""
|
|
75
|
+
}`}
|
|
76
|
+
aria-pressed={Boolean(filter.active)}
|
|
77
|
+
aria-disabled={!onFilterSelect ? true : undefined}
|
|
78
|
+
tabIndex={!onFilterSelect ? -1 : undefined}
|
|
79
|
+
onClick={onFilterSelect ? () => onFilterSelect(filter.id) : undefined}
|
|
80
|
+
>
|
|
81
|
+
{filter.label}
|
|
82
|
+
</button>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
) : null}
|
|
86
|
+
|
|
87
|
+
{projects.length ? (
|
|
88
|
+
<>
|
|
89
|
+
<ProjectStatusSummary projects={projects} />
|
|
90
|
+
<ProjectTable
|
|
91
|
+
projects={projects}
|
|
92
|
+
onRowSelect={onSelect}
|
|
93
|
+
selectedProjectId={activeProjectId}
|
|
94
|
+
aria-label={activeProjectId ? `Projects, active ${activeProjectId}` : "Projects"}
|
|
95
|
+
/>
|
|
96
|
+
</>
|
|
97
|
+
) : (
|
|
98
|
+
<EmptyState
|
|
99
|
+
title="No projects yet"
|
|
100
|
+
description="Create a project to connect resources, members, model settings, and app domains."
|
|
101
|
+
action={
|
|
102
|
+
onCreate ? (
|
|
103
|
+
<Button intent="primary" onClick={onCreate}>
|
|
104
|
+
New project
|
|
105
|
+
</Button>
|
|
106
|
+
) : undefined
|
|
107
|
+
}
|
|
108
|
+
/>
|
|
109
|
+
)}
|
|
110
|
+
</main>
|
|
111
|
+
);
|
|
112
|
+
}
|