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


Reply via email to