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 f76f25be7ff81971a61308edf4d62bd65eea9b75
Author: JB Onofré <[email protected]>
AuthorDate: Sat Oct 25 08:56:16 2025 +0200

    Bootstrap UI
---
 ui/package.json       |  36 ++++++++++++++++
 ui/public/favicon.ico | Bin 0 -> 1150 bytes
 ui/public/index.html  |  17 ++++++++
 ui/public/logo.png    | Bin 0 -> 283351 bytes
 ui/public/robots.txt  |   3 ++
 ui/src/app.css        |  43 +++++++++++++++++++
 ui/src/app.tsx        |  41 ++++++++++++++++++
 ui/src/home.tsx       |  44 +++++++++++++++++++
 ui/src/index.css      |  12 ++++++
 ui/src/index.tsx      |  26 ++++++++++++
 ui/src/login.tsx      |  82 +++++++++++++++++++++++++++++++++++
 ui/src/workspace.tsx  | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++
 12 files changed, 419 insertions(+)

diff --git a/ui/package.json b/ui/package.json
new file mode 100644
index 0000000..e168a3a
--- /dev/null
+++ b/ui/package.json
@@ -0,0 +1,36 @@
+{
+  "name": "polaris-ui",
+  "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/ui/public/favicon.ico b/ui/public/favicon.ico
new file mode 100644
index 0000000..6ca7eef
Binary files /dev/null and b/ui/public/favicon.ico differ
diff --git a/ui/public/index.html b/ui/public/index.html
new file mode 100644
index 0000000..42c8ffe
--- /dev/null
+++ b/ui/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/ui/public/logo.png b/ui/public/logo.png
new file mode 100644
index 0000000..6573f7a
Binary files /dev/null and b/ui/public/logo.png differ
diff --git a/ui/public/robots.txt b/ui/public/robots.txt
new file mode 100644
index 0000000..e9e57dc
--- /dev/null
+++ b/ui/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/ui/src/app.css b/ui/src/app.css
new file mode 100644
index 0000000..55c68b3
--- /dev/null
+++ b/ui/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/ui/src/app.tsx b/ui/src/app.tsx
new file mode 100644
index 0000000..df0f2a7
--- /dev/null
+++ b/ui/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/ui/src/home.tsx b/ui/src/home.tsx
new file mode 100644
index 0000000..d0f6b93
--- /dev/null
+++ b/ui/src/home.tsx
@@ -0,0 +1,44 @@
+/**
+ * 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, Row, Col, Card } from 'antd';
+import { HomeOutlined } from '@ant-design/icons';
+
+export default function Home(props) {
+
+    return(
+        <>
+        <Breadcrumb items={[ { title: <Link to="/"><HomeOutlined/></Link> } ]} 
/>
+        <Row>
+            <Col>
+                <Card title="Catalogs Overview">
+                    Catalogs
+                </Card>
+            </Col>
+        </Row>
+        <Row>
+            <Col>
+                <Card title="Governance Overview">
+                    Governance
+                </Card>
+            </Col>
+        </Row>
+        </>
+    );
+}
\ No newline at end of file
diff --git a/ui/src/index.css b/ui/src/index.css
new file mode 100644
index 0000000..8fc644c
--- /dev/null
+++ b/ui/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/ui/src/index.tsx b/ui/src/index.tsx
new file mode 100644
index 0000000..c70874a
--- /dev/null
+++ b/ui/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/ui/src/login.tsx b/ui/src/login.tsx
new file mode 100644
index 0000000..d06b938
--- /dev/null
+++ b/ui/src/login.tsx
@@ -0,0 +1,82 @@
+/**
+ * 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) => {
+
+                console.log(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/ui/src/workspace.tsx b/ui/src/workspace.tsx
new file mode 100644
index 0000000..eb21d99
--- /dev/null
+++ b/ui/src/workspace.tsx
@@ -0,0 +1,115 @@
+/**
+ * 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 { Layout, Input, Col, Row, Image, Menu, Space } from 'antd';
+import { Route, Switch } from 'react-router';
+import { BrowserRouter as Router } from 'react-router-dom';
+import { Link } from 'react-router-dom';
+import { UserOutlined, BlockOutlined, SettingOutlined, DeleteOutlined, 
PlayCircleOutlined, LogoutOutlined, ApartmentOutlined, DashboardOutlined, 
AreaChartOutlined, NotificationOutlined, SafetyOutlined, TeamOutlined, 
BuildOutlined, CrownOutlined, ProfileOutlined, CheckSquareOutlined, 
PlusCircleOutlined } from '@ant-design/icons';
+import Home from './home.tsx';
+
+function SideMenu() {
+
+    const[ collapsed, setCollapsed ] = useState(false);
+
+    const mainMenu = [
+        { key: 'catalogs', label: 'Catalogs', icon: <BlockOutlined/>, 
children: [
+            { key: 'new', label: 'Create catalog', icon: <PlusCircleOutlined/> 
},
+            { key: 'default', label: 'Default', icon: <ApartmentOutlined/> },
+            { key: 'my', label: 'My Catalog', icon: <ApartmentOutlined/> }
+        ] },
+        { key: 'governance', label: 'Governance', icon: <SafetyOutlined/>, 
children: [
+            { key: 'principals', label: 'Principals', 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: 'policies', label: 'Policies & TMS', icon: 
<CheckSquareOutlined/> },
+        { key: 'observe', label: 'Observability', icon: <DashboardOutlined/>, 
children: [
+            { key: 'metrics', label: 'Metrics', icon: <AreaChartOutlined/> },
+            { key: 'events', label: 'Events', icon: <NotificationOutlined/> }
+        ]},
+        { key: 'profiles', label: 'Profiles', icon: <ProfileOutlined/> },
+        { key: 'settings', label: 'Settings', icon: <SettingOutlined/>, 
children: [
+            { key: 'bootstrap', label: 'Bootstrap', icon: 
<PlayCircleOutlined/> },
+            { key: 'purge', label: 'Purge', icon: <DeleteOutlined/> }
+        ] }
+    ];
+
+    return(
+        <Layout.Sider collapsible={true} collapsed={collapsed} 
onCollapse={newValue => setCollapsed(newValue)}>
+            <Menu items={mainMenu} mode="inline"/>
+        </Layout.Sider>
+    );
+
+}
+
+function Header(props) {
+
+    const { Search } = Input;
+
+    const userMenu = [
+        { key: 'user', label: props.user, icon: <UserOutlined/>, children: [
+            { key: 'preferences', label: 'Preferences', icon: 
<SettingOutlined/> },
+            { 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={60}/></Col>
+                <Col span={19}><Search /></Col>
+                <Col span={2}><Menu items={userMenu} onClick={(e) => {
+                    if (e.key === 'logout') {
+                        props.setUser(null);
+                    }
+                    if (e.key === 'preferences') {
+                        console.log(e);
+                    }
+                }} /></Col>
+            </Row>
+        </Layout.Header>
+    );
+
+}
+
+export default function Workspace(props) {
+
+    return(
+        <Layout style={{ height: "105vh" }}>
+            <Header user={props.user} setUser={props.setUser} />
+            <Layout hasSider={true}>
+                <Router>
+                <SideMenu />
+                <Layout.Content style={{ margin: "15px" }}>
+                    <Switch>
+                        <Route path="/" key="home" exact={true}>
+                            <Home user={props.user} token={props.token} />
+                        </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