Improve overall dashboard design
|
@ -44,7 +44,6 @@ services:
|
||||||
- ${APP_PORT}:80
|
- ${APP_PORT}:80
|
||||||
volumes:
|
volumes:
|
||||||
- ${APP_DATA_DIR}/data/nextcloud:/var/www/html
|
- ${APP_DATA_DIR}/data/nextcloud:/var/www/html
|
||||||
- /volumes/nfs:/nfs
|
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_HOST=db-nextcloud
|
- POSTGRES_HOST=db-nextcloud
|
||||||
- REDIS_HOST=redis-nextcloud
|
- REDIS_HOST=redis-nextcloud
|
||||||
|
|
|
@ -16,8 +16,7 @@ services:
|
||||||
TZ: ${TZ}
|
TZ: ${TZ}
|
||||||
WEBPASSWORD: ${APP_PASSWORD}
|
WEBPASSWORD: ${APP_PASSWORD}
|
||||||
PIHOLE_DNS_: 127.0.0.1#5335
|
PIHOLE_DNS_: 127.0.0.1#5335
|
||||||
FTLCONF_REPLY_ADDR4: 192.168.2.132
|
FTLCONF_REPLY_ADDR4: ${INTERNAL_IP}
|
||||||
PIHOLE_DNS_: 127.0.0.1#5335
|
|
||||||
DNSSEC: "true"
|
DNSSEC: "true"
|
||||||
DNSMASQ_LISTENING: single
|
DNSMASQ_LISTENING: single
|
||||||
networks:
|
networks:
|
||||||
|
|
2
dashboard/.eslintignore
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*.config.js
|
||||||
|
.eslintrc.js
|
|
@ -1,10 +1,10 @@
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
console.log(process.env);
|
const { NODE_ENV, INTERNAL_IP } = process.env;
|
||||||
|
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
env: {
|
env: {
|
||||||
INTERNAL_IP: process.env.INTERNAL_IP,
|
INTERNAL_IP: NODE_ENV === 'development' ? 'localhost' : INTERNAL_IP,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
"@chakra-ui/react": "^1.8.7",
|
"@chakra-ui/react": "^1.8.7",
|
||||||
"@emotion/react": "^11",
|
"@emotion/react": "^11",
|
||||||
"@emotion/styled": "^11",
|
"@emotion/styled": "^11",
|
||||||
|
"@fontsource/open-sans": "^4.5.8",
|
||||||
"axios": "^0.26.1",
|
"axios": "^0.26.1",
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
"final-form": "^4.20.6",
|
"final-form": "^4.20.6",
|
||||||
|
|
BIN
dashboard/public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
dashboard/public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
dashboard/public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
9
dashboard/public/browserconfig.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/mstile-150x150.png"/>
|
||||||
|
<TileColor>#da532c</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
BIN
dashboard/public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
dashboard/public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 14 KiB |
BIN
dashboard/public/mstile-150x150.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
20
dashboard/public/safari-pinned-tab.svg
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<metadata>
|
||||||
|
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||||
|
</metadata>
|
||||||
|
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M2263 5057 c-67 -34 -125 -65 -128 -68 -3 -4 53 -126 125 -271 l132
|
||||||
|
-263 -122 -275 c-66 -151 -130 -295 -142 -320 -11 -25 -37 -83 -58 -130 -21
|
||||||
|
-47 -347 -706 -725 -1465 -378 -759 -786 -1578 -906 -1820 l-219 -440 2337 -3
|
||||||
|
c1285 -1 2338 0 2340 2 2 2 -397 809 -888 1792 -706 1417 -931 1879 -1085
|
||||||
|
2224 l-194 434 134 267 133 267 -135 66 c-103 51 -137 64 -143 54 -5 -7 -41
|
||||||
|
-79 -81 -160 -40 -81 -75 -147 -78 -148 -3 0 -27 44 -54 98 -101 203 -111 222
|
||||||
|
-116 221 -3 0 -60 -29 -127 -62z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1,003 B |
19
dashboard/public/site.webmanifest
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"short_name": "",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
BIN
dashboard/public/tipi.png
Normal file
After Width: | Height: | Size: 11 KiB |
|
@ -1,4 +1,4 @@
|
||||||
import { Box, SlideFade, Image } from '@chakra-ui/react';
|
import { Box, SlideFade, Image, useColorModeValue } from '@chakra-ui/react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FiChevronRight } from 'react-icons/fi';
|
import { FiChevronRight } from 'react-icons/fi';
|
||||||
|
@ -6,10 +6,12 @@ import { AppConfig } from '../../core/types';
|
||||||
import AppStatus from './AppStatus';
|
import AppStatus from './AppStatus';
|
||||||
|
|
||||||
const AppTile: React.FC<{ app: AppConfig }> = ({ app }) => {
|
const AppTile: React.FC<{ app: AppConfig }> = ({ app }) => {
|
||||||
|
const bg = useColorModeValue('white', '#1a202c');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/apps/${app.id}`} passHref>
|
<Link href={`/apps/${app.id}`} passHref>
|
||||||
<SlideFade in className="flex flex-1" offsetY="20px">
|
<SlideFade in className="flex flex-1" offsetY="20px">
|
||||||
<Box minWidth={400} className="flex flex-1 bg-white drop-shadow-lg rounded-lg p-3 items-center cursor-pointer group hover:drop-shadow-md hover:bg-gray-100 transition-all">
|
<Box minWidth={400} bg={bg} className="flex flex-1 border-2 drop-shadow-sm rounded-lg p-3 items-center cursor-pointer group hover:drop-shadow-md transition-all">
|
||||||
<Image alt={`${app.name} logo`} className="rounded-md drop-shadow mr-3 group-hover:scale-105 transition-all" src={app.image} width={100} height={100} />
|
<Image alt={`${app.name} logo`} className="rounded-md drop-shadow mr-3 group-hover:scale-105 transition-all" src={app.image} width={100} height={100} />
|
||||||
<div className="mr-3 flex-1">
|
<div className="mr-3 flex-1">
|
||||||
<h3 className="font-bold text-xl">{app.name}</h3>
|
<h3 className="font-bold text-xl">{app.name}</h3>
|
||||||
|
|
|
@ -9,14 +9,14 @@ interface IProps {
|
||||||
|
|
||||||
const Header: React.FC<IProps> = ({ onClickMenu }) => {
|
const Header: React.FC<IProps> = ({ onClickMenu }) => {
|
||||||
return (
|
return (
|
||||||
<header style={{ width: '100%' }} className="flex">
|
<header style={{ width: '100%' }} className="flex h-12 md:h-0">
|
||||||
<Flex className="items-center bg-gray-700 drop-shadow-md px-5 flex-1">
|
<Flex className="items-center border-b-2 bg-graycool px-5 flex-1 py-2">
|
||||||
<div onClick={onClickMenu} className="visible md:invisible absolute cursor-pointer py-2">
|
<div onClick={onClickMenu} className="visible md:invisible absolute cursor-pointer py-2">
|
||||||
<FiMenu color="white" />
|
<FiMenu color="black" />
|
||||||
</div>
|
</div>
|
||||||
<Flex justifyContent="center" flex="1">
|
<Flex justifyContent="center" flex="1">
|
||||||
<Link href="/" passHref>
|
<Link href="/" passHref>
|
||||||
<img src="/logo.png" alt="Tipi" width={230} height={60} />
|
<img src="/tipi.png" alt="Tipi Logo" width={30} height={30} />
|
||||||
</Link>
|
</Link>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Flex, useDisclosure, Spinner, Breadcrumb, BreadcrumbItem } from '@chakra-ui/react';
|
import { Flex, useDisclosure, Spinner, Breadcrumb, BreadcrumbItem, useColorModeValue, Box } from '@chakra-ui/react';
|
||||||
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FiChevronRight } from 'react-icons/fi';
|
import { FiChevronRight } from 'react-icons/fi';
|
||||||
|
@ -12,7 +13,9 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Layout: React.FC<IProps> = ({ children, loading, breadcrumbs }) => {
|
const Layout: React.FC<IProps> = ({ children, loading, breadcrumbs }) => {
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||||
|
const menubg = useColorModeValue('#F1F3F4', '#202736');
|
||||||
|
const bg = useColorModeValue('white', '#1a202c');
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
@ -41,23 +44,26 @@ const Layout: React.FC<IProps> = ({ children, loading, breadcrumbs }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex height="100vh" className="drop-shadow-md border-r-8" direction="column">
|
<>
|
||||||
<MenuDrawer isOpen={isOpen} onClose={onClose}>
|
<Head>
|
||||||
<Menu />
|
<title>Tipi</title>
|
||||||
</MenuDrawer>
|
</Head>
|
||||||
<Header onClickMenu={onOpen} />
|
<Flex height="100vh" direction="column">
|
||||||
<Flex flex="1">
|
<MenuDrawer isOpen={isOpen} onClose={onClose}>
|
||||||
<Flex className="invisible md:visible w-0 md:w-56">
|
|
||||||
<Menu />
|
<Menu />
|
||||||
</Flex>
|
</MenuDrawer>
|
||||||
<Flex className="bg-slate-200 flex flex-1 p-5">
|
<Header onClickMenu={onOpen} />
|
||||||
<div className="flex-1 flex flex-col">
|
<Flex flex={1}>
|
||||||
|
<Flex height="100vh" bg={menubg} className="sticky top-0 invisible md:visible w-0 md:w-64">
|
||||||
|
<Menu />
|
||||||
|
</Flex>
|
||||||
|
<Box bg={bg} className="flex-1 px-4 py-4 md:px-10 md:py-8">
|
||||||
{renderBreadcrumbs()}
|
{renderBreadcrumbs()}
|
||||||
<div className="flex-1 ">{renderContent()}</div>
|
{renderContent()}
|
||||||
</div>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { AiOutlineDashboard, AiOutlineSetting, AiOutlineAppstore } from 'react-icons/ai';
|
import { AiOutlineDashboard, AiOutlineSetting, AiOutlineAppstore } from 'react-icons/ai';
|
||||||
import { Divider, List, ListItem } from '@chakra-ui/react';
|
import { FaRegMoon } from 'react-icons/fa';
|
||||||
|
import Package from '../../../package.json';
|
||||||
|
import { Box, Divider, Flex, List, ListItem, Switch, useColorMode } from '@chakra-ui/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
@ -8,32 +10,54 @@ import { IconType } from 'react-icons';
|
||||||
|
|
||||||
const SideMenu: React.FC = () => {
|
const SideMenu: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { colorMode, setColorMode } = useColorMode();
|
||||||
const path = router.pathname.split('/')[1];
|
const path = router.pathname.split('/')[1];
|
||||||
|
|
||||||
const renderMenuItem = (title: string, name: string, Icon: IconType) => {
|
const renderMenuItem = (title: string, name: string, Icon: IconType) => {
|
||||||
const selected = path === name;
|
const selected = path === name;
|
||||||
|
|
||||||
|
const itemClass = clsx('mx-3 border-transparent rounded-lg p-3 transition-colors border-1', {
|
||||||
|
'drop-shadow-sm border-gray-200': selected && colorMode === 'light',
|
||||||
|
'bg-white': selected && colorMode === 'light',
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/${name}`} passHref>
|
<Link href={`/${name}`} passHref>
|
||||||
<div className={clsx('mx-3 rounded-lg p-3 transition-colors', { 'bg-slate-200 drop-shadow-sm': selected })}>
|
<div className={itemClass}>
|
||||||
<ListItem className={'flex items-center cursor-pointer hover:font-bold'}>
|
<ListItem className={'flex items-center cursor-pointer hover:font-bold'}>
|
||||||
<Icon size={20} className="mr-3" />
|
<Icon size={20} className={clsx('mr-3', { 'text-red-600': selected && colorMode === 'light', 'text-red-200': selected && colorMode === 'dark' })} />
|
||||||
<p className={clsx({ 'font-bold': selected })}>{title}</p>
|
<p className={clsx({ 'font-bold': selected, 'text-red-600': selected && colorMode === 'light', 'text-red-200': selected && colorMode === 'dark' })}>{title}</p>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChangeColorMode = (checked: boolean) => {
|
||||||
|
setColorMode(checked ? 'dark' : 'light');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List spacing={3} className="pt-5 flex-1 bg-white md:border-r-2">
|
<Box className="flex-1 flex flex-col p-0 md:p-4">
|
||||||
{renderMenuItem('Dashboard', '', AiOutlineDashboard)}
|
<img className="self-center mb-5 logo mt-0 md:mt-5" src="/tipi.png" width={512} height={512} />
|
||||||
<Divider />
|
<List spacing={3} className="pt-5">
|
||||||
{renderMenuItem('Apps', 'apps', AiOutlineAppstore)}
|
{renderMenuItem('Dashboard', '', AiOutlineDashboard)}
|
||||||
<Divider />
|
{renderMenuItem('Apps', 'apps', AiOutlineAppstore)}
|
||||||
{renderMenuItem('Settings', 'settings', AiOutlineSetting)}
|
{renderMenuItem('Settings', 'settings', AiOutlineSetting)}
|
||||||
</List>
|
</List>
|
||||||
|
<Divider className="my-3" />
|
||||||
|
<Flex flex="1" />
|
||||||
|
<List>
|
||||||
|
<div className="mx-3">
|
||||||
|
<ListItem className="flex items-center">
|
||||||
|
<FaRegMoon size={20} className="mr-3" />
|
||||||
|
<p className="flex-1">Dark mode</p>
|
||||||
|
<Switch checked={colorMode === 'dark'} onChange={(event) => handleChangeColorMode(event.target.checked)} />
|
||||||
|
</ListItem>
|
||||||
|
</div>
|
||||||
|
</List>
|
||||||
|
<div className="pb-1 text-center text-sm text-gray-400 mt-5">Tipi version {Package.version}</div>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Drawer, DrawerBody, DrawerCloseButton, DrawerContent, DrawerFooter, DrawerHeader, DrawerOverlay } from '@chakra-ui/react';
|
import { Drawer, DrawerBody, DrawerCloseButton, DrawerContent, DrawerHeader, DrawerOverlay, useColorModeValue } from '@chakra-ui/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
@ -7,16 +7,15 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const MenuDrawer: React.FC<IProps> = ({ children, isOpen, onClose }) => {
|
const MenuDrawer: React.FC<IProps> = ({ children, isOpen, onClose }) => {
|
||||||
|
const menubg = useColorModeValue('#F1F3F4', '#202736');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer size="xs" isOpen={isOpen} placement="left" onClose={onClose}>
|
<Drawer size="xs" isOpen={isOpen} placement="left" onClose={onClose}>
|
||||||
<DrawerOverlay />
|
<DrawerOverlay />
|
||||||
<DrawerContent>
|
<DrawerContent bg={menubg}>
|
||||||
<DrawerCloseButton />
|
<DrawerCloseButton />
|
||||||
<DrawerHeader>My Tipi</DrawerHeader>
|
<DrawerHeader>My Tipi</DrawerHeader>
|
||||||
<DrawerBody>{children}</DrawerBody>
|
<DrawerBody display="flex">{children}</DrawerBody>
|
||||||
<DrawerFooter>
|
|
||||||
<div>Github</div>
|
|
||||||
</DrawerFooter>
|
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import axios, { Method } from 'axios';
|
import axios, { Method } from 'axios';
|
||||||
|
|
||||||
export const BASE_URL = 'http://192.168.2.132:3001';
|
export const BASE_URL = `http://${process.env.INTERNAL_IP}:3001`;
|
||||||
|
|
||||||
console.log(process.env);
|
|
||||||
|
|
||||||
interface IFetchParams {
|
interface IFetchParams {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
|
|
|
@ -28,7 +28,7 @@ const AppActions: React.FC<IProps> = ({ app, onInstall, onUninstall, onStart, on
|
||||||
<FiTrash2 className="ml-1" />
|
<FiTrash2 className="ml-1" />
|
||||||
</Button>
|
</Button>
|
||||||
{hasSettings && (
|
{hasSettings && (
|
||||||
<Button onClick={onUpdate} width={160} className="mt-3 mr-2">
|
<Button onClick={onUpdate} width={160} colorScheme="gray" className="mt-3 mr-2">
|
||||||
Settings
|
Settings
|
||||||
<FiSettings className="ml-1" />
|
<FiSettings className="ml-1" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -93,7 +93,7 @@ const AppDetails: React.FC<IProps> = ({ app }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SlideFade in className="flex flex-1" offsetY="20px">
|
<SlideFade in className="flex flex-1" offsetY="20px">
|
||||||
<div className="flex flex-1 bg-white p-4 mt-3 rounded-lg drop-shadow-xl flex-col">
|
<div className="flex flex-1 p-4 mt-3 rounded-lg flex-col">
|
||||||
<Flex className="flex-col md:flex-row">
|
<Flex className="flex-col md:flex-row">
|
||||||
<Image src={app?.image} height={180} width={180} className="rounded-xl self-center sm:self-auto" alt={app.name} />
|
<Image src={app?.image} height={180} width={180} className="rounded-xl self-center sm:self-auto" alt={app.name} />
|
||||||
<VStack align="flex-start" justify="space-between" className="ml-0 md:ml-4">
|
<VStack align="flex-start" justify="space-between" className="ml-0 md:ml-4">
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import { ChakraProvider } from '@chakra-ui/react';
|
import '@fontsource/open-sans/700.css';
|
||||||
|
import '@fontsource/open-sans/400.css';
|
||||||
import '../styles/globals.css';
|
import '../styles/globals.css';
|
||||||
|
import { ChakraProvider } from '@chakra-ui/react';
|
||||||
import type { AppProps } from 'next/app';
|
import type { AppProps } from 'next/app';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useNetworkStore } from '../state/networkStore';
|
import { useNetworkStore } from '../state/networkStore';
|
||||||
|
import { theme } from '../styles/theme';
|
||||||
|
|
||||||
function MyApp({ Component, pageProps }: AppProps) {
|
function MyApp({ Component, pageProps }: AppProps) {
|
||||||
const { fetchInternalIp } = useNetworkStore();
|
const { fetchInternalIp } = useNetworkStore();
|
||||||
|
@ -12,7 +15,7 @@ function MyApp({ Component, pageProps }: AppProps) {
|
||||||
}, [fetchInternalIp]);
|
}, [fetchInternalIp]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChakraProvider>
|
<ChakraProvider theme={theme}>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</ChakraProvider>
|
</ChakraProvider>
|
||||||
);
|
);
|
||||||
|
|
25
dashboard/src/pages/_document.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Html, Head, Main, NextScript } from 'next/document';
|
||||||
|
import { ColorModeScript } from '@chakra-ui/react';
|
||||||
|
import { theme } from '../styles/theme';
|
||||||
|
|
||||||
|
export default function MyDocument() {
|
||||||
|
return (
|
||||||
|
<Html lang="en">
|
||||||
|
<Head>
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
|
||||||
|
<meta name="msapplication-TileColor" content="#da532c" />
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
</Head>
|
||||||
|
<body>
|
||||||
|
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
}
|
|
@ -19,13 +19,13 @@ const Apps: NextPage = () => {
|
||||||
return (
|
return (
|
||||||
<Layout loading={loading}>
|
<Layout loading={loading}>
|
||||||
<Flex className="flex-col">
|
<Flex className="flex-col">
|
||||||
{installedCount > 0 && <h1 className="font-bold text-2xl mb-3">Your Apps ({installedCount})</h1>}
|
{installedCount > 0 && <h1 className="font-bold text-3xl mb-5">Your Apps ({installedCount})</h1>}
|
||||||
<SimpleGrid minChildWidth="400px" spacing="20px">
|
<SimpleGrid minChildWidth="400px" spacing="20px">
|
||||||
{installed().map((app) => (
|
{installed().map((app) => (
|
||||||
<AppTile key={app.name} app={app} />
|
<AppTile key={app.name} app={app} />
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
{available().length && <h1 className="font-bold text-2xl mb-3 mt-3">Available Apps</h1>}
|
{available().length && <h1 className="font-bold text-3xl mb-5 mt-5">Available Apps</h1>}
|
||||||
<SimpleGrid minChildWidth="400px" spacing="20px">
|
<SimpleGrid minChildWidth="400px" spacing="20px">
|
||||||
{available().map((app) => (
|
{available().map((app) => (
|
||||||
<AppTile key={app.name} app={app} />
|
<AppTile key={app.name} app={app} />
|
||||||
|
|
|
@ -1,10 +1,70 @@
|
||||||
|
import { Progress, SimpleGrid, Stat, StatHelpText, StatLabel, StatNumber, Text } from '@chakra-ui/react';
|
||||||
import type { NextPage } from 'next';
|
import type { NextPage } from 'next';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import Layout from '../components/Layout';
|
import Layout from '../components/Layout';
|
||||||
|
import { useSytemStore } from '../state/systemStore';
|
||||||
|
import { BsCpu } from 'react-icons/bs';
|
||||||
|
import { FiHardDrive } from 'react-icons/fi';
|
||||||
|
import { FaMemory } from 'react-icons/fa';
|
||||||
|
|
||||||
const Home: NextPage = () => {
|
const Home: NextPage = () => {
|
||||||
|
const { fetchDiskSpace, fetchCpuLoad, fetchMemoryLoad, disk, cpuLoad, memory } = useSytemStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDiskSpace();
|
||||||
|
fetchCpuLoad();
|
||||||
|
fetchMemoryLoad();
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
fetchDiskSpace();
|
||||||
|
fetchCpuLoad();
|
||||||
|
fetchMemoryLoad();
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [fetchCpuLoad, fetchDiskSpace]);
|
||||||
|
|
||||||
|
// Convert bytes to GB
|
||||||
|
const diskFree = Math.round(disk.available / 1024 / 1024 / 1024);
|
||||||
|
const diskSize = Math.round(disk.size / 1024 / 1024 / 1024);
|
||||||
|
const diskUsed = diskSize - diskFree;
|
||||||
|
const percentUsed = Math.round((diskUsed / diskSize) * 100);
|
||||||
|
|
||||||
|
const memoryTotal = Math.round(memory?.total / 1024 / 1024 / 1024);
|
||||||
|
const memoryUsed = Math.round(memory?.used / 1024 / 1024 / 1024);
|
||||||
|
const percentUsedMemory = Math.round((memoryUsed / memoryTotal) * 100);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div>Content</div>
|
<Text fontSize="3xl" className="font-bold">
|
||||||
|
Tipi Dashboard
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="xl" color="gray.500">
|
||||||
|
Welcome home!
|
||||||
|
</Text>
|
||||||
|
<SimpleGrid className="mt-5" minChildWidth="180px" spacing="20px">
|
||||||
|
<Stat className="border-2 px-5 py-3 rounded-lg">
|
||||||
|
<StatLabel>Disk space</StatLabel>
|
||||||
|
<StatNumber>{diskUsed} GB</StatNumber>
|
||||||
|
<StatHelpText>Used out of {diskSize} GB</StatHelpText>
|
||||||
|
<Progress value={percentUsed} size="sm" />
|
||||||
|
<FiHardDrive size={30} className="absolute top-3 right-3" />
|
||||||
|
</Stat>
|
||||||
|
<Stat className="border-2 px-5 py-3 rounded-lg">
|
||||||
|
<StatLabel>CPU Load</StatLabel>
|
||||||
|
<StatNumber>{cpuLoad.toFixed(2)}%</StatNumber>
|
||||||
|
<StatHelpText>Uninstall apps if there is to much load</StatHelpText>
|
||||||
|
<Progress value={cpuLoad} size="sm" />
|
||||||
|
<BsCpu size={30} className="absolute top-3 right-3" />
|
||||||
|
</Stat>
|
||||||
|
<Stat className="border-2 px-5 py-3 rounded-lg">
|
||||||
|
<StatLabel>Memory Used</StatLabel>
|
||||||
|
<StatNumber>{percentUsedMemory}%</StatNumber>
|
||||||
|
<StatHelpText>{memoryTotal} GB</StatHelpText>
|
||||||
|
<Progress value={percentUsedMemory} size="sm" />
|
||||||
|
<FaMemory size={30} className="absolute top-3 right-3" />
|
||||||
|
</Stat>
|
||||||
|
</SimpleGrid>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
41
dashboard/src/state/systemStore.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import create from 'zustand';
|
||||||
|
import api from '../core/api';
|
||||||
|
|
||||||
|
type Store = {
|
||||||
|
cpuLoad: number;
|
||||||
|
disk: { size: number; used: number; available: number };
|
||||||
|
memory: { total: number; used: number; free: number };
|
||||||
|
fetchDiskSpace: () => void;
|
||||||
|
fetchCpuLoad: () => void;
|
||||||
|
fetchMemoryLoad: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSytemStore = create<Store>((set) => ({
|
||||||
|
cpuLoad: 0,
|
||||||
|
memory: { total: 0, used: 0, free: 0 },
|
||||||
|
disk: { size: 0, used: 0, available: 0 },
|
||||||
|
fetchDiskSpace: async () => {
|
||||||
|
const response = await api.fetch<any>({
|
||||||
|
endpoint: '/system/disk',
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
|
||||||
|
set({ disk: response });
|
||||||
|
},
|
||||||
|
fetchCpuLoad: async () => {
|
||||||
|
const response = await api.fetch<any>({
|
||||||
|
endpoint: '/system/cpu',
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
|
||||||
|
set({ cpuLoad: response.load });
|
||||||
|
},
|
||||||
|
fetchMemoryLoad: async () => {
|
||||||
|
const response = await api.fetch<any>({
|
||||||
|
endpoint: '/system/memory',
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
|
||||||
|
set({ memory: response });
|
||||||
|
},
|
||||||
|
}));
|
|
@ -7,7 +7,7 @@ body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
font-family: Open Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,3 +19,20 @@ a {
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-graycool {
|
||||||
|
background-color:#F1F3F4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-graycool {
|
||||||
|
border-color:#F1F3F4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-1 {
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
17
dashboard/src/styles/theme.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { extendTheme, type ThemeConfig, type Theme, withDefaultColorScheme } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const config: ThemeConfig = {
|
||||||
|
initialColorMode: 'light',
|
||||||
|
useSystemColorMode: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const theme: Theme = extendTheme(
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
fonts: {
|
||||||
|
heading: 'Open Sans, sans-serif',
|
||||||
|
body: 'Open Sans, sans-serif',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
withDefaultColorScheme({ colorScheme: 'red' }),
|
||||||
|
) as Theme;
|
|
@ -13,7 +13,8 @@
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true
|
"incremental": true,
|
||||||
|
"strictNullChecks": true
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
|
|
|
@ -755,6 +755,11 @@
|
||||||
minimatch "^3.0.4"
|
minimatch "^3.0.4"
|
||||||
strip-json-comments "^3.1.1"
|
strip-json-comments "^3.1.1"
|
||||||
|
|
||||||
|
"@fontsource/open-sans@^4.5.8":
|
||||||
|
version "4.5.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@fontsource/open-sans/-/open-sans-4.5.8.tgz#31f727353e89ce886e1076bd58536834e0778fda"
|
||||||
|
integrity sha512-3b94XDdRLqL7OlE7OjWg/4pgG825Juw8PLVEDm6h5pio0gMU89ICxfatGxHsBxMGfqad+wnvdmUweZWlELDFpQ==
|
||||||
|
|
||||||
"@humanwhocodes/config-array@^0.9.2":
|
"@humanwhocodes/config-array@^0.9.2":
|
||||||
version "0.9.5"
|
version "0.9.5"
|
||||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7"
|
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7"
|
||||||
|
|
|
@ -89,8 +89,6 @@ compose() {
|
||||||
export APP_DATA_DIR="${app_data_dir}"
|
export APP_DATA_DIR="${app_data_dir}"
|
||||||
export APP_DIR="${app_dir}"
|
export APP_DIR="${app_dir}"
|
||||||
|
|
||||||
# TODO: Fix for dynamic detection
|
|
||||||
export DEVICE_IP="192.168.2.132"
|
|
||||||
export ROOT_FOLDER="${ROOT_FOLDER}"
|
export ROOT_FOLDER="${ROOT_FOLDER}"
|
||||||
|
|
||||||
# Docker-compose does not support multiple env files
|
# Docker-compose does not support multiple env files
|
||||||
|
|