From 56c0ed4a045e6aaafa7ff536cffee616bb6a0748 Mon Sep 17 00:00:00 2001 From: Itai Spiegel Date: Sun, 11 Oct 2026 16:19:55 +0300 Subject: [PATCH] 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) --- config-ui/src/api/index.ts | 2 + config-ui/src/api/org/index.ts | 43 ++++ config-ui/src/app/router.tsx | 5 + config-ui/src/routes/index.ts | 1 + config-ui/src/routes/layout/config.tsx | 6 + config-ui/src/routes/team/editable-table.tsx | 122 ++++++++++ config-ui/src/routes/team/index.ts | 19 ++ config-ui/src/routes/team/team.tsx | 238 +++++++++++++++++++ config-ui/src/utils/csv.test.ts | 73 ++++++ config-ui/src/utils/csv.ts | 119 ++++++++++ config-ui/src/utils/index.ts | 1 + 11 files changed, 629 insertions(+) create mode 100644 config-ui/src/api/org/index.ts create mode 100644 config-ui/src/routes/team/editable-table.tsx create mode 100644 config-ui/src/routes/team/index.ts create mode 100644 config-ui/src/routes/team/team.tsx create mode 100644 config-ui/src/utils/csv.test.ts create mode 100644 config-ui/src/utils/csv.ts diff --git a/config-ui/src/api/index.ts b/config-ui/src/api/index.ts index 378ba14777e..f69c8cb13ca 100644 --- a/config-ui/src/api/index.ts +++ b/config-ui/src/api/index.ts @@ -22,6 +22,7 @@ import * as apiKey from './api-key'; import * as auth from './auth'; import * as blueprint from './blueprint'; import * as connection from './connection'; +import * as org from './org'; import * as pipeline from './pipeline'; import plugin from './plugin'; import * as project from './project'; @@ -39,6 +40,7 @@ export const API = { auth, blueprint, connection, + org, pipeline, plugin, project, diff --git a/config-ui/src/api/org/index.ts b/config-ui/src/api/org/index.ts new file mode 100644 index 00000000000..deb5cc98328 --- /dev/null +++ b/config-ui/src/api/org/index.ts @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { request } from '@/utils'; + +// The org plugin stores teams, users and their account mapping as whole CSV files: +// every PUT replaces the entire table (deleteAll + save), so the UI always sends +// back the complete set of rows rather than a delta. +const upload = (path: string, csv: string, filename: string) => { + const data = new FormData(); + data.append('file', new Blob([csv], { type: 'text/csv' }), filename); + return request(path, { method: 'put', data }); +}; + +export const getTeams = (): Promise => request('/plugins/org/teams.csv'); + +export const updateTeams = (csv: string) => upload('/plugins/org/teams.csv', csv, 'teams.csv'); + +export const getUsers = (): Promise => request('/plugins/org/users.csv'); + +export const updateUsers = (csv: string) => upload('/plugins/org/users.csv', csv, 'users.csv'); + +// Returns every known account (one per connected tool identity) with the UserId it +// is currently mapped to, not only the accounts that already have one. +export const getUserAccountMapping = (): Promise => request('/plugins/org/user_account_mapping.csv'); + +export const updateUserAccountMapping = (csv: string) => + upload('/plugins/org/user_account_mapping.csv', csv, 'user_account_mapping.csv'); diff --git a/config-ui/src/app/router.tsx b/config-ui/src/app/router.tsx index c9ffe5f235c..e7f1a9043f3 100644 --- a/config-ui/src/app/router.tsx +++ b/config-ui/src/app/router.tsx @@ -35,6 +35,7 @@ import { Pipelines, Pipeline, ApiKeys, + Team, NotFound, } from '@/routes'; @@ -112,6 +113,10 @@ export const router = createBrowserRouter([ }, ], }, + { + path: 'teams', + element: , + }, { path: 'keys', element: , diff --git a/config-ui/src/routes/index.ts b/config-ui/src/routes/index.ts index 12e9f77bafe..8550e0a0f9d 100644 --- a/config-ui/src/routes/index.ts +++ b/config-ui/src/routes/index.ts @@ -27,3 +27,4 @@ export * from './not-found'; export * from './onboard'; export * from './pipeline'; export * from './project'; +export * from './team'; diff --git a/config-ui/src/routes/layout/config.tsx b/config-ui/src/routes/layout/config.tsx index ae6ac5dbf17..862d9529ab2 100644 --- a/config-ui/src/routes/layout/config.tsx +++ b/config-ui/src/routes/layout/config.tsx @@ -21,6 +21,7 @@ import { ProjectOutlined, ExperimentOutlined, KeyOutlined, + TeamOutlined, DashboardOutlined, FileSearchOutlined, ApiOutlined, @@ -66,6 +67,11 @@ export const menuItems: MenuItem[] = [ }, ], }, + { + key: `${PATH_PREFIX}/teams`, + label: 'Teams', + icon: , + }, { key: `${PATH_PREFIX}/keys`, label: 'API Keys', diff --git a/config-ui/src/routes/team/editable-table.tsx b/config-ui/src/routes/team/editable-table.tsx new file mode 100644 index 00000000000..a6316df9507 --- /dev/null +++ b/config-ui/src/routes/team/editable-table.tsx @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { DeleteOutlined } from '@ant-design/icons'; +import { Table, Input, Select, Button, Tooltip } from 'antd'; + +export type EditableRow = Record & { _key: string }; + +export type EditableField = { + key: string; + title: string; + type?: 'text' | 'select' | 'multiple'; + width?: number; + placeholder?: string; + options?: Array<{ label: string; value: string }>; + // Multi-value fields are stored in the CSV as a single delimited cell. + separator?: string; + editable?: boolean; + invalid?: (row: EditableRow, rows: EditableRow[]) => string | undefined; +}; + +interface Props { + fields: EditableField[]; + rows: EditableRow[]; + onChange: (rows: EditableRow[]) => void; + removable?: boolean; + emptyText?: string; +} + +export const EditableTable = ({ fields, rows, onChange, removable = true, emptyText }: Props) => { + const update = (key: string, field: string, value: string) => + onChange(rows.map((row) => (row._key === key ? { ...row, [field]: value } : row))); + + const columns = fields.map((field) => ({ + title: field.title, + dataIndex: field.key, + width: field.width, + render: (value: string, row: EditableRow) => { + if (field.editable === false) { + return value || —; + } + + if (field.type === 'select' || field.type === 'multiple') { + const separator = field.separator ?? ';'; + const multiple = field.type === 'multiple'; + return ( + update(row._key, field.key, e.target.value)} + /> + + ); + }, + })); + + if (removable) { + columns.push({ + title: '', + dataIndex: '_actions', + width: 50, + render: (_: string, row: EditableRow) => ( +