This is an automated email from the ASF dual-hosted git repository. jbonofre pushed a commit to branch ui in repository https://gitbox.apache.org/repos/asf/polaris-tools.git
commit 4281a4f896d0802d8362836d578e958578443d7e Author: JB Onofré <[email protected]> AuthorDate: Sat Oct 25 08:56:16 2025 +0200 Bootstrap Polaris Console --- console/README.md | 67 ++++++++++ console/package.json | 36 ++++++ console/public/favicon.ico | Bin 0 -> 1150 bytes console/public/index.html | 17 +++ console/public/logo.png | Bin 0 -> 283351 bytes console/public/robots.txt | 3 + console/src/app.css | 43 +++++++ console/src/app.tsx | 41 ++++++ console/src/browse.tsx | 97 ++++++++++++++ console/src/catalog.tsx | 310 +++++++++++++++++++++++++++++++++++++++++++++ console/src/home.tsx | 91 +++++++++++++ console/src/index.css | 12 ++ console/src/index.tsx | 26 ++++ console/src/login.tsx | 80 ++++++++++++ console/src/principals.tsx | 71 +++++++++++ console/src/settings.tsx | 63 +++++++++ console/src/workspace.tsx | 172 +++++++++++++++++++++++++ 17 files changed, 1129 insertions(+) diff --git a/console/README.md b/console/README.md new file mode 100644 index 0000000..ccc1b9c --- /dev/null +++ b/console/README.md @@ -0,0 +1,67 @@ +<!-- + - 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. + --> + +# Apache Polaris Console + +The Apache Polaris Console is a web console allowing you to manage a Polaris server. +It allows you to manage Polaris catalogs, principals, catalog roles, ... + +You can also browse the catalog entities (namespaces, tables, ...). + +## Prerequisite + +The Polaris Console is using `yarn` or `npm` to be built. + +You have to install `yarn` or `npm` using your OS package manager (e.g. `brew install yarn`). + +## Configuring the Polaris Console to connect to a Polaris server + +By default, the Polaris Console connects to a local Polaris server on the port 8181 (`http://localhost:8181`). + +If the Polaris server is not located on `localhost:8181`, you have to update the Console `proxy` configuration in the `package.json`: + +``` +"proxy": "http://localhost:8181" +``` + +## Downloading the Polaris Console dependencies + +The Polaris Console uses React (https://react.dev/) and Ant Design (https://ant.design/) frameworks, with transitive dependencies. + +To download the Polaris Console dependencies, you can just do: + +``` +yarn +``` + +## Starting the Polaris Console + +``` +yarn start +``` + +## Building static Polaris Console + +``` +yarn build +``` + +## Polaris Console Docker image and Helm Charts + +TODO diff --git a/console/package.json b/console/package.json new file mode 100644 index 0000000..b16843b --- /dev/null +++ b/console/package.json @@ -0,0 +1,36 @@ +{ + "name": "polaris-console", + "version": "1.3.0-incubating-SNAPSHOT", + "main": "/index.tsx", + "dependencies": { + "@ant-design/icons": "^6.0.0", + "antd": "^5.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^5.3.0", + "react-scripts": "^5.0.1" + }, + "devDependencies": { + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "typescript": "^5" + }, + "scripts": { + "start": "react-scripts start", + "build": "BUILD_PATH=target/classes/META-INF/resources react-scripts build", + "eject": "react-scripts eject" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "proxy": "http://localhost:8181" +} diff --git a/console/public/favicon.ico b/console/public/favicon.ico new file mode 100644 index 0000000..6ca7eef Binary files /dev/null and b/console/public/favicon.ico differ diff --git a/console/public/index.html b/console/public/index.html new file mode 100644 index 0000000..42c8ffe --- /dev/null +++ b/console/public/index.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="theme-color" content="#000000" /> + <meta name="description" content="Apache Polaris, the lakehouse catalog" /> + <title>Apache Polaris</title> +</head> + +<body> + <noscript>You need to enable JavaScript to run this app.</noscript> + <div id="root"></div> +</body> + +</html> diff --git a/console/public/logo.png b/console/public/logo.png new file mode 100644 index 0000000..6573f7a Binary files /dev/null and b/console/public/logo.png differ diff --git a/console/public/robots.txt b/console/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/console/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/console/src/app.css b/console/src/app.css new file mode 100644 index 0000000..55c68b3 --- /dev/null +++ b/console/src/app.css @@ -0,0 +1,43 @@ +.logo { + height: 48px; + margin: 16px; + display: flex; + align-items: center; + justify-content: space-evenly; + color: white; + font-size: large; +} + +.logo > a { + text-decoration: none; + color: white; +} + +.site-layout .site-layout-background { + background: #fff; +} +.layout { + min-height: 100vh !important; +} +.site-layout { + padding: 0 50px !important; +} +.ant-layout-header { + padding: 10px; +} +.ant-layout-sider { + background: #fff; +} +.ant-layout-sider-children > svg { + margin: auto; +} +.ant-breadcrumb { + margin: 16px 0px !important; +} +.site-layout-background { + padding: 24px !important; + min-height: 360px !important; +} +.ant-layout-footer { + text-align: center !important; +} diff --git a/console/src/app.tsx b/console/src/app.tsx new file mode 100644 index 0000000..df0f2a7 --- /dev/null +++ b/console/src/app.tsx @@ -0,0 +1,41 @@ +/** + * 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 React, { useState } from 'react'; +import Login from './login.tsx'; +import Workspace from './workspace.tsx'; + +import './app.css'; + +export default function App() { + + const [ user, setUser ] = useState(); + const [ token, setToken ] = useState(); + + if (user) { + return( + <Workspace user={user} setUser={setUser} token={token} /> + ); + } else { + return( + <Login user={user} setUser={setUser} setToken={setToken} /> + ); + } + +} \ No newline at end of file diff --git a/console/src/browse.tsx b/console/src/browse.tsx new file mode 100644 index 0000000..4514b1a --- /dev/null +++ b/console/src/browse.tsx @@ -0,0 +1,97 @@ +/** + * 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 { useState, useEffect } from 'react'; +import { useParams } from 'react-router'; +import { Link } from 'react-router-dom'; +import { Breadcrumb, Card, Space, Tree, Row, Col, Spin, message } from 'antd'; +import { HomeOutlined, ApartmentOutlined } from '@ant-design/icons'; + +function Detail(props) { + + return( + <b>{ props.item }</b> + ); + +} + +export default function Browse(props) { + + const [ browseTree, setBrowseTree ] = useState(); + const [ item, setItem ] = useState(); + + const bearer = 'Bearer ' + props.token; + const realmHeader = props.realmHeader; + const realm = props.realm; + + let { catalogName } = useParams(); + + const browse = () => { + fetch('/api/catalog/v1/' + catalogName + '/namespaces', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + realmHeader: realm, + 'Authorization': bearer + } + }) + .then((response) => { + if (!response.ok) { + throw new Error(response.status); + } + return response.json(); + }) + .then((data) => { + setBrowseTree(data); + }) + .catch((error) => { + message.error('An error occurred: ' + error.message); + console.error(error); + }) + }; + + useEffect(browse, [catalogName]); + + if (!browseTree) { + return(<Spin/>); + } + + let treeData = [ + { + title: catalogName, + key: catalogName, + children: browseTree.namespaces + } + ]; + + return( + <> + <Breadcrumb items={[ { title: <Link to="/"><HomeOutlined/></Link> }, { title: <ApartmentOutlined/> } ]} /> + <Card title={<Space><ApartmentOutlined/> {catalogName}</Space>} style={{ width: '100%' }} > + <Row> + <Col span="12"> + <Tree treeData={treeData} onClick={(e) => console.log(e)} /> + </Col> + <Col span="12"> + <Detail item={item} /> + </Col> + </Row> + </Card> + </> + ); +} \ No newline at end of file diff --git a/console/src/catalog.tsx b/console/src/catalog.tsx new file mode 100644 index 0000000..2181998 --- /dev/null +++ b/console/src/catalog.tsx @@ -0,0 +1,310 @@ +/** + * 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 { useState, useEffect } from 'react'; +import { useParams } from 'react-router'; +import { Link } from 'react-router-dom'; +import { Breadcrumb, Spin, Card, Form, Input, Select, Tabs, Collapse, Divider, Button, Space, Popconfirm, message } from 'antd'; +import { HomeOutlined, ApartmentOutlined, AmazonOutlined, GoogleOutlined, CloudOutlined, FileSyncOutlined, SaveOutlined, PauseCircleOutlined, DeleteOutlined } from '@ant-design/icons'; + +function S3() { + + return( + <> + <Form.Item name="s3.roleArn" label="Role ARN"> + <Input allowClear={true} /> + </Form.Item> + <Form.Item name="s3.externalId" label="External ID"> + <Input allowClear={true} /> + </Form.Item> + <Form.Item name="s3.userArn" label="User ARN"> + <Input allowClear={true} /> + </Form.Item> + <Form.Item name="s3.region" label="Region"> + <Input allowClear={true} /> + </Form.Item> + <Form.Item name="s3.endpoint" label="Endpoint"> + <Input allowClear={true} /> + </Form.Item> + <Form.Item name="s3.endpointInternal" label="Endpoint internal"> + <Input allowClear={true} /> + </Form.Item> + <Form.Item name="s3.pathStyleAccess" label="Path style access"> + <Select options={[ + { value: 'true', label: 'Enabled' }, + { value: 'false', label: 'Disabled' } + ]} /> + </Form.Item> + <Form.Item name="s3.stsUnavailable" label="STS"> + <Select options={[ + { value: 'false', label: 'Enabled' }, + { value: 'true', label: 'Disabled' } + ]} /> + </Form.Item> + <Form.Item name="s3.stsEndpoint" label="STS endpoint"> + <Input allowClear={true} /> + </Form.Item> + <Form.Item name="s3.accountId" label="Account ID"> + <Input allowClear={true} /> + </Form.Item> + <Form.Item name="s3.partition" label="Partition"> + <Input allowClear={true} /> + </Form.Item> + </> + ); +} + +function GCP() { + + return( + <> + <Form.Item name="gcp.serviceAccount" label="Service account"> + <Input allowClear={true} /> + </Form.Item> + </> + ); + +} + +function Azure() { + + return( + <> + <Form.Item name="azure.tenantId" label="Tenant ID"> + <Input allowClear={true} /> + </Form.Item> + <Form.Item name="azure.multiTenantAppName" label="Multi tenant app name"> + <Input allowClear={true} /> + </Form.Item> + <Form.Item name="azure.consentUrl" label="Consent URL"> + <Input allowClear={true} /> + </Form.Item> + </> + ); + +} + +function StorageConfig() { + + const tabItems = [ + { + key: 's3', + label: 'S3', + icon: <AmazonOutlined/>, + children: <S3/> + }, + { + key: 'gcp', + label: 'GCP', + icon: <GoogleOutlined/>, + children: <GCP/> + }, + { + key: 'azure', + label: 'Azure', + icon: <CloudOutlined/>, + children: <Azure/> + } + ]; + + return( + <> + <Form.Item name="storageType" label="Storage Type" rules={[{ required: true, message: 'The storage type is required' }]}> + <Select options={[ + { value: 'S3', label: 'S3' }, + { value: 'GCP', label: 'GCP' }, + { value: 'AZURE', label: 'Azure' }, + { value: 'FILE', label: 'File' } + ]}/> + </Form.Item> + <Form.Item name="location" label="Default Base Location" rules={[{ required: true, message: 'The storage location is required' }]}> + <Input allowClear={true} /> + </Form.Item> + <Form.Item name="allowedLocations" label="Allowed locations"> + <Select mode="tags" /> + </Form.Item> + <Tabs centered items={tabItems} /> + </> + ); + +} + +export default function Catalog(props) { + + const [ catalogDetail, setCatalogDetail ] = useState(); + + const bearer = 'Bearer ' + props.token; + const realmHeader = props.realmHeader; + const realm = props.realm; + + const [ catalogForm ] = Form.useForm(); + + let { catalogName } = useParams(); + + const createCatalog = (values) => { + const request = { + 'catalog': { + 'name': values.name, + 'type': values.type, + 'properties': { + 'default-base-location': values.location + }, + 'storageConfigInfo': { + 'storageType': values.storageType, + 'allowedLocations': values.allowedLocations + } + } + }; + fetch('/api/management/v1/catalogs', { + method: 'POST', + body: JSON.stringify(request), + headers: { + 'Content-Type': 'application/json', + realmHeader: realm, + 'Authorization': bearer + } + }) + .then((response) => { + if(!response.ok) { + throw new Error(response.status); + } + return response.json(); + }) + .then((data) => { + console.log(data); + message.info('Catalog ' + data.name + ' created.'); + props.fetchCatalogs(); + catalogName = data.name; + }) + .catch((error) => { + message.error('An error occurred: ' + error.message); + console.error(error); + }); + }; + + let cardTitle = 'Create Catalog'; + let onFinish = createCatalog; + let deleteButton = null; + + if (catalogName) { + // get catalog details + const fetchCatalogDetail = () => { + fetch('/api/management/v1/catalogs/' + catalogName, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + realmHeader: realm, + 'Authorization': bearer + } + }) + .then((response) => { + if (!response.ok) { + throw new Error(response.status); + } + return response.json(); + }) + .then((data) => { + const catalogDetailValues = { + name: data.name, + type: data.type, + storageType: data.storageConfigInfo.storageType, + allowedLocations: data.storageConfigInfo.allowedLocations, + }; + setCatalogDetail(catalogDetailValues); + }) + .catch((error) => { + message.error('An error occurred: ' + error.message); + console.error(error); + }); + }; + + useEffect(fetchCatalogDetail, [catalogName]); + + if (!catalogDetail) { + return(<Spin/>); + } + + cardTitle = 'Catalog ' + catalogDetail.name; + // TODO implement catalog update (PUT) + onFinish = () => console.log('Update catalog'); + + const deleteCatalog = (catalog) => { + fetch('/api/management/v1/catalogs/' + catalog, { + method: 'DELETE', + headers: { + 'Authorization': bearer + } + }) + .then((response) => { + if (!response.ok) { + throw new Error(response.status); + } + return response; + }) + .then((data) => { + message.info('Catalog ' + catalog + ' has been removed'); + props.fetchCatalogs(); + // TODO redirect to home with <Redirect /> + }) + .catch((error) => { + message.error('An error occurred: ' + error.message); + console.error(error); + }) + }; + + deleteButton = <Popconfirm title="Delete Catalog" description="Are you sure you want to delete this catalog ?" okText="Yes" cancelText="No" onConfirm={() => deleteCatalog(catalogDetail.name) }><Button danger icon={<DeleteOutlined/>}>Delete</Button></Popconfirm>; + } + + return( + <> + <Breadcrumb items={[ { title: <Link to="/"><HomeOutlined/></Link> }, { title: <ApartmentOutlined/> } ]} /> + <Card title={cardTitle} style={{ width: '100%' }}> + <Form name="catalog" form={catalogForm} labelCol={{ span: 8 }} + wrapperCol={{ span: 16 }} + style={{ width: '100%' }} + onFinish={onFinish} initialValues={catalogDetail}> + <Form.Item name="name" label="Name" rules={[{ required: true, message: 'The catalog name is required' }]}> + <Input allowClear={true} /> + </Form.Item> + <Form.Item name="type" label="Type" rules={[{ required: true, message: 'The catalog type is required' }]}> + <Select options={[ + { value: 'INTERNAL', label: 'Internal' }, + { value: 'EXTERNAL', label: 'External' } + ]}/> + </Form.Item> + <Collapse items={[ + { key: '1', label: 'Storage specific configuration', children: <StorageConfig /> } + ]}/> + <Divider /> + <Collapse items={[ + { key: '1', label: 'Additional properties', children: <Form.Item name="properties" label="Additional properties"><Select mode="tags"/></Form.Item> } + ]}/> + <Divider /> + <Form.Item label={null}> + <Space> + <Button type="primary" icon={<SaveOutlined/>} onClick={() => catalogForm.submit()}>Save</Button> + <Button icon={<PauseCircleOutlined/>} onClick={() => catalogForm.resetFields()}>Cancel</Button> + { deleteButton } + </Space> + </Form.Item> + </Form> + </Card> + </> + ); + +} \ No newline at end of file diff --git a/console/src/home.tsx b/console/src/home.tsx new file mode 100644 index 0000000..31a41db --- /dev/null +++ b/console/src/home.tsx @@ -0,0 +1,91 @@ +/** + * 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 { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { Breadcrumb, Card, Row, Col, Space, Button, Table, Spin, Popconfirm, message } from 'antd'; +import { HomeOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; + +export default function Home(props) { + + const bearer = 'Bearer ' + props.token; + + const deleteCatalog = (catalog) => { + fetch('/api/management/v1/catalogs/' + catalog, { + method: 'DELETE', + headers: { + 'Authorization': bearer + } + }) + .then((response) => { + if (!response.ok) { + throw new Error(response.status); + } + return response; + }) + .then((data) => { + message.info('Catalog ' + catalog + ' has been removed'); + props.fetchCatalogs(); + }) + .catch((error) => { + message.error('An error occurred: ' + error.message); + console.error(error); + }) + }; + + const catalogColumns = [ + { + title: 'Catalog', + dataIndex: 'name', + key: 'name' + }, + { + title: 'Type', + dataIndex: 'type', + key: 'type' + }, + { + title: '', + key: 'action', + render: (_,record) => ( + <Space> + <Button><EditOutlined/></Button> + <Popconfirm title="Delete Catalog" + description="Are you sure you want to delete catalog ?" + onConfirm={() => deleteCatalog(record.name)} + okText="Yes" cancelText="No"> + <Button danger icon={<DeleteOutlined/>} /> + </Popconfirm> + </Space> + ) + } + ]; + + return( + <> + <Breadcrumb items={[ { title: <Link to="/"><HomeOutlined/></Link> } ]} /> + <Card title="Overview" style={{ width: '100%' }}> + <Row gutter={[16,16]}> + <Col span={24}> + <Table columns={catalogColumns} dataSource={props.catalogs} /> + </Col> + </Row> + </Card> + </> + ); +} \ No newline at end of file diff --git a/console/src/index.css b/console/src/index.css new file mode 100644 index 0000000..8fc644c --- /dev/null +++ b/console/src/index.css @@ -0,0 +1,12 @@ +html, body { + padding: 0; + margin: 0; + background: #fff; + height: 100%; +} + +#root { + padding: 0; + background: #fff; + height: 100vh; +} \ No newline at end of file diff --git a/console/src/index.tsx b/console/src/index.tsx new file mode 100644 index 0000000..c70874a --- /dev/null +++ b/console/src/index.tsx @@ -0,0 +1,26 @@ +/** + * 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 React from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './app.tsx'; +import './index.css'; + +const root = createRoot(document.getElementById("root")); +root.render(<App />); \ No newline at end of file diff --git a/console/src/login.tsx b/console/src/login.tsx new file mode 100644 index 0000000..9923c27 --- /dev/null +++ b/console/src/login.tsx @@ -0,0 +1,80 @@ +/** + * 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 React, { useState, useEffect } from 'react'; +import { Modal, Form, Button, Input, Space, Checkbox, Image, Spin, message } from 'antd'; + +export default function Login(props) { + + const [ loginForm ] = Form.useForm(); + const [ checked, setChecked ] = useState(true); + + return ( + <Modal centered={true} mask={false} title={<Space><Image width={30} src="/logo.png" preview={false}/> Apache Polaris (incubating) </Space>} open={true} okText="Login" cancelText="Cancel" closable={false} onOk={() => loginForm.submit()} onCancel={() => loginForm.resetFields()}> + <Form name="login" form={loginForm} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} autoComplete="off" onFinish={(values) => { + + const fetchUser = () => { + fetch('/api/catalog/v1/oauth/tokens', { + method: 'POST', + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: new URLSearchParams( + { + client_id: values.username, + client_secret: values.password, + scope: 'PRINCIPAL_ROLE:ALL', + grant_type: 'client_credentials' + } + ) + }) + .then((response) => { + if (!response.ok) { + throw new Error('Authentication failed (' + response.status + ')'); + } + return response.json(); + }) + .then((data) => { + props.setUser(values.username); + props.setToken(data.access_token); + }) + .catch((error) => { + message.error(error.message); + console.error(error); + }) + }; + + useEffect(fetchUser(), []); + + if (!props.token) { + return(<Spin/>); + } + + }} onKeyUp={(event) => { + if (event.keyCode === 13) { + loginForm.submit(); + } + }}> + <Form.Item name="username" label="Username" rules={[{ required: true, message: 'The username is required' }]}><Input allowClear={true} /></Form.Item> + <Form.Item name="password" label="Password" rules={[{ required: true, message: 'The password is required' }]}><Input.Password allowClear={true} /></Form.Item> + </Form> + </Modal> + ); + +} diff --git a/console/src/principals.tsx b/console/src/principals.tsx new file mode 100644 index 0000000..24379b1 --- /dev/null +++ b/console/src/principals.tsx @@ -0,0 +1,71 @@ +/** + * 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 { Link } from 'react-router-dom'; +import { Breadcrumb, Card, Space, Button, Popconfirm } from 'antd'; +import { HomeOutlined, UserOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'; + +export default function Principals(props) { + + const fetchPrincipals = () => { + fetch('/api/management/v1/principals', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + + } + }) + }; + + const principalColumns = [ + { + title: 'Principal', + dataIndex: 'name', + key: 'name' + }, + { + title: 'Client ID', + dataIndex: 'clientId', + key: 'clientId' + }, + { + title: '', + key: 'action', + render: (_,record) => ( + <Space> + <Button><EditOutlined/></Button> + <Popconfirm title="Delete Principal" + description="Are you sure you want to delete this principal ?" + okText="Yes" cancelText="No"> + <Button danger icon={<DeleteOutlined/>} /> + </Popconfirm> + </Space> + ) + } + ]; + + return( + <> + <Breadcrumb items={[ { title: <Link to="/"><HomeOutlined/></Link> }, { title: <UserOutlined/> } ]} /> + <Card title="Principals" style={{ width: '100%' }}> + + </Card> + </> + ); + +} \ No newline at end of file diff --git a/console/src/settings.tsx b/console/src/settings.tsx new file mode 100644 index 0000000..0796f7f --- /dev/null +++ b/console/src/settings.tsx @@ -0,0 +1,63 @@ +/** + * 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 { Link } from 'react-router-dom'; +import { Breadcrumb, Card, Form, Input, Space, Button, message } from 'antd'; +import { HomeOutlined, SettingOutlined, SaveOutlined, PauseCircleOutlined } from '@ant-design/icons'; + +export default function Settings(props) { + + const initialValues = { + realmHeader: props.realmHeader, + realm: props.realm + }; + + const [ settingsForm ] = Form.useForm(); + + const onFinish = (values) => { + props.setRealmHeader(values.realmHeader); + props.setRealm(values.realm); + message.info('Settings updated'); + }; + + return( + <> + <Breadcrumb items={[ { title: <Link to="/"><HomeOutlined/></Link> }, { title: <SettingOutlined/> } ]} /> + <Card title="Settings" style={{ width: '100%' }}> + <Form name="settings" form={settingsForm} labelCol={{ span: 8 }} + wrapperCol={{ span: 16 }} + style={{ style: '100%' }} + initialValues={initialValues} onFinish={onFinish}> + <Form.Item name="realmHeader" label="Realm Header" rules={[{ required: true, message: 'The Realm Header is required' }]}> + <Input allowClear={true} /> + </Form.Item> + <Form.Item name="realm" label="Realm" rules={[{ required: true, message: 'The Realm is required' }]}> + <Input allowClear={true} /> + </Form.Item> + <Form.Item label={null}> + <Space> + <Button type="primary" icon={<SaveOutlined/>} onClick={() => settingsForm.submit()}>Save</Button> + <Button icon={<PauseCircleOutlined/>} onClick={() => settingsForm.resetFields()}>Cancel</Button> + </Space> + </Form.Item> + </Form> + </Card> + </> + ); + +} \ No newline at end of file diff --git a/console/src/workspace.tsx b/console/src/workspace.tsx new file mode 100644 index 0000000..0cf951c --- /dev/null +++ b/console/src/workspace.tsx @@ -0,0 +1,172 @@ +/** + * 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 React, { useState, useEffect } from 'react'; +import { Layout, Input, Col, Row, Image, Menu, Space, Spin, message } from 'antd'; +import { Route, Switch } from 'react-router'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { Link } from 'react-router-dom'; +import { HomeOutlined, UserOutlined, BlockOutlined, SettingOutlined, DeleteOutlined, MenuUnfoldOutlined, PlayCircleOutlined, LogoutOutlined, ApartmentOutlined, DashboardOutlined, AreaChartOutlined, NotificationOutlined, SafetyOutlined, TeamOutlined, BuildOutlined, CrownOutlined, ProfileOutlined, CheckSquareOutlined, PlusCircleOutlined } from '@ant-design/icons'; +import Home from './home.tsx'; +import Catalog from './catalog.tsx'; +import Browse from './browse.tsx'; +import Principals from './principals.tsx'; +import Settings from './settings.tsx'; + +function SideMenu(props) { + + const[ collapsed, setCollapsed ] = useState(false); + + const catalogElementsMenu = props.catalogs.map((element) => { + const configLink = "/catalog/" + element.name; + const browseLink = "/browse/" + element.name; + return({ + key: element.name, + label: element.name, + icon: <ApartmentOutlined/>, + children: [ + { key: browseLink, label: <Link to={browseLink}>Browse</Link>, icon: <MenuUnfoldOutlined/> }, + { key: configLink, label: <Link to={configLink}>Configuration</Link>, icon: <SettingOutlined/> } + ] + }); + }); + const newCatalogMenu = [ + { + key: 'catalog/create', + label: <Link to="/catalog/create">Create</Link>, + icon: <PlusCircleOutlined/>, + } + ]; + + const catalogMenu = newCatalogMenu.concat(catalogElementsMenu); + + const mainMenu = [ + { key: 'home', label: <Link to="/">Home</Link>, icon: <HomeOutlined/> }, + { key: 'catalogs', label: 'Catalogs', icon: <BlockOutlined/>, children: catalogMenu }, + { key: 'governance', label: 'Governance', icon: <SafetyOutlined/>, children: [ + { key: 'principals', label: <Link to="/principals">Principals</Link>, icon: <UserOutlined/> }, + { key: 'principal_roles', label: 'Principal Roles', icon: <TeamOutlined/> }, + { key: 'catalog_roles', label: 'Catalog Roles', icon: <BuildOutlined/> }, + { key: 'privileges', label: 'Privileges', icon: <CrownOutlined/> } + ]}, + { key: 'settings', label: <Link to="/settings">Settings</Link>, icon: <SettingOutlined/> } + ]; + + return( + <Layout.Sider collapsible={true} collapsed={collapsed} onCollapse={newValue => setCollapsed(newValue)}> + <Menu items={mainMenu} mode="inline" defaultOpenKeys={[ 'catalogs', 'governance' ]}/> + </Layout.Sider> + ); + +} + +function Header(props) { + + const { Search } = Input; + + const userMenu = [ + { key: 'user', label: props.user, icon: <UserOutlined/>, children: [ + { key: 'logout', label: 'Logout', icon: <LogoutOutlined/> } + ] } + ]; + + return( + <Layout.Header style={{ height: "80px", background: "#fff", padding: "5px", margin: "10px" }}> + <Row align="middle" justify="center" wrap="false"> + <Col span={3}><Image src="/logo.png" preview={false} width={50}/></Col> + <Col span={19}><Search /></Col> + <Col span={2}><Menu items={userMenu} onClick={(e) => { + if (e.key === 'logout') { + props.setUser(null); + } + }} /></Col> + </Row> + </Layout.Header> + ); + +} + +export default function Workspace(props) { + + const [ realmHeader, setRealmHeader ] = useState("Polaris-Realm"); + const [ realm, setRealm ] = useState("POLARIS"); + const [ catalogs, setCatalogs ] = useState(); + + const bearer = 'Bearer ' + props.token; + const fetchCatalogs = () => { + fetch('/api/management/v1/catalogs', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': bearer + } + }) + .then((response) => { + if (!response.ok) { + throw new Error(response.status); + } + return response.json(); + }) + .then((data) => setCatalogs(data.catalogs)) + .catch((error) => { + message.error('An error occurred: ' + error.message); + console.error(error); + }); + }; + + useEffect(fetchCatalogs, []); + + if (!catalogs) { + return(<Spin/>); + } + + return( + <Layout style={{ height: "105vh" }}> + <Header user={props.user} setUser={props.setUser} /> + <Layout hasSider={true}> + <Router> + <SideMenu catalogs={catalogs} /> + <Layout.Content style={{ margin: "15px" }}> + <Switch> + <Route path="/" key="home" exact={true}> + <Home catalogs={catalogs} token={props.token} fetchCatalogs={fetchCatalogs} /> + </Route> + <Route path="/catalog/create" key="catalog-create" exact> + <Catalog token={props.token} fetchCatalogs={fetchCatalogs} realmHeader={realmHeader} realm={realm} /> + </Route> + <Route path="/catalog/:catalogName" key="catalogSettings"> + <Catalog token={props.token} fetchCatalogs={fetchCatalogs} realmHeader={realmHeader} realm={realm} /> + </Route> + <Route path="/browse/:catalogName" key="catalogBrowse"> + <Browse token={props.token} realmHeader={realmHeader} realm={realm} /> + </Route> + <Route path="/principals" key="principals" exact> + <Principals token={props.token} realmHeader={realmHeader} realm={realm} /> + </Route> + <Route path="/settings" key="settings" exact> + <Settings realm={realm} realmHeader={realmHeader} setRealm={setRealm} setRealmHeader={setRealmHeader} /> + </Route> + </Switch> + </Layout.Content> + </Router> + </Layout> + <Layout.Footer>Apache®, Apache Polaris™ are either registered trademarks or trademarks of the Apache Software Foundation in the United States and/or other countries.</Layout.Footer> + </Layout> + ); + +} \ No newline at end of file
