implemented navbar
This commit is contained in:
parent
f5cddc4bcd
commit
9a49b30cd1
@ -1,4 +1,5 @@
|
|||||||
import { useLocation, Outlet, Navigate } from "react-router-dom";
|
import { useLocation, Outlet, Navigate } from "react-router-dom";
|
||||||
|
import NavBar from "./components/navbar/NavBar";
|
||||||
import useAuth from "./hooks/useAuth";
|
import useAuth from "./hooks/useAuth";
|
||||||
|
|
||||||
const ProtectedRoute = () => {
|
const ProtectedRoute = () => {
|
||||||
@ -7,6 +8,7 @@ const ProtectedRoute = () => {
|
|||||||
|
|
||||||
return ((auth.loginNeeded !== undefined && !auth.loginNeeded) || auth?.isLoggedIn) ? (
|
return ((auth.loginNeeded !== undefined && !auth.loginNeeded) || auth?.isLoggedIn) ? (
|
||||||
<>
|
<>
|
||||||
|
<NavBar />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -6,12 +6,14 @@ import axios from "../../api/axios";
|
|||||||
import useAuth from "../../hooks/useAuth";
|
import useAuth from "../../hooks/useAuth";
|
||||||
import Tag from "../../interfaces/Tag";
|
import Tag from "../../interfaces/Tag";
|
||||||
import Repository from "../../interfaces/Repositoriy";
|
import Repository from "../../interfaces/Repositoriy";
|
||||||
|
import Layer from "../../interfaces/Layer";
|
||||||
|
|
||||||
const Home: React.FC = () => {
|
const Home: React.FC = () => {
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
|
|
||||||
const [repositories, setRepositories] = useState<Repository[]>([]);
|
const [repositories, setRepositories] = useState<Repository[]>([]);
|
||||||
|
|
||||||
|
auth['repositories'] = repositories;
|
||||||
const getTagInfo = async(newRepositories: Repository[]) => {
|
const getTagInfo = async(newRepositories: Repository[]) => {
|
||||||
let promises: Promise<void>[] = [];
|
let promises: Promise<void>[] = [];
|
||||||
newRepositories.forEach((repository: Repository) => {
|
newRepositories.forEach((repository: Repository) => {
|
||||||
@ -28,7 +30,16 @@ const Home: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
).then((response) => {
|
).then((response) => {
|
||||||
|
tag.schemaVersion = response?.data['schemaVersion'];
|
||||||
|
tag.mediaType = response?.data['mediaType'];
|
||||||
tag.digest = response?.headers['docker-content-digest'];
|
tag.digest = response?.headers['docker-content-digest'];
|
||||||
|
tag.config = response?.data?.config;
|
||||||
|
tag.layers = response?.data?.layers;
|
||||||
|
let size = 0;
|
||||||
|
response?.data?.layers.forEach((layer: Layer) => {
|
||||||
|
size += layer.size;
|
||||||
|
});
|
||||||
|
tag.size = size;
|
||||||
return axios.get(
|
return axios.get(
|
||||||
"/" + repository.name + "/blobs/" + response?.data?.config['digest'],
|
"/" + repository.name + "/blobs/" + response?.data?.config['digest'],
|
||||||
{
|
{
|
||||||
@ -71,6 +82,11 @@ const Home: React.FC = () => {
|
|||||||
architecture: "",
|
architecture: "",
|
||||||
os: "",
|
os: "",
|
||||||
created: undefined,
|
created: undefined,
|
||||||
|
schemaVersion: undefined,
|
||||||
|
mediaType: "",
|
||||||
|
config: undefined,
|
||||||
|
layers: undefined,
|
||||||
|
size: undefined,
|
||||||
digest: ""
|
digest: ""
|
||||||
}
|
}
|
||||||
tags.push(tag);
|
tags.push(tag);
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ListItemButton, Box, Typography, tableFooterClasses } from "@mui/material";
|
import { ListItemButton, Box, Typography, Grid, ListItemText } from "@mui/material";
|
||||||
import Tag from "../../interfaces/Tag";
|
import Tag from "../../interfaces/Tag";
|
||||||
import Repository from "../../interfaces/Repositoriy";
|
import Repository from "../../interfaces/Repositoriy";
|
||||||
import { printTimePassed } from "../../utils";
|
import { printSize, printTimePassed } from "../../utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@ -14,10 +14,18 @@ const RepositoryItem: React.FC<Props> = (props: Props) => {
|
|||||||
<ListItemButton
|
<ListItemButton
|
||||||
component="a"
|
component="a"
|
||||||
>
|
>
|
||||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
{/* <Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||||
<Typography color="black" variant="h5">{props.repository.name}</Typography>
|
</Box> */}
|
||||||
<Typography color="grey" variant="h6">{"Last updated: " + props.repository.tags.filter((tag: Tag) => (tag.label === "latest")).map((tag: Tag) => (tag.created ? printTimePassed(tag.created) : "")).join(" ")}</Typography>
|
<Grid container>
|
||||||
</Box>
|
<Grid item sm={9}>
|
||||||
|
<ListItemText primary={props.repository.name} secondary={"Last updated: " + props.repository.tags.filter((tag: Tag) => (tag.label === "latest")).map((tag: Tag) => (tag.created ? printTimePassed(tag.created) : "")).join(" ")} />
|
||||||
|
{/* <Typography color="black" variant="h5">{props.repository.name}</Typography>
|
||||||
|
<Typography color="grey" variant="h6">{"Last updated: " + props.repository.tags.filter((tag: Tag) => (tag.label === "latest")).map((tag: Tag) => (tag.created ? printTimePassed(tag.created) : "")).join(" ")}</Typography> */}
|
||||||
|
</Grid>
|
||||||
|
<Grid item sm={3}>
|
||||||
|
<ListItemText style={{display:'flex', justifyContent:'flex-end'}} primary={props.repository.tags.filter((tag: Tag) => (tag.label === "latest")).map((tag: Tag) => (tag.size ? printSize(tag.size) : "")).join(" ")} />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
81
src/components/navbar/NavBar.tsx
Normal file
81
src/components/navbar/NavBar.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { AppBar, Container, Toolbar, Typography, Box, IconButton, Menu, MenuItem , Tooltip, Avatar, Snackbar, Alert} from "@mui/material";
|
||||||
|
import useAuth from "../../hooks/useAuth";
|
||||||
|
|
||||||
|
const NavBar: React.FC = () => {
|
||||||
|
const auth = useAuth();
|
||||||
|
const [anchorElUser, setAnchorElUser] = React.useState<(EventTarget & HTMLButtonElement) | null>(null);
|
||||||
|
const [snackbarOpen, setSnackbarOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const handleCloseUserMenu = () => {
|
||||||
|
setAnchorElUser(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
const url = process.env.REGISTRY_URL ? process.env.REGISTRY_URL : "";
|
||||||
|
const urlSplit = url.split('/');
|
||||||
|
navigator.clipboard.writeText(urlSplit[urlSplit.length - 1]);
|
||||||
|
setSnackbarOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setSnackbarOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppBar className="navbar" position="static">
|
||||||
|
<Container maxWidth={false} sx={{ m: 0, width: '100%' }}>
|
||||||
|
<Toolbar disableGutters>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
noWrap
|
||||||
|
component="div"
|
||||||
|
sx={{ mr: 2, display: 'flex' }}
|
||||||
|
>
|
||||||
|
Docker Registry
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ flexGrow: 1, display: 'flex' }}>
|
||||||
|
<Typography onClick={handleClick} className="centerText" > Your registry instance is at {process.env.REGISTRY_URL ? process.env.REGISTRY_URL : ""} </Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Snackbar open={snackbarOpen} autoHideDuration={6000} onClose={handleClose}>
|
||||||
|
<Alert onClose={handleClose} severity="success" sx={{ width: '100%' }}>
|
||||||
|
Registry address copied!
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
|
||||||
|
<Box sx={{ flexGrow: 0 }}>
|
||||||
|
<Tooltip title="Open settings">
|
||||||
|
<IconButton onClick={(event) => {setAnchorElUser(event.currentTarget)}} sx={{ p: 0 }}>
|
||||||
|
<Avatar alt={auth['username']} src="/pic.png" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Menu
|
||||||
|
sx={{ mt: '45px' }}
|
||||||
|
id="menu-appbar"
|
||||||
|
anchorEl={anchorElUser}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
|
keepMounted
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
|
open={Boolean(anchorElUser)}
|
||||||
|
onClose={handleCloseUserMenu}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={handleCloseUserMenu}>
|
||||||
|
<Typography textAlign="center">Account</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</Box>
|
||||||
|
</Toolbar>
|
||||||
|
</Container>
|
||||||
|
</AppBar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavBar;
|
||||||
@ -1,21 +1,24 @@
|
|||||||
import { createContext, useState } from "react";
|
import { createContext, useState } from "react";
|
||||||
|
import Repository from "../interfaces/Repositoriy";
|
||||||
|
|
||||||
interface loginInfo {
|
interface loginInfo {
|
||||||
loginNeeded: boolean;
|
loginNeeded: boolean;
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
repositories: Repository[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<loginInfo>({
|
const AuthContext = createContext<loginInfo>({
|
||||||
loginNeeded: true,
|
loginNeeded: true,
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
username: "",
|
username: "",
|
||||||
password: ""
|
password: "",
|
||||||
|
repositories: []
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
const [auth] = useState<loginInfo>({ loginNeeded: true, isLoggedIn: false, username: "", password: "" });
|
const [auth] = useState<loginInfo>({ loginNeeded: true, isLoggedIn: false, username: "", password: "", repositories: [] });
|
||||||
|
|
||||||
return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
|
return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import AuthContext from "../context/AuthProvider";
|
import AuthContext from "../context/AuthProvider";
|
||||||
|
import Repository from "../interfaces/Repositoriy";
|
||||||
|
|
||||||
interface loginInfo {
|
interface loginInfo {
|
||||||
loginNeeded: boolean;
|
loginNeeded: boolean;
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
repositories: Repository[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const useAuth = () => {
|
const useAuth = () => {
|
||||||
|
|||||||
7
src/interfaces/Layer.tsx
Normal file
7
src/interfaces/Layer.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
interface Layer {
|
||||||
|
mediaType: string;
|
||||||
|
size: number;
|
||||||
|
digest: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layer;
|
||||||
@ -1,8 +1,21 @@
|
|||||||
|
import Layer from "./Layer"
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
mediaType: string;
|
||||||
|
size: number;
|
||||||
|
digest: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface Tag {
|
interface Tag {
|
||||||
label: string;
|
label: string;
|
||||||
architecture: string;
|
architecture: string;
|
||||||
os: string;
|
os: string;
|
||||||
created: Date | undefined;
|
created: Date | undefined;
|
||||||
|
schemaVersion: number | undefined;
|
||||||
|
mediaType: string;
|
||||||
|
config: Config | undefined;
|
||||||
|
layers: Layer[] | undefined;
|
||||||
|
size: number | undefined;
|
||||||
digest: string;
|
digest: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
.navbar
|
||||||
|
margin: 0
|
||||||
|
.centerText
|
||||||
|
text-align: center
|
||||||
|
width: 90%
|
||||||
@ -6,6 +6,15 @@ a {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .centerText {
|
||||||
|
text-align: center;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
.sectionHome .repositoryList {
|
.sectionHome .repositoryList {
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"mappings": "AG8CA,AAAA,WAAW,CAAC;EACR,KAAK,EAAE,IAAI;CAAG;;AAElB,AAAA,CAAC,CAAC;EACE,eAAe,EAAE,IAAI;CAAG;;AKlD5B,AACI,YADQ,CACR,eAAe,CAAC;EACZ,SAAS,EAAE,GAAG;EACd,MAAM,EAAE,IAAI;CAAG;;ACHvB,AACI,MADE,CACF,eAAe,CAAC;EACZ,WAAW,EAAE,MAAM;EACnB,MAAM,EAAE,IAAI;EACZ,UAAU,EAAE,GAAG;EACf,SAAS,EAAE,cAAc;CAeS;;AApB1C,AAMQ,MANF,CACF,eAAe,GAKT,CAAC,CAAC;EACA,KAAK,EAAE,GAAG;EACV,MAAM,EAAE,MAAM;CAAG;;AAR7B,AASQ,MATF,CACF,eAAe,CAQX,YAAY,CAAC;EACT,OAAO,EAAE,YAAY;EACrB,UAAU,EAAE,MAAM;EAClB,MAAM,EAAE,IAAI;EACZ,SAAS,EAAE,IAAI;CAAG;;AAb9B,AAcQ,MAdF,CACF,eAAe,CAaX,UAAU,CAAC;EACP,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,GAAG;CACpB;;AAjBF,AAkBQ,MAlBF,CACF,eAAe,CAiBX,eAAe,CAAC;EACZ,MAAM,EAAE,IAAI;EACZ,gBAAgB,EAAE,OAAO;CAAG;;ACpBxC,AAAA,QAAQ,CAAC;EACL,SAAS,EAAE,GAAG;EACd,MAAM,EAAE,IAAI;EACZ,UAAU,EAAE,GAAG;CAEU;;AAL7B,AAII,QAJI,CAIJ,eAAe,CAAC;EACZ,UAAU,EAAE,IAAI;CAAG",
|
"mappings": "AG8CA,AAAA,WAAW,CAAC;EACR,KAAK,EAAE,IAAI;CAAG;;AAElB,AAAA,CAAC,CAAC;EACE,eAAe,EAAE,IAAI;CAAG;;AElD5B,AAAA,OAAO,CAAC;EACJ,MAAM,EAAE,CAAC;CAGU;;AAJvB,AAEI,OAFG,CAEH,WAAW,CAAC;EACR,UAAU,EAAE,MAAM;EAClB,KAAK,EAAE,GAAG;CAAG;;AGJrB,AACI,YADQ,CACR,eAAe,CAAC;EACZ,SAAS,EAAE,GAAG;EACd,MAAM,EAAE,IAAI;CAAG;;ACHvB,AACI,MADE,CACF,eAAe,CAAC;EACZ,WAAW,EAAE,MAAM;EACnB,MAAM,EAAE,IAAI;EACZ,UAAU,EAAE,GAAG;EACf,SAAS,EAAE,cAAc;CAeS;;AApB1C,AAMQ,MANF,CACF,eAAe,GAKT,CAAC,CAAC;EACA,KAAK,EAAE,GAAG;EACV,MAAM,EAAE,MAAM;CAAG;;AAR7B,AASQ,MATF,CACF,eAAe,CAQX,YAAY,CAAC;EACT,OAAO,EAAE,YAAY;EACrB,UAAU,EAAE,MAAM;EAClB,MAAM,EAAE,IAAI;EACZ,SAAS,EAAE,IAAI;CAAG;;AAb9B,AAcQ,MAdF,CACF,eAAe,CAaX,UAAU,CAAC;EACP,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,GAAG;CACpB;;AAjBF,AAkBQ,MAlBF,CACF,eAAe,CAiBX,eAAe,CAAC;EACZ,MAAM,EAAE,IAAI;EACZ,gBAAgB,EAAE,OAAO;CAAG;;ACpBxC,AAAA,QAAQ,CAAC;EACL,SAAS,EAAE,GAAG;EACd,MAAM,EAAE,IAAI;EACZ,UAAU,EAAE,GAAG;CAEU;;AAL7B,AAII,QAJI,CAIJ,eAAe,CAAC;EACZ,UAAU,EAAE,IAAI;CAAG",
|
||||||
"sources": [
|
"sources": [
|
||||||
"style.sass",
|
"style.sass",
|
||||||
"_variables.sass",
|
"_variables.sass",
|
||||||
|
|||||||
@ -15,6 +15,10 @@ const checkValidURL = async (url: string) => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const printTwoDecimalPlaces = (num: number): string => {
|
||||||
|
return (Math.round(num * 100) / 100).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
const printTimePassed = (date: Date): string => {
|
const printTimePassed = (date: Date): string => {
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
const secondsPassed = (currentTime - date.getTime()) / 1000;
|
const secondsPassed = (currentTime - date.getTime()) / 1000;
|
||||||
@ -37,4 +41,12 @@ const printTimePassed = (date: Date): string => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { checkValidURL , printTimePassed };
|
const printSize = (sizeInByte: number): string => {
|
||||||
|
if (sizeInByte < 1073741824) {
|
||||||
|
return printTwoDecimalPlaces(sizeInByte / 1048576) + " MiB";
|
||||||
|
} else {
|
||||||
|
return printTwoDecimalPlaces(sizeInByte / 1073741824) + " GiB";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { checkValidURL, printTwoDecimalPlaces, printTimePassed, printSize };
|
||||||
Loading…
Reference in New Issue
Block a user