@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,165 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Badge,
|
|
4
|
+
StatusDot,
|
|
5
|
+
statusLabel,
|
|
6
|
+
type EthOperationalStatus,
|
|
7
|
+
type EthSeverity
|
|
8
|
+
} from "@echothink-ui/core";
|
|
9
|
+
import { KPIBlock } from "@echothink-ui/charts";
|
|
10
|
+
import type { ProjectBaseProps, ProjectStatusSummaryData, ProjectSummary } from "../types";
|
|
11
|
+
import { classNames, projectHtmlProps, summarizeProjects } from "../utils";
|
|
12
|
+
|
|
13
|
+
export interface ProjectStatusSummaryProps extends ProjectBaseProps {
|
|
14
|
+
projects?: ProjectSummary[];
|
|
15
|
+
summary?: ProjectStatusSummaryData;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const priorityStatuses: EthOperationalStatus[] = [
|
|
19
|
+
"running",
|
|
20
|
+
"in-progress",
|
|
21
|
+
"approval-required",
|
|
22
|
+
"blocked",
|
|
23
|
+
"failed",
|
|
24
|
+
"queued",
|
|
25
|
+
"pending-approval",
|
|
26
|
+
"warning",
|
|
27
|
+
"stale",
|
|
28
|
+
"paused",
|
|
29
|
+
"completed",
|
|
30
|
+
"succeeded",
|
|
31
|
+
"synced",
|
|
32
|
+
"active",
|
|
33
|
+
"not-started",
|
|
34
|
+
"inactive"
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const statusClassNames: Partial<Record<EthOperationalStatus, string>> = {
|
|
38
|
+
"approval-required": "eth-project-status-summary__bar-segment--attention",
|
|
39
|
+
"pending-approval": "eth-project-status-summary__bar-segment--warning",
|
|
40
|
+
"in-progress": "eth-project-status-summary__bar-segment--inflight",
|
|
41
|
+
blocked: "eth-project-status-summary__bar-segment--attention",
|
|
42
|
+
failed: "eth-project-status-summary__bar-segment--attention",
|
|
43
|
+
running: "eth-project-status-summary__bar-segment--inflight",
|
|
44
|
+
queued: "eth-project-status-summary__bar-segment--queued",
|
|
45
|
+
warning: "eth-project-status-summary__bar-segment--warning",
|
|
46
|
+
stale: "eth-project-status-summary__bar-segment--warning",
|
|
47
|
+
paused: "eth-project-status-summary__bar-segment--paused",
|
|
48
|
+
completed: "eth-project-status-summary__bar-segment--complete",
|
|
49
|
+
succeeded: "eth-project-status-summary__bar-segment--complete",
|
|
50
|
+
synced: "eth-project-status-summary__bar-segment--complete",
|
|
51
|
+
active: "eth-project-status-summary__bar-segment--complete",
|
|
52
|
+
"not-started": "eth-project-status-summary__bar-segment--queued",
|
|
53
|
+
inactive: "eth-project-status-summary__bar-segment--queued"
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function ProjectStatusSummary({
|
|
57
|
+
projects = [],
|
|
58
|
+
summary,
|
|
59
|
+
title = "Project status",
|
|
60
|
+
className,
|
|
61
|
+
...props
|
|
62
|
+
}: ProjectStatusSummaryProps) {
|
|
63
|
+
const headingId = React.useId();
|
|
64
|
+
const computed = summary ?? summarizeProjects(projects);
|
|
65
|
+
const total = Object.values(computed.byStatus).reduce((sum, value) => sum + (value ?? 0), 0);
|
|
66
|
+
const activeCount = (computed.byStatus.running ?? 0) + (computed.byStatus["in-progress"] ?? 0);
|
|
67
|
+
const prioritizedStatuses = priorityStatuses.filter((status) => computed.byStatus[status]);
|
|
68
|
+
const additionalStatuses = Object.keys(computed.byStatus).filter(
|
|
69
|
+
(status): status is EthOperationalStatus =>
|
|
70
|
+
!priorityStatuses.includes(status as EthOperationalStatus) &&
|
|
71
|
+
Boolean(computed.byStatus[status as EthOperationalStatus])
|
|
72
|
+
);
|
|
73
|
+
const displayedStatuses = [...prioritizedStatuses, ...additionalStatuses];
|
|
74
|
+
const statusSummary = total
|
|
75
|
+
? `${total} project${total === 1 ? "" : "s"} across ${displayedStatuses.length} status${
|
|
76
|
+
displayedStatuses.length === 1 ? "" : "es"
|
|
77
|
+
}`
|
|
78
|
+
: "No tracked projects";
|
|
79
|
+
const attentionLabel = total
|
|
80
|
+
? computed.blockedCount
|
|
81
|
+
? `${computed.blockedCount} needs attention`
|
|
82
|
+
: "Operational"
|
|
83
|
+
: "No projects";
|
|
84
|
+
const attentionSeverity: EthSeverity = total
|
|
85
|
+
? computed.blockedCount
|
|
86
|
+
? "danger"
|
|
87
|
+
: "success"
|
|
88
|
+
: "neutral";
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<section
|
|
92
|
+
{...projectHtmlProps(props)}
|
|
93
|
+
className={classNames("eth-project-status-summary", className)}
|
|
94
|
+
data-eth-component="ProjectStatusSummary"
|
|
95
|
+
role="region"
|
|
96
|
+
aria-labelledby={headingId}
|
|
97
|
+
tabIndex={-1}
|
|
98
|
+
>
|
|
99
|
+
<header className="eth-project-status-summary__header">
|
|
100
|
+
<div className="eth-project-status-summary__heading">
|
|
101
|
+
<h3 id={headingId}>{title}</h3>
|
|
102
|
+
<p>{statusSummary}</p>
|
|
103
|
+
</div>
|
|
104
|
+
<Badge severity={attentionSeverity}>{attentionLabel}</Badge>
|
|
105
|
+
</header>
|
|
106
|
+
|
|
107
|
+
<div className="eth-project-status-summary__kpis">
|
|
108
|
+
<KPIBlock title="Projects" value={total} description="Total tracked" />
|
|
109
|
+
<KPIBlock title="Healthy" value={`${computed.healthyPercent}%`} description="Operational" />
|
|
110
|
+
<KPIBlock title="Active" value={activeCount} description="Running or in progress" />
|
|
111
|
+
<KPIBlock title="Blocked" value={computed.blockedCount} description="Needs attention" />
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div className="eth-project-status-summary__distribution">
|
|
115
|
+
<div
|
|
116
|
+
className="eth-project-status-summary__bar"
|
|
117
|
+
aria-label="Project status distribution"
|
|
118
|
+
role="list"
|
|
119
|
+
>
|
|
120
|
+
{displayedStatuses.length ? (
|
|
121
|
+
displayedStatuses.map((status) => {
|
|
122
|
+
const count = computed.byStatus[status] ?? 0;
|
|
123
|
+
return (
|
|
124
|
+
<span
|
|
125
|
+
key={status}
|
|
126
|
+
aria-label={`${statusLabel(status)}: ${count}`}
|
|
127
|
+
className={classNames(
|
|
128
|
+
"eth-project-status-summary__bar-segment",
|
|
129
|
+
statusClassNames[status],
|
|
130
|
+
`eth-project-status-summary__bar-segment--${status}`
|
|
131
|
+
)}
|
|
132
|
+
role="listitem"
|
|
133
|
+
style={{ flexGrow: count }}
|
|
134
|
+
title={`${statusLabel(status)}: ${count}`}
|
|
135
|
+
/>
|
|
136
|
+
);
|
|
137
|
+
})
|
|
138
|
+
) : (
|
|
139
|
+
<span
|
|
140
|
+
aria-label="No project status data"
|
|
141
|
+
className="eth-project-status-summary__bar-segment"
|
|
142
|
+
role="listitem"
|
|
143
|
+
style={{ flexGrow: 1 }}
|
|
144
|
+
/>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{displayedStatuses.length ? (
|
|
149
|
+
<div className="eth-project-status-summary__legend" role="list">
|
|
150
|
+
{displayedStatuses.map((status) => (
|
|
151
|
+
<span key={status} role="listitem">
|
|
152
|
+
<StatusDot
|
|
153
|
+
status={status}
|
|
154
|
+
label={`${statusLabel(status)} ${computed.byStatus[status]}`}
|
|
155
|
+
/>
|
|
156
|
+
</span>
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
) : (
|
|
160
|
+
<p className="eth-project-status-summary__empty">No project status data</p>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
</section>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { ProjectSummaryPanel } from "./ProjectSummaryPanel";
|
|
4
|
+
|
|
5
|
+
describe("@echothink-ui/project ProjectSummaryPanel", () => {
|
|
6
|
+
it("renders an accessible inspector summary with status and metadata", () => {
|
|
7
|
+
render(
|
|
8
|
+
<ProjectSummaryPanel
|
|
9
|
+
project={{
|
|
10
|
+
id: "p1",
|
|
11
|
+
name: "Marketing Campaign",
|
|
12
|
+
status: "in-progress",
|
|
13
|
+
description: "Q3 product launch and lifecycle messaging.",
|
|
14
|
+
ownerLabel: "JD",
|
|
15
|
+
updatedAt: "May 27",
|
|
16
|
+
appDomainsCount: 4,
|
|
17
|
+
openTasksCount: 12,
|
|
18
|
+
healthStatus: "healthy"
|
|
19
|
+
}}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const panel = screen.getByRole("region", { name: "Marketing Campaign" });
|
|
24
|
+
|
|
25
|
+
expect(panel.className).toContain("eth-project-summary-panel--in-progress");
|
|
26
|
+
expect(panel.getAttribute("data-status")).toBe("in-progress");
|
|
27
|
+
expect(panel.getAttribute("aria-describedby")).toBe("p1-summary-description");
|
|
28
|
+
expect(screen.getByText("Q3 product launch and lifecycle messaging.").id).toBe(
|
|
29
|
+
"p1-summary-description"
|
|
30
|
+
);
|
|
31
|
+
expect(screen.getByText("Project ID")).toBeTruthy();
|
|
32
|
+
expect(screen.getByText("p1")).toBeTruthy();
|
|
33
|
+
expect(screen.getByText("Open tasks")).toBeTruthy();
|
|
34
|
+
expect(screen.getByText("12")).toBeTruthy();
|
|
35
|
+
expect(screen.getByText("Healthy")).toBeTruthy();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Badge, StatusDot, statusLabel } from "@echothink-ui/core";
|
|
3
|
+
import type { ProjectBaseProps, ProjectSummary } from "../types";
|
|
4
|
+
import {
|
|
5
|
+
classNames,
|
|
6
|
+
formatNumber,
|
|
7
|
+
healthLabel,
|
|
8
|
+
healthToStatus,
|
|
9
|
+
projectHtmlProps,
|
|
10
|
+
statusToSeverity
|
|
11
|
+
} from "../utils";
|
|
12
|
+
|
|
13
|
+
export interface ProjectSummaryPanelProps extends ProjectBaseProps {
|
|
14
|
+
project?: ProjectSummary;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ProjectSummaryPanel({ project, className, ...props }: ProjectSummaryPanelProps) {
|
|
18
|
+
const resolvedProject: ProjectSummary = project ?? {
|
|
19
|
+
id: "project",
|
|
20
|
+
name: "Project",
|
|
21
|
+
status: "not-started",
|
|
22
|
+
description: "Project summary"
|
|
23
|
+
};
|
|
24
|
+
const titleId = `${resolvedProject.id}-summary-title`;
|
|
25
|
+
const descriptionId = resolvedProject.description
|
|
26
|
+
? `${resolvedProject.id}-summary-description`
|
|
27
|
+
: undefined;
|
|
28
|
+
const metadataItems = [
|
|
29
|
+
{ label: "Project ID", value: resolvedProject.id },
|
|
30
|
+
{ label: "Owner", value: resolvedProject.ownerLabel ?? "Unassigned" },
|
|
31
|
+
{ label: "Updated", value: resolvedProject.updatedAt ?? "Not updated" },
|
|
32
|
+
{ label: "App domains", value: formatNumber(resolvedProject.appDomainsCount) },
|
|
33
|
+
{ label: "Open tasks", value: formatNumber(resolvedProject.openTasksCount) }
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<section
|
|
38
|
+
{...projectHtmlProps(props)}
|
|
39
|
+
className={classNames(
|
|
40
|
+
"eth-project-summary-panel",
|
|
41
|
+
`eth-project-summary-panel--${resolvedProject.status}`,
|
|
42
|
+
className
|
|
43
|
+
)}
|
|
44
|
+
data-eth-component="ProjectSummaryPanel"
|
|
45
|
+
data-status={resolvedProject.status}
|
|
46
|
+
role="region"
|
|
47
|
+
aria-labelledby={titleId}
|
|
48
|
+
aria-describedby={descriptionId}
|
|
49
|
+
tabIndex={-1}
|
|
50
|
+
>
|
|
51
|
+
<header className="eth-project-summary-panel__header">
|
|
52
|
+
<div className="eth-project-summary-panel__heading">
|
|
53
|
+
<h3 id={titleId}>{resolvedProject.name}</h3>
|
|
54
|
+
{resolvedProject.description ? (
|
|
55
|
+
<p id={descriptionId}>{resolvedProject.description}</p>
|
|
56
|
+
) : null}
|
|
57
|
+
</div>
|
|
58
|
+
<div className="eth-project-summary-panel__status">
|
|
59
|
+
<Badge severity={statusToSeverity(resolvedProject.status)}>
|
|
60
|
+
{statusLabel(resolvedProject.status)}
|
|
61
|
+
</Badge>
|
|
62
|
+
</div>
|
|
63
|
+
</header>
|
|
64
|
+
<dl className="eth-project-summary-panel__metadata">
|
|
65
|
+
{metadataItems.map((item) => (
|
|
66
|
+
<div className="eth-project-summary-panel__metadata-item" key={item.label}>
|
|
67
|
+
<dt>{item.label}</dt>
|
|
68
|
+
<dd>{item.value}</dd>
|
|
69
|
+
</div>
|
|
70
|
+
))}
|
|
71
|
+
<div
|
|
72
|
+
className="eth-project-summary-panel__metadata-item eth-project-summary-panel__metadata-item--health"
|
|
73
|
+
>
|
|
74
|
+
<dt>Health</dt>
|
|
75
|
+
<dd>
|
|
76
|
+
<StatusDot
|
|
77
|
+
status={healthToStatus(resolvedProject.healthStatus)}
|
|
78
|
+
label={healthLabel(resolvedProject.healthStatus)}
|
|
79
|
+
/>
|
|
80
|
+
</dd>
|
|
81
|
+
</div>
|
|
82
|
+
</dl>
|
|
83
|
+
</section>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { BrowserTab, type BrowserTabProps } from "@echothink-ui/layouts";
|
|
2
|
+
import { classNames } from "../utils";
|
|
3
|
+
|
|
4
|
+
export type ProjectTabProps = BrowserTabProps;
|
|
5
|
+
|
|
6
|
+
export function ProjectTab({ className, ...props }: ProjectTabProps) {
|
|
7
|
+
return <BrowserTab {...props} className={classNames("eth-project-tab", className)} />;
|
|
8
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { ProjectTabCollection, type ProjectTabItem } from "@echothink-ui/layouts";
|
|
3
|
+
import type { ProjectBaseProps } from "../types";
|
|
4
|
+
import { classNames, projectHtmlProps } from "../utils";
|
|
5
|
+
|
|
6
|
+
export interface ProjectTabGroupProps extends ProjectBaseProps {
|
|
7
|
+
projectId: string;
|
|
8
|
+
projectLabel: React.ReactNode;
|
|
9
|
+
tabs: ProjectTabItem[];
|
|
10
|
+
activeTabId?: string;
|
|
11
|
+
onActivateTab?: (id: string) => void;
|
|
12
|
+
onCloseTab?: (id: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ProjectTabGroup({
|
|
16
|
+
projectId,
|
|
17
|
+
projectLabel,
|
|
18
|
+
tabs,
|
|
19
|
+
activeTabId,
|
|
20
|
+
onActivateTab,
|
|
21
|
+
onCloseTab,
|
|
22
|
+
className,
|
|
23
|
+
...props
|
|
24
|
+
}: ProjectTabGroupProps) {
|
|
25
|
+
return (
|
|
26
|
+
<ProjectTabCollection
|
|
27
|
+
{...projectHtmlProps(props)}
|
|
28
|
+
className={classNames("eth-project-tab-group", className)}
|
|
29
|
+
data-eth-component="ProjectTabGroup"
|
|
30
|
+
projectId={projectId}
|
|
31
|
+
projectLabel={projectLabel}
|
|
32
|
+
tabs={tabs}
|
|
33
|
+
activeTabId={activeTabId}
|
|
34
|
+
onActivateTab={onActivateTab}
|
|
35
|
+
onCloseTab={onCloseTab}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Badge,
|
|
4
|
+
Button,
|
|
5
|
+
EmptyState,
|
|
6
|
+
statusLabel,
|
|
7
|
+
type EthAction
|
|
8
|
+
} from "@echothink-ui/core";
|
|
9
|
+
import { DataTable, type DataColumn } from "@echothink-ui/data";
|
|
10
|
+
import type { ProjectBaseProps, ProjectSummary } from "../types";
|
|
11
|
+
import {
|
|
12
|
+
formatNumber,
|
|
13
|
+
projectHtmlProps,
|
|
14
|
+
statusToSeverity
|
|
15
|
+
} from "../utils";
|
|
16
|
+
|
|
17
|
+
export interface ProjectTableProps extends ProjectBaseProps {
|
|
18
|
+
projects?: ProjectSummary[];
|
|
19
|
+
onRowSelect?: (projectId: string) => void;
|
|
20
|
+
selectedProjectId?: string;
|
|
21
|
+
rowActions?: (project: ProjectSummary) => EthAction[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ProjectTable({
|
|
25
|
+
projects = [],
|
|
26
|
+
onRowSelect,
|
|
27
|
+
selectedProjectId,
|
|
28
|
+
rowActions,
|
|
29
|
+
density = "compact",
|
|
30
|
+
className,
|
|
31
|
+
...props
|
|
32
|
+
}: ProjectTableProps) {
|
|
33
|
+
const columns = React.useMemo<DataColumn<ProjectSummary>[]>(
|
|
34
|
+
() => [
|
|
35
|
+
{
|
|
36
|
+
key: "name",
|
|
37
|
+
header: "Name",
|
|
38
|
+
width: "34%",
|
|
39
|
+
render: (project) => (
|
|
40
|
+
<div className="eth-project-table__name">
|
|
41
|
+
{onRowSelect ? (
|
|
42
|
+
<Button
|
|
43
|
+
className="eth-project-table__title eth-project-table__title-button"
|
|
44
|
+
intent="ghost"
|
|
45
|
+
onClick={() => onRowSelect(project.id)}
|
|
46
|
+
>
|
|
47
|
+
{project.name}
|
|
48
|
+
</Button>
|
|
49
|
+
) : (
|
|
50
|
+
<strong className="eth-project-table__title">{project.name}</strong>
|
|
51
|
+
)}
|
|
52
|
+
{project.description ? (
|
|
53
|
+
<small className="eth-project-table__description">{project.description}</small>
|
|
54
|
+
) : null}
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: "status",
|
|
60
|
+
header: "Status",
|
|
61
|
+
width: "10rem",
|
|
62
|
+
render: (project) => (
|
|
63
|
+
<span className="eth-project-table__status">
|
|
64
|
+
<Badge severity={statusToSeverity(project.status)}>{statusLabel(project.status)}</Badge>
|
|
65
|
+
</span>
|
|
66
|
+
)
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
key: "ownerLabel",
|
|
70
|
+
header: "Owner",
|
|
71
|
+
width: "7rem",
|
|
72
|
+
render: (project) => (
|
|
73
|
+
<span className="eth-project-table__owner">{project.ownerLabel ?? "Unassigned"}</span>
|
|
74
|
+
)
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: "appDomainsCount",
|
|
78
|
+
header: "App domains",
|
|
79
|
+
width: "7.5rem",
|
|
80
|
+
align: "end",
|
|
81
|
+
render: (project) => (
|
|
82
|
+
<span className="eth-project-table__metric">
|
|
83
|
+
{formatNumber(project.appDomainsCount)}
|
|
84
|
+
</span>
|
|
85
|
+
)
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
key: "openTasksCount",
|
|
89
|
+
header: "Open tasks",
|
|
90
|
+
width: "7.5rem",
|
|
91
|
+
align: "end",
|
|
92
|
+
render: (project) => (
|
|
93
|
+
<span className="eth-project-table__metric">
|
|
94
|
+
{formatNumber(project.openTasksCount)}
|
|
95
|
+
</span>
|
|
96
|
+
)
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
key: "updatedAt",
|
|
100
|
+
header: "Updated",
|
|
101
|
+
width: "7.5rem",
|
|
102
|
+
render: (project) => (
|
|
103
|
+
<span className="eth-project-table__updated">{project.updatedAt ?? "-"}</span>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
[onRowSelect]
|
|
108
|
+
);
|
|
109
|
+
const htmlProps = projectHtmlProps(props);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<section
|
|
113
|
+
{...htmlProps}
|
|
114
|
+
className={`eth-project-table ${className ?? ""}`}
|
|
115
|
+
data-eth-component="ProjectTable"
|
|
116
|
+
role="region"
|
|
117
|
+
aria-label={htmlProps["aria-label"] ?? "Project list"}
|
|
118
|
+
tabIndex={-1}
|
|
119
|
+
>
|
|
120
|
+
<DataTable
|
|
121
|
+
aria-label="Project list"
|
|
122
|
+
className="eth-project-table__table"
|
|
123
|
+
density={density}
|
|
124
|
+
rows={projects}
|
|
125
|
+
columns={columns}
|
|
126
|
+
rowKey="id"
|
|
127
|
+
selectedRows={selectedProjectId ? [selectedProjectId] : []}
|
|
128
|
+
rowActions={rowActions}
|
|
129
|
+
emptyState={
|
|
130
|
+
<EmptyState
|
|
131
|
+
title="No projects"
|
|
132
|
+
description="Create a project to start organizing resources and app domains."
|
|
133
|
+
/>
|
|
134
|
+
}
|
|
135
|
+
/>
|
|
136
|
+
</section>
|
|
137
|
+
);
|
|
138
|
+
}
|