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) => ( +