Skip to content

Commit

Permalink
Support namespace creation and deletion
Browse files Browse the repository at this point in the history
  • Loading branch information
datavisorwenjiejiang committed Feb 4, 2024
1 parent 2cd86bc commit d9624af
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 46 deletions.
1 change: 1 addition & 0 deletions webui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@mui/icons-material": "^5.15.7",
"@mui/material": "^5.15.5",
"@types/js-yaml": "^4.0.9",
"axios": "^1.6.7",
"js-yaml": "^4.1.0",
"next": "14.1.0",
"react": "^18",
Expand Down
4 changes: 2 additions & 2 deletions webui/src/app/cluster/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ export default function Cluster() {
return (
<div className="flex h-full">
<Sidebar />
<Container maxWidth={false} disableGutters className="h-full overflow-y-auto ml-4">
<Container maxWidth={false} disableGutters sx={{height: '100%', overflowY: 'auto', marginLeft: '16px'}}>
<div>
todo: show all clusters in namespace here
todo: show all clusters in selected namespace here
</div>
</Container>
</div>
Expand Down
27 changes: 0 additions & 27 deletions webui/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}

body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(to bottom,
transparent,
rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb));
}

@layer utilities {
.text-balance {
text-wrap: balance;
}
}
44 changes: 44 additions & 0 deletions webui/src/app/lib/actions.ts
Original file line number Diff line number Diff line change
@@ -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.
*/
'use server';

import { redirect } from "next/navigation";
import { createNamespace, deleteNamespace } from "./api";
import { revalidatePath } from "next/cache";

export async function createNamespaceAction(formData: FormData) {
const formObj = Object.fromEntries(formData.entries());
if(typeof formObj['name'] === 'string') {
const success = await createNamespace(formObj['name']);
if(success) {
revalidatePath('/cluster');
return true;
} else {
return false;
}
} else {
return false;
}
}

export async function deleteNamespaceAction(name: string) {
const result = deleteNamespace(name);
revalidatePath('/cluster');
return result;
}
26 changes: 23 additions & 3 deletions webui/src/app/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import yaml from 'js-yaml';
import fs from 'fs';
import path from 'path';
import axios from 'axios';

const configFile = './config/config.yaml';
const apiPrefix = '/api/v1';
Expand All @@ -35,10 +36,29 @@ const apiHost = `http://${host}${apiPrefix}`;

export async function fetchNamespaces(): Promise<string[]> {
try {
const response = await fetch(`${apiHost}/namespaces`);
const responseJson = await response.json();
return responseJson.data.namespaces || [];
const { data: responseData } = await axios.get(`${apiHost}/namespaces`);
return responseData.data.namespaces || [];
} catch (error) {
console.error(error);
return [];
}
}
export async function createNamespace(name: string): Promise<boolean> {
try {
const { data: responseData } = await axios.post(`${apiHost}/namespaces`, {namespace: name});
return responseData?.data == 'created';
} catch (error) {
console.error(error);
return false;
}
}

export async function deleteNamespace(name: string): Promise<boolean> {
try {
const { data: responseData } = await axios.delete(`${apiHost}/namespaces/${name}`);
return responseData?.data == 'ok';
} catch (error) {
console.error(error);
return false;
}
}
70 changes: 70 additions & 0 deletions webui/src/app/ui/namespace/namespaceCreation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.
*/

'use client';
import { createNamespaceAction } from "@/app/lib/actions";
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from "@mui/material";
import { useCallback, useState } from "react";

export default function NamespaceCreation() {
const [showDialog, setShowDialog] = useState(false);
const openDialog = useCallback(() => setShowDialog(true), []);
const closeDialog = useCallback(() => setShowDialog(false), []);
return (
<>
<Button variant="outlined" onClick={openDialog}>Create Namespace</Button>
<Dialog
open={showDialog}
PaperProps={{
component: 'form',
action: async (formData: FormData) => {
const success = await createNamespaceAction(formData);
if(success) {
closeDialog();
} else {
//todo: error handle
}
},
}}
onClose={closeDialog}
>
<DialogTitle>Create Namespace</DialogTitle>
<DialogContent
sx={{
width: '500px'
}}
>
<TextField
autoFocus
required
name="name"
label="Input Name"
type="name"
fullWidth
variant="standard"
/>
</DialogContent>
<DialogActions>
<Button onClick={closeDialog}>Cancel</Button>
<Button type="submit">Create</Button>
</DialogActions>
</Dialog>
</>
)
}
81 changes: 81 additions & 0 deletions webui/src/app/ui/namespace/namespaceItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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.
*/

'use client';
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Drawer, IconButton, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Menu, MenuItem, Popover, Tooltip } from "@mui/material";
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import { useCallback, useRef, useState } from "react";
import { deleteNamespaceAction } from "@/app/lib/actions";

export default function NamespaceItem({ item }: {item: string}) {
const [hover, setHover] = useState<boolean>(false);
const [showMenu, setShowMenu] = useState<boolean>(false);
const listItemTextRef = useRef(null);
const openMenu = useCallback(() => setShowMenu(true), []);
const closeMenu = useCallback(() => (setShowMenu(false), setHover(false)), []);
const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false);
const openDeleteConfirmDialog = useCallback(() => (setShowDeleteConfirm(true), closeMenu()), [closeMenu]);
const closeDeleteConfirmDialog = useCallback(() => setShowDeleteConfirm(false), []);
const confirmDelete = useCallback(async () => {
await deleteNamespaceAction(item);
closeMenu();
},[item, closeMenu])
return (<ListItem
disablePadding
secondaryAction={
hover && <IconButton onClick={openMenu} ref={listItemTextRef} >
<MoreHorizIcon />
</IconButton>
}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => !showMenu && setHover(false)}
>
<ListItemButton sx={{paddingRight: '10px'}}>
<Tooltip title={item} arrow>
<ListItemText classes={{primary: 'overflow-hidden text-ellipsis text-nowrap'}} primary={`${item}`} />
</Tooltip>
</ListItemButton>
<Menu
id={item}
open={showMenu}
onClose={closeMenu}
anchorEl={listItemTextRef.current}
anchorOrigin={{
vertical: 'center',
horizontal: 'center',
}}
>
<MenuItem color="red" onClick={openDeleteConfirmDialog}>Delete</MenuItem>
</Menu>
<Dialog
open={showDeleteConfirm}
>
<DialogTitle>Confirm</DialogTitle>
<DialogContent>
<DialogContentText>
Please confirm you want to delete namespace {item}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={closeDeleteConfirmDialog}>Cancel</Button>
<Button onClick={confirmDelete} color="error">Delete</Button>
</DialogActions>
</Dialog>
</ListItem>)
}
23 changes: 9 additions & 14 deletions webui/src/app/ui/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,28 @@
* under the License.
*/

import { Button, Divider, Drawer, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from "@mui/material";
import InboxIcon from '@mui/icons-material/MoveToInbox';
import MailIcon from '@mui/icons-material/Mail';
import { Divider, List } from "@mui/material";
import { fetchNamespaces } from "@/app/lib/api";
import NamespaceItem from "./namespace/namespaceItem";
import NamespaceCreation from "./namespace/namespaceCreation";

const dividerColor = 'rgba(255,255,255,.2)';
export default async function Sidebar() {
const namespaces = await fetchNamespaces();
return (
<div className="w-60 h-full flex">
<List className="w-full overflow-y-auto">
<div className="mt-2 mb-4 text-center">
<Button variant="outlined">Create Namespace</Button>
<NamespaceCreation />
</div>
{namespaces.map((text, index) => (<>
{namespaces.map((namespace, index) => (<>
{index === 0 && (
<Divider variant="middle" sx={{ bgcolor: dividerColor}}/>
<Divider variant="middle"/>
)}
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemText classes={{primary: 'overflow-hidden text-ellipsis text-nowrap'}} primary={text} />
</ListItemButton>
</ListItem>
<Divider variant="middle" sx={{ bgcolor: dividerColor}}/>
<NamespaceItem key={namespace} item={namespace} />
<Divider variant="middle"/>
</>))}
</List>
<Divider orientation="vertical" flexItem sx={{bgcolor: dividerColor}}/>
<Divider orientation="vertical" flexItem/>
</div>
)
}

0 comments on commit d9624af

Please sign in to comment.