Browse Source

Merge pull request #140 from meienberger/release/0.4.1

Release/0.4.1
Nicolas Meienberger 2 years ago
parent
commit
d4a2b15c48

+ 1 - 1
docker-compose.yml

@@ -1,4 +1,4 @@
-version: "3.7"
+version: "3.9"
 
 services:
   reverse-proxy:

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "runtipi",
-  "version": "0.4.0",
+  "version": "0.4.1",
   "description": "A homeserver for everyone",
   "scripts": {
     "test": "jest",

+ 1 - 1
packages/dashboard/package.json

@@ -1,6 +1,6 @@
 {
   "name": "dashboard",
-  "version": "0.4.0",
+  "version": "0.4.1",
   "private": true,
   "scripts": {
     "test": "jest --colors",

+ 3 - 1
packages/dashboard/src/components/Layout/Layout.tsx

@@ -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>

+ 10 - 2
packages/dashboard/src/components/Layout/SideMenu.tsx

@@ -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 - 0
packages/dashboard/src/components/Layout/UpdateBanner.tsx

@@ -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;

+ 0 - 34
packages/dashboard/src/state/systemStore.ts

@@ -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 - 1
packages/system-api/package.json

@@ -1,6 +1,6 @@
 {
   "name": "system-api",
-  "version": "0.4.0",
+  "version": "0.4.1",
   "description": "",
   "exports": "./dist/server.js",
   "type": "module",

+ 14 - 0
packages/system-api/src/modules/apps/__tests__/apps.service.test.ts

@@ -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');
   });

+ 17 - 5
packages/system-api/src/modules/apps/apps.service.ts

@@ -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 });

+ 11 - 16
scripts/stop.sh

@@ -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"
-
-  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
+# 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'))
+
+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