Skip to content

Commit 56c0ed4

Browse files
itaispiegelclaude
andcommitted
feat(config-ui): add a Teams page for org data management
DevLake's team dashboards (DORA by Team, Engineering Throughput - Team View, Copilot by Team) join through `teams`, `team_users` and `user_accounts`, but config-ui offered no way to populate them: the org plugin's four CSV endpoints were reachable only by hand-crafting a multipart upload. On a fresh install those tables stay empty and every team dashboard renders blank. Adds a Teams page with three tabs backed by the existing org endpoints, so no backend change is required: - Teams: id, name, alias, sorting index, and parent team as a select of the other teams rather than a raw id. - Members: id, name, email, and team membership as a multi-select that serialises back to the `;`-delimited TeamIds cell. - Account Mapping: every collected tool account, with its identity columns read-only and a select to map it to a member. Rows cannot be added or removed here because accounts come from data collection. Each tab round-trips a whole CSV file, matching the endpoints' existing delete-all-then-save semantics; the page says so in an alert, since a save overwrites rows added elsewhere since the page loaded. Columns the UI does not edit (AvatarUrl, CreatedDate, Status) are preserved by keeping the server's header order and writing back untouched cells. The CSV helpers cover the subset of RFC 4180 that gocsv emits - quoted delimiters, doubled quotes, embedded newlines, CRLF - and are unit tested for round-trip stability. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 0ce8f28 commit 56c0ed4

11 files changed

Lines changed: 629 additions & 0 deletions

File tree

‎config-ui/src/api/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import * as apiKey from './api-key';
2222
import * as auth from './auth';
2323
import * as blueprint from './blueprint';
2424
import * as connection from './connection';
25+
import * as org from './org';
2526
import * as pipeline from './pipeline';
2627
import plugin from './plugin';
2728
import * as project from './project';
@@ -39,6 +40,7 @@ export const API = {
3940
auth,
4041
blueprint,
4142
connection,
43+
org,
4244
pipeline,
4345
plugin,
4446
project,

‎config-ui/src/api/org/index.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
import { request } from '@/utils';
20+
21+
// The org plugin stores teams, users and their account mapping as whole CSV files:
22+
// every PUT replaces the entire table (deleteAll + save), so the UI always sends
23+
// back the complete set of rows rather than a delta.
24+
const upload = (path: string, csv: string, filename: string) => {
25+
const data = new FormData();
26+
data.append('file', new Blob([csv], { type: 'text/csv' }), filename);
27+
return request(path, { method: 'put', data });
28+
};
29+
30+
export const getTeams = (): Promise<string> => request('/plugins/org/teams.csv');
31+
32+
export const updateTeams = (csv: string) => upload('/plugins/org/teams.csv', csv, 'teams.csv');
33+
34+
export const getUsers = (): Promise<string> => request('/plugins/org/users.csv');
35+
36+
export const updateUsers = (csv: string) => upload('/plugins/org/users.csv', csv, 'users.csv');
37+
38+
// Returns every known account (one per connected tool identity) with the UserId it
39+
// is currently mapped to, not only the accounts that already have one.
40+
export const getUserAccountMapping = (): Promise<string> => request('/plugins/org/user_account_mapping.csv');
41+
42+
export const updateUserAccountMapping = (csv: string) =>
43+
upload('/plugins/org/user_account_mapping.csv', csv, 'user_account_mapping.csv');

‎config-ui/src/app/router.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
Pipelines,
3636
Pipeline,
3737
ApiKeys,
38+
Team,
3839
NotFound,
3940
} from '@/routes';
4041

@@ -112,6 +113,10 @@ export const router = createBrowserRouter([
112113
},
113114
],
114115
},
116+
{
117+
path: 'teams',
118+
element: <Team />,
119+
},
115120
{
116121
path: 'keys',
117122
element: <ApiKeys />,

‎config-ui/src/routes/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ export * from './not-found';
2727
export * from './onboard';
2828
export * from './pipeline';
2929
export * from './project';
30+
export * from './team';

‎config-ui/src/routes/layout/config.tsx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
ProjectOutlined,
2222
ExperimentOutlined,
2323
KeyOutlined,
24+
TeamOutlined,
2425
DashboardOutlined,
2526
FileSearchOutlined,
2627
ApiOutlined,
@@ -66,6 +67,11 @@ export const menuItems: MenuItem[] = [
6667
},
6768
],
6869
},
70+
{
71+
key: `${PATH_PREFIX}/teams`,
72+
label: 'Teams',
73+
icon: <TeamOutlined />,
74+
},
6975
{
7076
key: `${PATH_PREFIX}/keys`,
7177
label: 'API Keys',
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
import { DeleteOutlined } from '@ant-design/icons';
20+
import { Table, Input, Select, Button, Tooltip } from 'antd';
21+
22+
export type EditableRow = Record<string, string> & { _key: string };
23+
24+
export type EditableField = {
25+
key: string;
26+
title: string;
27+
type?: 'text' | 'select' | 'multiple';
28+
width?: number;
29+
placeholder?: string;
30+
options?: Array<{ label: string; value: string }>;
31+
// Multi-value fields are stored in the CSV as a single delimited cell.
32+
separator?: string;
33+
editable?: boolean;
34+
invalid?: (row: EditableRow, rows: EditableRow[]) => string | undefined;
35+
};
36+
37+
interface Props {
38+
fields: EditableField[];
39+
rows: EditableRow[];
40+
onChange: (rows: EditableRow[]) => void;
41+
removable?: boolean;
42+
emptyText?: string;
43+
}
44+
45+
export const EditableTable = ({ fields, rows, onChange, removable = true, emptyText }: Props) => {
46+
const update = (key: string, field: string, value: string) =>
47+
onChange(rows.map((row) => (row._key === key ? { ...row, [field]: value } : row)));
48+
49+
const columns = fields.map((field) => ({
50+
title: field.title,
51+
dataIndex: field.key,
52+
width: field.width,
53+
render: (value: string, row: EditableRow) => {
54+
if (field.editable === false) {
55+
return value || <span style={{ color: '#9ca3af' }}>—</span>;
56+
}
57+
58+
if (field.type === 'select' || field.type === 'multiple') {
59+
const separator = field.separator ?? ';';
60+
const multiple = field.type === 'multiple';
61+
return (
62+
<Select
63+
style={{ width: '100%' }}
64+
mode={multiple ? 'multiple' : undefined}
65+
allowClear
66+
showSearch
67+
optionFilterProp="label"
68+
placeholder={field.placeholder}
69+
options={field.options}
70+
value={multiple ? (value ? value.split(separator).filter(Boolean) : []) : value || undefined}
71+
onChange={(next) =>
72+
update(
73+
row._key,
74+
field.key,
75+
multiple ? ((next as string[]) ?? []).join(separator) : ((next as string) ?? ''),
76+
)
77+
}
78+
/>
79+
);
80+
}
81+
82+
const error = field.invalid?.(row, rows);
83+
return (
84+
<Tooltip title={error} open={error ? undefined : false}>
85+
<Input
86+
status={error ? 'error' : undefined}
87+
placeholder={field.placeholder}
88+
value={value}
89+
onChange={(e) => update(row._key, field.key, e.target.value)}
90+
/>
91+
</Tooltip>
92+
);
93+
},
94+
}));
95+
96+
if (removable) {
97+
columns.push({
98+
title: '',
99+
dataIndex: '_actions',
100+
width: 50,
101+
render: (_: string, row: EditableRow) => (
102+
<Button
103+
type="text"
104+
icon={<DeleteOutlined />}
105+
onClick={() => onChange(rows.filter((it) => it._key !== row._key))}
106+
/>
107+
),
108+
});
109+
}
110+
111+
return (
112+
<Table
113+
rowKey="_key"
114+
size="small"
115+
columns={columns}
116+
dataSource={rows}
117+
pagination={false}
118+
locale={{ emptyText }}
119+
scroll={{ x: true }}
120+
/>
121+
);
122+
};

‎config-ui/src/routes/team/index.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
export * from './team';

0 commit comments

Comments
 (0)