commit
d4a2b15c48
11 changed files with 96 additions and 61 deletions
|
@ -1,4 +1,4 @@
|
|||
version: "3.7"
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
reverse-proxy:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "runtipi",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"description": "A homeserver for everyone",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "dashboard",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "jest --colors",
|
||||
|
|
|
@ -6,6 +6,7 @@ import { FiChevronRight } from 'react-icons/fi';
|
|||
import Header from './Header';
|
||||
import Menu from './SideMenu';
|
||||
import MenuDrawer from './MenuDrawer';
|
||||
// import UpdateBanner from './UpdateBanner';
|
||||
|
||||
interface IProps {
|
||||
loading?: boolean;
|
||||
|
@ -15,6 +16,7 @@ interface IProps {
|
|||
|
||||
const Layout: React.FC<IProps> = ({ children, loading, breadcrumbs }) => {
|
||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||
|
||||
const menubg = useColorModeValue('#F1F3F4', '#202736');
|
||||
const bg = useColorModeValue('white', '#1a202c');
|
||||
|
||||
|
@ -49,7 +51,6 @@ const Layout: React.FC<IProps> = ({ children, loading, breadcrumbs }) => {
|
|||
<Head>
|
||||
<title>Tipi</title>
|
||||
</Head>
|
||||
|
||||
<Flex height="100vh" direction="column">
|
||||
<MenuDrawer isOpen={isOpen} onClose={onClose}>
|
||||
<Menu />
|
||||
|
@ -60,6 +61,7 @@ const Layout: React.FC<IProps> = ({ children, loading, breadcrumbs }) => {
|
|||
<Menu />
|
||||
</Flex>
|
||||
<Box bg={bg} className="flex-1 px-4 py-4 md:px-10 md:py-8">
|
||||
{/* <UpdateBanner /> */}
|
||||
{renderBreadcrumbs()}
|
||||
{renderContent()}
|
||||
</Box>
|
||||
|
|
|
@ -2,20 +2,23 @@ import { AiOutlineDashboard, AiOutlineSetting, AiOutlineAppstore } from 'react-i
|
|||
import { FaAppStore, FaRegMoon } from 'react-icons/fa';
|
||||
import { FiLogOut } from 'react-icons/fi';
|
||||
import Package from '../../../package.json';
|
||||
import { Box, Divider, Flex, List, ListItem, Switch, useColorMode } from '@chakra-ui/react';
|
||||
import { Badge, Box, Divider, Flex, List, ListItem, Switch, useColorMode } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import clsx from 'clsx';
|
||||
import { useRouter } from 'next/router';
|
||||
import { IconType } from 'react-icons';
|
||||
import { useLogoutMutation } from '../../generated/graphql';
|
||||
import { useLogoutMutation, useVersionQuery } from '../../generated/graphql';
|
||||
|
||||
const SideMenu: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { colorMode, setColorMode } = useColorMode();
|
||||
const [logout] = useLogoutMutation({ refetchQueries: ['Me'] });
|
||||
const versionQuery = useVersionQuery();
|
||||
const path = router.pathname.split('/')[1];
|
||||
|
||||
const isLatest = versionQuery.data?.version.latest === versionQuery.data?.version.current;
|
||||
|
||||
const renderMenuItem = (title: string, name: string, Icon: IconType) => {
|
||||
const selected = path === name;
|
||||
|
||||
|
@ -65,6 +68,11 @@ const SideMenu: React.FC = () => {
|
|||
</div>
|
||||
</List>
|
||||
<div className="pb-1 text-center text-sm text-gray-400 mt-5">Tipi version {Package.version}</div>
|
||||
{!isLatest && (
|
||||
<Badge className="self-center mt-1" colorScheme="green">
|
||||
New version available
|
||||
</Badge>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
38
packages/dashboard/src/components/Layout/UpdateBanner.tsx
Normal file
38
packages/dashboard/src/components/Layout/UpdateBanner.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { Alert, AlertDescription, AlertIcon, AlertTitle, Box, CloseButton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { useVersionQuery } from '../../generated/graphql';
|
||||
|
||||
const UpdateBanner = () => {
|
||||
const { data, loading } = useVersionQuery();
|
||||
|
||||
const isLatest = data?.version.latest === data?.version.current;
|
||||
|
||||
if (isLatest || (loading && !data?.version)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(data);
|
||||
|
||||
const onClose = () => {};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Alert status="info" className="flex mb-3">
|
||||
<AlertIcon />
|
||||
<Box className="flex-1">
|
||||
<AlertTitle>New version available!</AlertTitle>
|
||||
<AlertDescription>
|
||||
There is a new version of Tipi available ({data?.version.latest}). Visit{' '}
|
||||
<a className="text-blue-600" target="_blank" rel="noreferrer" href={'https://github.com/meienberger/runtipi/releases/latest'}>
|
||||
Github
|
||||
</a>{' '}
|
||||
for update instructions.
|
||||
</AlertDescription>
|
||||
</Box>
|
||||
<CloseButton alignSelf="flex-start" position="relative" right={-1} top={-1} onClick={onClose} />
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateBanner;
|
|
@ -1,45 +1,11 @@
|
|||
import create from 'zustand';
|
||||
import api from '../core/api';
|
||||
|
||||
type Store = {
|
||||
cpuLoad: number;
|
||||
internalIp: string;
|
||||
disk: { total: number; used: number; available: number };
|
||||
memory: { total: number; used: number; available: number };
|
||||
fetchDiskSpace: () => void;
|
||||
fetchCpuLoad: () => void;
|
||||
fetchMemoryLoad: () => void;
|
||||
setInternalIp: (internalIp: string) => void;
|
||||
};
|
||||
|
||||
export const useSytemStore = create<Store>((set) => ({
|
||||
cpuLoad: 0,
|
||||
internalIp: '',
|
||||
setInternalIp: (internalIp: string) => set((state) => ({ ...state, internalIp })),
|
||||
memory: { total: 0, used: 0, available: 0 },
|
||||
disk: { total: 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 });
|
||||
},
|
||||
}));
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "system-api",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"description": "",
|
||||
"exports": "./dist/server.js",
|
||||
"type": "module",
|
||||
|
|
|
@ -78,6 +78,20 @@ describe('Install app', () => {
|
|||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('Should delete app if install script fails', async () => {
|
||||
const spy = jest.spyOn(childProcess, 'execFile');
|
||||
spy.mockImplementation(() => {
|
||||
throw new Error('Test error');
|
||||
});
|
||||
|
||||
await expect(AppsService.installApp(app1.id, { TEST_FIELD: 'test' })).rejects.toThrow('Test error');
|
||||
|
||||
const app = await App.findOne({ where: { id: app1.id } });
|
||||
|
||||
expect(app).toBeNull();
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('Should throw if required form fields are missing', async () => {
|
||||
await expect(AppsService.installApp(app1.id, {})).rejects.toThrowError('Variable TEST_FIELD is required');
|
||||
});
|
||||
|
|
|
@ -46,7 +46,7 @@ const startApp = async (appName: string): Promise<App> => {
|
|||
await App.update({ id: appName }, { status: AppStatusEnum.RUNNING });
|
||||
} catch (e) {
|
||||
await App.update({ id: appName }, { status: AppStatusEnum.STOPPED });
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
app = (await App.findOne({ where: { id: appName } })) as App;
|
||||
|
@ -75,7 +75,12 @@ const installApp = async (id: string, form: Record<string, string>): Promise<App
|
|||
app = await App.create({ id, status: AppStatusEnum.INSTALLING, config: form }).save();
|
||||
|
||||
// Run script
|
||||
await runAppScript(['install', id]);
|
||||
try {
|
||||
await runAppScript(['install', id]);
|
||||
} catch (e) {
|
||||
await App.delete({ id });
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
await App.update({ id }, { status: AppStatusEnum.RUNNING });
|
||||
|
@ -125,9 +130,15 @@ const stopApp = async (id: string): Promise<App> => {
|
|||
|
||||
// Run script
|
||||
await App.update({ id }, { status: AppStatusEnum.STOPPING });
|
||||
await runAppScript(['stop', id]);
|
||||
|
||||
await App.update({ id }, { status: AppStatusEnum.STOPPED });
|
||||
try {
|
||||
await runAppScript(['stop', id]);
|
||||
await App.update({ id }, { status: AppStatusEnum.STOPPED });
|
||||
} catch (e) {
|
||||
await App.update({ id }, { status: AppStatusEnum.RUNNING });
|
||||
throw e;
|
||||
}
|
||||
|
||||
app = (await App.findOne({ where: { id } })) as App;
|
||||
|
||||
return app;
|
||||
|
@ -148,7 +159,8 @@ const uninstallApp = async (id: string): Promise<App> => {
|
|||
try {
|
||||
await runAppScript(['uninstall', id]);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
await App.update({ id }, { status: AppStatusEnum.STOPPED });
|
||||
throw e;
|
||||
}
|
||||
|
||||
await App.delete({ id });
|
||||
|
|
|
@ -23,22 +23,17 @@ cd "$ROOT_FOLDER"
|
|||
export DOCKER_CLIENT_TIMEOUT=240
|
||||
export COMPOSE_HTTP_TIMEOUT=240
|
||||
|
||||
function get_json_field() {
|
||||
local json_file="$1"
|
||||
local field="$2"
|
||||
# Get all app names from the apps folder
|
||||
apps_folder="${ROOT_FOLDER}/apps"
|
||||
apps_names=($(ls -d ${apps_folder}/*/ | xargs -n 1 basename | sed 's/\///g'))
|
||||
|
||||
echo $(jq -r ".${field}" "${json_file}")
|
||||
}
|
||||
|
||||
str=$(get_json_field ${STATE_FOLDER}/apps.json installed)
|
||||
apps_to_start=($str)
|
||||
|
||||
# If apps_to_start is not empty, then we're stopping all apps
|
||||
if [[ ${#apps_to_start[@]} -gt 0 ]]; then
|
||||
for app in "${apps_to_start[@]}"; do
|
||||
"${ROOT_FOLDER}/scripts/app.sh" stop $app
|
||||
done
|
||||
fi
|
||||
for app_name in "${apps_names[@]}"; do
|
||||
# if folder ${ROOT_FOLDER}/app-data/app_name exists, then stop app
|
||||
if [[ -d "${ROOT_FOLDER}/app-data/${app_name}" ]]; then
|
||||
echo "Stopping ${app_name}"
|
||||
"${ROOT_FOLDER}/scripts/app.sh" stop $app_name
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Stopping Docker services..."
|
||||
echo
|
||||
|
|
Loading…
Add table
Reference in a new issue