Release/1.0.0 (#316)
* fix: create default media folder structure on install * feat: add link to open exposed app to domain * [ImgBot] Optimize images *Total -- 2,048.42kb -> 1,263.43kb (38.32%) /screenshots/darkmode.png -- 998.43kb -> 609.77kb (38.93%) /screenshots/appstore.png -- 1,006.73kb -> 620.12kb (38.4%) /packages/dashboard/public/error.png -- 42.38kb -> 32.70kb (22.84%) /packages/dashboard/public/empty.svg -- 0.87kb -> 0.85kb (2.35%) Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com> * chore: bump version 0.8.1 * refactor: move all dashboard's files into a client folder * feat: setup trpc and create system router * test: split jest config for client and server * refactor: replace grapqhl queries with trpc in the frontend * refactor: remove now un-used system queries/mutations/resolvers from both client and server * chore: bump dependencies * feat: setup prisma and configure it for tests and development * feat: create trpc router for auth service * refactor: migrate client auth queries to trpc procedures * refactor: cleanup now un-used graphql resolvers and services * feat: create sql migrations by replicating typeorm ones in an idempotent manner * feat: create server-preload script to run migrations upon server start * chore: remove legacy migrations steps * feat: add redis_host as an env variable * refactor: remove prisma from context and use client directly in service * feat: create trpc router & service for apps * refactor: migrate client app queries/mutations to trpc * refactor: removal and replace usage of old graphql generated types * refactor: move from node --require to custom next server * test: fix tests and bump various dependencies * chore: cleanup system-api from now un-used files * refactor(dashboard): remove code related to apollo * refactor: serve static files through next's server instead of system-api * refactor(server): move auth and system services to class * refactor(client): remove layoutv2 abstraction * fix: return correct update info * chore: remove legacy system-api folder * refactor: remove system-api from docker files * feat: create scheduler to run cron jobs and setup periodic repo update * fix: failing build caused by remark-mdx * refactor: move migrations to server folder * feat: compile server using esbuild * refactor: ts issue mis-used file from client in server * ci: make pipeline pass by cd into dashboard before each step (temp) * chore: drop armv7 support * refactor: move dashboard files in root folder * feat(db): create migration to add operator field on user * feat(user): create routes and services for password reset * feat(auth): add reset password page, container & form * refactor(dashboard): change layout and page of auth to be url based instead of state based * feat(script): add reset-password script * fix(dashboard): only check status if restart or update has been requested * test: increase coverage for get-server-auth-session * fix(start.sh): prompt for network interface only if there is not an internal ip set * feat(script): support user docker-compose.yml and app.env * chore: bump version * fix: add missing postgres variables to start script * fix: check for 32 bits before installing/starting * fix: create default media folder structure on install * Updated demo instance link Changed demo.runtipi.com to https://demo.runtipi.com * feat: adding config for codespaces * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] --------- Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com> Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com> Co-authored-by: Freddie Sackur <github@dustyfox.uk> Co-authored-by: Kieran Klukas <92754843+kcoderhtml@users.noreply.github.com> Co-authored-by: alwerner <alexander.werner@bonprix.net> Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
This commit is contained in:
parent
91c3162e3e
commit
3925cfa7bb
441 changed files with 7919 additions and 15209 deletions
|
@ -10,3 +10,18 @@ dist/
|
|||
docker-compose*.yml
|
||||
Dockerfile*
|
||||
.dockerignore
|
||||
|
||||
# Tipi folder
|
||||
logs/
|
||||
tests/
|
||||
state/
|
||||
templates/
|
||||
scripts/
|
||||
screenshots/
|
||||
repos/
|
||||
media/
|
||||
data/
|
||||
apps/
|
||||
app-data/
|
||||
.github/
|
||||
__mocks__/
|
18
.env.example
Normal file
18
.env.example
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Only edit this file if you know what you are doing!
|
||||
# It will be overwritten on update.
|
||||
|
||||
APPS_REPO_ID=7a92c8307e0a8074763c80be1fcfa4f87da6641daea9211aea6743b0116aba3b
|
||||
APPS_REPO_URL=https://github.com/meienberger/runtipi-appstore
|
||||
TZ=UTC
|
||||
INTERNAL_IP=localhost
|
||||
DNS_IP=9.9.9.9
|
||||
ARCHITECTURE=arm64
|
||||
TIPI_VERSION=0.8.0
|
||||
JWT_SECRET=secret
|
||||
ROOT_FOLDER_HOST=/Users/nicolas/Projects/runtipi
|
||||
NGINX_PORT=3000
|
||||
NGINX_PORT_SSL=443
|
||||
POSTGRES_PASSWORD=postgres
|
||||
DOMAIN=tipi.localhost
|
||||
STORAGE_PATH=/Users/nicolas/Projects/runtipi
|
||||
REDIS_HOST=tipi-redis
|
6
.env.test
Normal file
6
.env.test
Normal file
|
@ -0,0 +1,6 @@
|
|||
POSTGRES_HOST=localhost
|
||||
POSTGRES_DBNAME=postgres
|
||||
POSTGRES_USERNAME=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_PORT=5433
|
||||
APPS_REPO_ID=repo-id
|
|
@ -1,5 +1,5 @@
|
|||
module.exports = {
|
||||
plugins: ['@typescript-eslint', 'import', 'react', 'jest'],
|
||||
plugins: ['@typescript-eslint', 'import', 'react', 'jest', 'jsdoc'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'next/core-web-vitals',
|
||||
|
@ -10,6 +10,7 @@ module.exports = {
|
|||
'plugin:import/typescript',
|
||||
'prettier',
|
||||
'plugin:react/recommended',
|
||||
'plugin:jsdoc/recommended',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
|
@ -28,10 +29,13 @@ module.exports = {
|
|||
'react/jsx-props-no-spreading': 0,
|
||||
'react/no-unused-prop-types': 0,
|
||||
'react/button-has-type': 0,
|
||||
'import/no-extraneous-dependencies': ['error', { devDependencies: ['**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}', '**/mocks/**', 'tests/**'] }],
|
||||
'import/no-extraneous-dependencies': ['error', { devDependencies: ['esbuild.js', '**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}', '**/*.factory.{ts,tsx}', '**/mocks/**', 'tests/**'] }],
|
||||
'no-underscore-dangle': 0,
|
||||
'arrow-body-style': 0,
|
||||
},
|
||||
globals: {
|
||||
JSX: true,
|
||||
NodeJS: true,
|
||||
},
|
||||
env: {
|
||||
'jest/globals': true,
|
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
|
@ -7,7 +7,11 @@ env:
|
|||
JWT_SECRET: "secret"
|
||||
ROOT_FOLDER_HOST: /tipi
|
||||
APPS_REPO_ID: repo-id
|
||||
INTERNAL_IP: 192.168.1.10
|
||||
INTERNAL_IP: localhost
|
||||
REDIS_HOST: redis
|
||||
APPS_REPO_URL: https://repo.github.com/
|
||||
DOMAIN: localhost
|
||||
TIPI_VERSION: 0.0.1
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
|
@ -57,14 +61,14 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm -r build
|
||||
- name: Build client
|
||||
run: pnpm build:next
|
||||
|
||||
- name: Run linter
|
||||
run: pnpm -r lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm -r test
|
||||
run: pnpm test
|
||||
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
|
|
2
.github/workflows/release-candidate.yml
vendored
2
.github/workflows/release-candidate.yml
vendored
|
@ -36,7 +36,7 @@ jobs:
|
|||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: meienberger/runtipi:rc-${{ steps.meta.outputs.TAG }}
|
||||
cache-from: type=registry,ref=meienberger/runtipi:buildcache
|
||||
|
|
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
|
@ -1,4 +1,3 @@
|
|||
|
||||
name: Publish release
|
||||
on:
|
||||
push:
|
||||
|
@ -36,7 +35,7 @@ jobs:
|
|||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: meienberger/runtipi:latest,meienberger/runtipi:${{ steps.meta.outputs.TAG }}
|
||||
cache-from: type=registry,ref=meienberger/runtipi:buildcache
|
||||
|
|
70
.gitignore
vendored
70
.gitignore
vendored
|
@ -4,38 +4,58 @@
|
|||
.DS_Store
|
||||
.vscode
|
||||
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
server-preload.js
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
logs
|
||||
.pnpm-debug.log
|
||||
.env*
|
||||
!.env.example
|
||||
!.env.test
|
||||
github.secrets
|
||||
node_modules/
|
||||
app-data/*
|
||||
data/postgres
|
||||
data/redis
|
||||
!app-data/.gitkeep
|
||||
repos/*
|
||||
!repos/.gitkeep
|
||||
apps/*
|
||||
!apps/.gitkeep
|
||||
/app-data/
|
||||
/data/
|
||||
/repos/
|
||||
/apps/
|
||||
traefik/shared
|
||||
|
||||
# media folder
|
||||
media
|
||||
!media/.gitkeep
|
||||
!media/data/.gitkeep
|
||||
!media/data/books/.gitkeep
|
||||
!media/data/books/ebooks/.gitkeep
|
||||
!media/data/books/spoken/.gitkeep
|
||||
!media/data/movies/.gitkeep
|
||||
!media/data/music/.gitkeep
|
||||
!media/data/podcasts/.gitkeep
|
||||
!media/data/tv/.gitkeep
|
||||
!media/data/images/.gitkeep
|
||||
!media/torrents/.gitkeep
|
||||
!media/torrents/complete/.gitkeep
|
||||
!media/torrents/incomplete/.gitkeep
|
||||
!media/torrents/watch/.gitkeep
|
||||
|
||||
# state folder
|
||||
state/*
|
||||
!state/.gitkeep
|
||||
/state/
|
||||
|
|
70
Dockerfile
70
Dockerfile
|
@ -1,43 +1,49 @@
|
|||
FROM node:18-alpine3.16 AS builder
|
||||
ARG NODE_VERSION="18.12.1"
|
||||
ARG ALPINE_VERSION="3.16"
|
||||
|
||||
# Required for argon2
|
||||
RUN apk --no-cache add g++
|
||||
RUN apk --no-cache add make
|
||||
RUN apk --no-cache add python3
|
||||
FROM node:${NODE_VERSION}-buster-slim AS node_base
|
||||
RUN apt update
|
||||
RUN apt install -y openssl
|
||||
|
||||
FROM node_base AS builder_base
|
||||
|
||||
# Required for sharp
|
||||
RUN apk --no-cache add vips-dev=8.12.2-r5
|
||||
RUN npm install node-gyp -g
|
||||
RUN npm install pnpm -g
|
||||
|
||||
WORKDIR /api
|
||||
COPY ./packages/system-api/package.json /api/package.json
|
||||
RUN npm i
|
||||
# ---
|
||||
WORKDIR /dashboard
|
||||
COPY ./packages/dashboard/package.json /dashboard/package.json
|
||||
RUN npm i
|
||||
# BUILDER
|
||||
FROM builder_base AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./pnpm-lock.yaml ./
|
||||
RUN pnpm fetch
|
||||
|
||||
COPY ./package*.json ./
|
||||
COPY ./prisma/schema.prisma ./prisma/
|
||||
|
||||
RUN pnpm install -r --prefer-offline
|
||||
COPY ./src ./src
|
||||
COPY ./esbuild.js ./esbuild.js
|
||||
COPY ./tsconfig.json ./tsconfig.json
|
||||
COPY ./next.config.mjs ./next.config.mjs
|
||||
COPY ./public ./public
|
||||
|
||||
WORKDIR /api
|
||||
COPY ./packages/system-api /api
|
||||
RUN npm run build
|
||||
# ---
|
||||
WORKDIR /dashboard
|
||||
COPY ./packages/dashboard /dashboard
|
||||
RUN npm run build
|
||||
|
||||
FROM node:18-alpine3.16 as app
|
||||
# APP
|
||||
FROM node_base AS app
|
||||
|
||||
WORKDIR /
|
||||
# USER node
|
||||
|
||||
WORKDIR /api
|
||||
COPY ./packages/system-api/package.json /api/
|
||||
COPY --from=builder /api/dist /api/dist
|
||||
WORKDIR /app
|
||||
|
||||
WORKDIR /dashboard
|
||||
COPY --from=builder /dashboard/next.config.js ./
|
||||
COPY --from=builder /dashboard/public ./public
|
||||
COPY --from=builder /dashboard/package.json ./package.json
|
||||
COPY --from=builder --chown=node:node /dashboard/.next/standalone ./
|
||||
COPY --from=builder --chown=node:node /dashboard/.next/static ./.next/static
|
||||
COPY --from=builder /app/dist ./
|
||||
COPY --from=builder /app/next.config.mjs ./
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
COPY --from=builder --chown=node:node /app/.next/standalone ./
|
||||
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
|
||||
|
||||
WORKDIR /
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
|
|
|
@ -1,19 +1,28 @@
|
|||
FROM node:18-alpine3.16
|
||||
ARG NODE_VERSION="18.12.1"
|
||||
ARG ALPINE_VERSION="3.16"
|
||||
|
||||
WORKDIR /
|
||||
FROM node:${NODE_VERSION}-buster-slim
|
||||
|
||||
RUN apk --no-cache add g++ make
|
||||
RUN apt update
|
||||
RUN apt install -y openssl
|
||||
|
||||
RUN npm install pnpm -g
|
||||
RUN npm install node-gyp -g
|
||||
|
||||
WORKDIR /api
|
||||
COPY ./packages/system-api/package*.json /api/
|
||||
RUN npm install
|
||||
WORKDIR /app
|
||||
|
||||
WORKDIR /dashboard
|
||||
COPY ./packages/dashboard/package*.json /dashboard/
|
||||
RUN npm install
|
||||
COPY ./pnpm-lock.yaml ./
|
||||
RUN pnpm fetch
|
||||
|
||||
COPY ./packages/system-api /api
|
||||
COPY ./packages/dashboard /dashboard
|
||||
COPY ./package*.json ./
|
||||
COPY ./prisma/schema.prisma ./prisma/
|
||||
|
||||
WORKDIR /
|
||||
RUN pnpm install -r --prefer-offline
|
||||
|
||||
COPY ./src ./src
|
||||
COPY ./esbuild.js ./esbuild.js
|
||||
COPY ./tsconfig.json ./tsconfig.json
|
||||
COPY ./next.config.mjs ./next.config.mjs
|
||||
COPY ./public ./public
|
||||
|
||||
CMD ["npm", "run", "dev"]
|
||||
|
|
118
__mocks__/fs-extra.ts
Normal file
118
__mocks__/fs-extra.ts
Normal file
|
@ -0,0 +1,118 @@
|
|||
import path from 'path';
|
||||
|
||||
class FsMock {
|
||||
private static instance: FsMock;
|
||||
|
||||
private mockFiles = Object.create(null);
|
||||
|
||||
// private constructor() {}
|
||||
|
||||
static getInstance(): FsMock {
|
||||
if (!FsMock.instance) {
|
||||
FsMock.instance = new FsMock();
|
||||
}
|
||||
return FsMock.instance;
|
||||
}
|
||||
|
||||
__createMockFiles = (newMockFiles: Record<string, string>) => {
|
||||
this.mockFiles = Object.create(null);
|
||||
|
||||
// Create folder tree
|
||||
Object.keys(newMockFiles).forEach((file) => {
|
||||
const dir = path.dirname(file);
|
||||
|
||||
if (!this.mockFiles[dir]) {
|
||||
this.mockFiles[dir] = [];
|
||||
}
|
||||
|
||||
this.mockFiles[dir].push(path.basename(file));
|
||||
this.mockFiles[file] = newMockFiles[file];
|
||||
});
|
||||
};
|
||||
|
||||
__resetAllMocks = () => {
|
||||
this.mockFiles = Object.create(null);
|
||||
};
|
||||
|
||||
readFileSync = (p: string) => this.mockFiles[p];
|
||||
|
||||
existsSync = (p: string) => this.mockFiles[p] !== undefined;
|
||||
|
||||
writeFileSync = (p: string, data: string | string[]) => {
|
||||
this.mockFiles[p] = data;
|
||||
};
|
||||
|
||||
mkdirSync = (p: string) => {
|
||||
this.mockFiles[p] = Object.create(null);
|
||||
};
|
||||
|
||||
rmSync = (p: string) => {
|
||||
if (this.mockFiles[p] instanceof Array) {
|
||||
this.mockFiles[p].forEach((file: string) => {
|
||||
delete this.mockFiles[path.join(p, file)];
|
||||
});
|
||||
}
|
||||
|
||||
delete this.mockFiles[p];
|
||||
};
|
||||
|
||||
readdirSync = (p: string) => {
|
||||
const files: string[] = [];
|
||||
|
||||
const depth = p.split('/').length;
|
||||
|
||||
Object.keys(this.mockFiles).forEach((file) => {
|
||||
if (file.startsWith(p)) {
|
||||
const fileDepth = file.split('/').length;
|
||||
|
||||
if (fileDepth === depth + 1) {
|
||||
files.push(file.split('/').pop() || '');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return files;
|
||||
};
|
||||
|
||||
copyFileSync = (source: string, destination: string) => {
|
||||
this.mockFiles[destination] = this.mockFiles[source];
|
||||
};
|
||||
|
||||
copySync = (source: string, destination: string) => {
|
||||
this.mockFiles[destination] = this.mockFiles[source];
|
||||
|
||||
if (this.mockFiles[source] instanceof Array) {
|
||||
this.mockFiles[source].forEach((file: string) => {
|
||||
this.mockFiles[`${destination}/${file}`] = this.mockFiles[`${source}/${file}`];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
createFileSync = (p: string) => {
|
||||
this.mockFiles[p] = '';
|
||||
};
|
||||
|
||||
unlinkSync = (p: string) => {
|
||||
if (this.mockFiles[p] instanceof Array) {
|
||||
this.mockFiles[p].forEach((file: string) => {
|
||||
delete this.mockFiles[path.join(p, file)];
|
||||
});
|
||||
}
|
||||
delete this.mockFiles[p];
|
||||
};
|
||||
|
||||
getMockFiles = () => this.mockFiles;
|
||||
|
||||
promises = {
|
||||
unlink: (p: string) => {
|
||||
if (this.mockFiles[p] instanceof Array) {
|
||||
this.mockFiles[p].forEach((file: string) => {
|
||||
delete this.mockFiles[path.join(p, file)];
|
||||
});
|
||||
}
|
||||
delete this.mockFiles[p];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default FsMock.getInstance();
|
16
__mocks__/redis.ts
Normal file
16
__mocks__/redis.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
export const createClient = jest.fn(() => {
|
||||
const values = new Map();
|
||||
const expirations = new Map();
|
||||
return {
|
||||
isOpen: true,
|
||||
connect: jest.fn(),
|
||||
set: (key: string, value: string, exp: number) => {
|
||||
values.set(key, value);
|
||||
expirations.set(key, exp);
|
||||
},
|
||||
get: (key: string) => values.get(key),
|
||||
quit: jest.fn(),
|
||||
del: (key: string) => values.delete(key),
|
||||
ttl: (key: string) => expirations.get(key),
|
||||
};
|
||||
});
|
|
@ -1,3 +0,0 @@
|
|||
module.exports = {
|
||||
extends: ["@commitlint/config-conventional"],
|
||||
};
|
|
@ -50,65 +50,40 @@ services:
|
|||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
api:
|
||||
dashboard:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
command: /bin/sh -c "cd /api && npm run build && npm run dev"
|
||||
container_name: dashboard
|
||||
depends_on:
|
||||
tipi-db:
|
||||
condition: service_healthy
|
||||
container_name: api
|
||||
volumes:
|
||||
- ${PWD}/repos:/runtipi/repos:ro
|
||||
- ${PWD}/apps:/runtipi/apps
|
||||
- ${PWD}/state:/runtipi/state
|
||||
- ${PWD}/packages/system-api/src:/api/src
|
||||
- ${STORAGE_PATH}:/app/storage
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${PWD}/.env.dev:/runtipi/.env
|
||||
# - /api/node_modules
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
INTERNAL_IP: ${INTERNAL_IP}
|
||||
TIPI_VERSION: ${TIPI_VERSION}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
NGINX_PORT: ${NGINX_PORT}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_USERNAME: tipi
|
||||
POSTGRES_DBNAME: tipi
|
||||
POSTGRES_HOST: tipi-db
|
||||
POSTGRES_USERNAME: ${POSTGRES_USERNAME}
|
||||
POSTGRES_DBNAME: ${POSTGRES_DBNAME}
|
||||
POSTGRES_HOST: ${POSTGRES_HOST}
|
||||
APPS_REPO_ID: ${APPS_REPO_ID}
|
||||
APPS_REPO_URL: ${APPS_REPO_URL}
|
||||
DOMAIN: ${DOMAIN}
|
||||
ARCHITECTURE: ${ARCHITECTURE}
|
||||
networks:
|
||||
- tipi_main_network
|
||||
labels:
|
||||
traefik.enable: true
|
||||
# Web
|
||||
traefik.http.routers.api.rule: PathPrefix(`/api`)
|
||||
traefik.http.routers.api.service: api
|
||||
traefik.http.routers.api.entrypoints: web
|
||||
traefik.http.routers.api.middlewares: api-stripprefix
|
||||
traefik.http.services.api.loadbalancer.server.port: 3001
|
||||
# Middlewares
|
||||
traefik.http.middlewares.api-stripprefix.stripprefix.prefixes: /api
|
||||
|
||||
dashboard:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
command: /bin/sh -c "cd /dashboard && npm run dev"
|
||||
container_name: dashboard
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_started
|
||||
REDIS_HOST: ${REDIS_HOST}
|
||||
networks:
|
||||
- tipi_main_network
|
||||
volumes:
|
||||
- ${PWD}/packages/dashboard/src:/dashboard/src
|
||||
- ${PWD}/src:/app/src
|
||||
# - /dashboard/node_modules
|
||||
# - /dashboard/.next
|
||||
- ${PWD}/state:/runtipi/state
|
||||
- ${PWD}/repos:/runtipi/repos:ro
|
||||
- ${PWD}/apps:/runtipi/apps
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${STORAGE_PATH}:/app/storage
|
||||
labels:
|
||||
traefik.enable: true
|
||||
# Web
|
||||
|
|
|
@ -45,65 +45,35 @@ services:
|
|||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
api:
|
||||
dashboard:
|
||||
image: meienberger/runtipi:rc-${TIPI_VERSION}
|
||||
command: /bin/sh -c "cd /api && npm run start"
|
||||
container_name: api
|
||||
container_name: dashboard
|
||||
networks:
|
||||
- tipi_main_network
|
||||
depends_on:
|
||||
tipi-db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ${PWD}/repos:/runtipi/repos:ro
|
||||
- ${PWD}/apps:/runtipi/apps
|
||||
- ${PWD}/state:/runtipi/state
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${STORAGE_PATH}:/app/storage
|
||||
- ${PWD}/.env:/runtipi/.env:ro
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
INTERNAL_IP: ${INTERNAL_IP}
|
||||
TIPI_VERSION: ${TIPI_VERSION}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
NGINX_PORT: ${NGINX_PORT}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_USERNAME: tipi
|
||||
POSTGRES_DBNAME: tipi
|
||||
POSTGRES_HOST: tipi-db
|
||||
NODE_ENV: production
|
||||
POSTGRES_USERNAME: ${POSTGRES_USERNAME}
|
||||
POSTGRES_DBNAME: ${POSTGRES_DBNAME}
|
||||
POSTGRES_HOST: ${POSTGRES_HOST}
|
||||
APPS_REPO_ID: ${APPS_REPO_ID}
|
||||
APPS_REPO_URL: ${APPS_REPO_URL}
|
||||
DOMAIN: ${DOMAIN}
|
||||
ARCHITECTURE: ${ARCHITECTURE}
|
||||
networks:
|
||||
- tipi_main_network
|
||||
labels:
|
||||
traefik.enable: true
|
||||
# Web
|
||||
traefik.http.routers.api.rule: PathPrefix(`/api`)
|
||||
traefik.http.routers.api.service: api
|
||||
traefik.http.routers.api.entrypoints: web
|
||||
traefik.http.routers.api.middlewares: api-stripprefix
|
||||
traefik.http.services.api.loadbalancer.server.port: 3001
|
||||
# Websecure
|
||||
traefik.http.routers.api-secure.rule: (Host(`${DOMAIN}`) && PathPrefix(`/api`))
|
||||
traefik.http.routers.api-secure.entrypoints: websecure
|
||||
traefik.http.routers.api-secure.service: api-secure
|
||||
traefik.http.routers.api-secure.tls.certresolver: myresolver
|
||||
traefik.http.routers.api-secure.middlewares: api-stripprefix
|
||||
traefik.http.services.api-secure.loadbalancer.server.port: 3001
|
||||
# Middlewares
|
||||
traefik.http.middlewares.api-stripprefix.stripprefix.prefixes: /api
|
||||
|
||||
dashboard:
|
||||
image: meienberger/runtipi:rc-${TIPI_VERSION}
|
||||
command: /bin/sh -c "cd /dashboard && node server.js"
|
||||
container_name: dashboard
|
||||
networks:
|
||||
- tipi_main_network
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_started
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
REDIS_HOST: ${REDIS_HOST}
|
||||
volumes:
|
||||
- ${PWD}/state:/runtipi/state
|
||||
- ${PWD}/repos:/runtipi/repos:ro
|
||||
- ${PWD}/apps:/runtipi/apps
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${STORAGE_PATH}:/app/storage
|
||||
labels:
|
||||
traefik.enable: true
|
||||
# Web
|
||||
|
|
|
@ -44,100 +44,51 @@ services:
|
|||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
command: /bin/sh -c "cd /api && npm run start"
|
||||
restart: unless-stopped
|
||||
container_name: api
|
||||
depends_on:
|
||||
tipi-db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ${PWD}/repos:/runtipi/repos:ro
|
||||
- ${PWD}/apps:/runtipi/apps
|
||||
- ${PWD}/state:/runtipi/state
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${STORAGE_PATH}:/app/storage
|
||||
- ${PWD}/.env:/runtipi/.env:ro
|
||||
environment:
|
||||
INTERNAL_IP: ${INTERNAL_IP}
|
||||
TIPI_VERSION: ${TIPI_VERSION}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
NGINX_PORT: ${NGINX_PORT}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_USERNAME: tipi
|
||||
POSTGRES_DBNAME: tipi
|
||||
POSTGRES_HOST: tipi-db
|
||||
NODE_ENV: production
|
||||
APPS_REPO_ID: ${APPS_REPO_ID}
|
||||
APPS_REPO_URL: ${APPS_REPO_URL}
|
||||
DOMAIN: ${DOMAIN}
|
||||
ARCHITECTURE: ${ARCHITECTURE}
|
||||
networks:
|
||||
- tipi_main_network
|
||||
labels:
|
||||
traefik.enable: true
|
||||
# Web
|
||||
traefik.http.routers.api.rule: PathPrefix(`/api`)
|
||||
traefik.http.routers.api.service: api
|
||||
traefik.http.routers.api.entrypoints: web
|
||||
traefik.http.routers.api.middlewares: api-stripprefix
|
||||
traefik.http.services.api.loadbalancer.server.port: 3001
|
||||
# Websecure
|
||||
traefik.http.routers.api-secure.rule: (Host(`${DOMAIN}`) && PathPrefix(`/api`))
|
||||
traefik.http.routers.api-secure.entrypoints: websecure
|
||||
traefik.http.routers.api-secure.service: api-secure
|
||||
traefik.http.routers.api-secure.tls.certresolver: myresolver
|
||||
traefik.http.routers.api-secure.middlewares: api-stripprefix
|
||||
traefik.http.services.api-secure.loadbalancer.server.port: 3001
|
||||
# Middlewares
|
||||
traefik.http.middlewares.api-stripprefix.stripprefix.prefixes: /api
|
||||
|
||||
dashboard:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
command: /bin/sh -c "cd /dashboard && node server.js"
|
||||
restart: unless-stopped
|
||||
container_name: dashboard
|
||||
networks:
|
||||
- tipi_main_network
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_started
|
||||
tipi-db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
INTERNAL_IP: ${INTERNAL_IP}
|
||||
TIPI_VERSION: ${TIPI_VERSION}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
NGINX_PORT: ${NGINX_PORT}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_USERNAME: ${POSTGRES_USERNAME}
|
||||
POSTGRES_DBNAME: ${POSTGRES_DBNAME}
|
||||
POSTGRES_HOST: ${POSTGRES_HOST}
|
||||
APPS_REPO_ID: ${APPS_REPO_ID}
|
||||
APPS_REPO_URL: ${APPS_REPO_URL}
|
||||
DOMAIN: ${DOMAIN}
|
||||
ARCHITECTURE: ${ARCHITECTURE}
|
||||
REDIS_HOST: ${REDIS_HOST}
|
||||
volumes:
|
||||
- ${PWD}/state:/runtipi/state
|
||||
- ${PWD}/repos:/runtipi/repos:ro
|
||||
- ${PWD}/apps:/runtipi/apps
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${STORAGE_PATH}:/app/storage
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.dashboard-redirect.rule: PathPrefix("/")
|
||||
traefik.http.routers.dashboard-redirect.entrypoints: web
|
||||
traefik.http.routers.dashboard-redirect.middlewares: redirect-middleware
|
||||
traefik.http.routers.dashboard-redirect.service: dashboard
|
||||
traefik.http.services.dashboard-redirect.loadbalancer.server.port: 3000
|
||||
|
||||
traefik.http.routers.dashboard-redirect-secure.rule: Host(`${DOMAIN}`) && PathPrefix(`/`)
|
||||
traefik.http.routers.dashboard-redirect-secure.entrypoints: websecure
|
||||
traefik.http.routers.dashboard-redirect-secure.middlewares: redirect-middleware
|
||||
traefik.http.routers.dashboard-redirect-secure.service: dashboard
|
||||
traefik.http.routers.dashboard-redirect-secure.tls.certresolver: myresolver
|
||||
traefik.http.services.dashboard-redirect-secure.loadbalancer.server.port: 3000
|
||||
|
||||
# Web
|
||||
traefik.http.routers.dashboard.rule: PathPrefix("/dashboard")
|
||||
traefik.http.routers.dashboard.rule: PathPrefix("/")
|
||||
traefik.http.routers.dashboard.service: dashboard
|
||||
traefik.http.routers.dashboard.entrypoints: web
|
||||
traefik.http.services.dashboard.loadbalancer.server.port: 3000
|
||||
# Websecure
|
||||
traefik.http.routers.dashboard-secure.rule: Host(`${DOMAIN}`) && PathPrefix(`/dashboard`)
|
||||
traefik.http.routers.dashboard-secure.rule: Host(`${DOMAIN}`) && PathPrefix(`/`)
|
||||
traefik.http.routers.dashboard-secure.service: dashboard-secure
|
||||
traefik.http.routers.dashboard-secure.entrypoints: websecure
|
||||
traefik.http.routers.dashboard-secure.tls.certresolver: myresolver
|
||||
traefik.http.services.dashboard-secure.loadbalancer.server.port: 3000
|
||||
# Middlewares
|
||||
traefik.http.middlewares.redirect-middleware.redirectregex.regex: .*
|
||||
traefik.http.middlewares.redirect-middleware.redirectregex.replacement: /dashboard
|
||||
|
||||
networks:
|
||||
tipi_main_network:
|
||||
|
|
|
@ -44,67 +44,36 @@ services:
|
|||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
api:
|
||||
image: meienberger/runtipi:${TIPI_VERSION}
|
||||
command: /bin/sh -c "cd /api && npm run start"
|
||||
restart: unless-stopped
|
||||
container_name: api
|
||||
depends_on:
|
||||
tipi-db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ${PWD}/repos:/runtipi/repos:ro
|
||||
- ${PWD}/apps:/runtipi/apps
|
||||
- ${PWD}/state:/runtipi/state
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${STORAGE_PATH}:/app/storage
|
||||
- ${PWD}/.env:/runtipi/.env:ro
|
||||
environment:
|
||||
INTERNAL_IP: ${INTERNAL_IP}
|
||||
TIPI_VERSION: ${TIPI_VERSION}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
NGINX_PORT: ${NGINX_PORT}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_USERNAME: tipi
|
||||
POSTGRES_DBNAME: tipi
|
||||
POSTGRES_HOST: tipi-db
|
||||
NODE_ENV: production
|
||||
APPS_REPO_ID: ${APPS_REPO_ID}
|
||||
APPS_REPO_URL: ${APPS_REPO_URL}
|
||||
DOMAIN: ${DOMAIN}
|
||||
ARCHITECTURE: ${ARCHITECTURE}
|
||||
networks:
|
||||
- tipi_main_network
|
||||
labels:
|
||||
traefik.enable: true
|
||||
# Web
|
||||
traefik.http.routers.api.rule: PathPrefix(`/api`)
|
||||
traefik.http.routers.api.service: api
|
||||
traefik.http.routers.api.entrypoints: web
|
||||
traefik.http.routers.api.middlewares: api-stripprefix
|
||||
traefik.http.services.api.loadbalancer.server.port: 3001
|
||||
# Websecure
|
||||
traefik.http.routers.api-secure.rule: (Host(`${DOMAIN}`) && PathPrefix(`/api`))
|
||||
traefik.http.routers.api-secure.entrypoints: websecure
|
||||
traefik.http.routers.api-secure.service: api-secure
|
||||
traefik.http.routers.api-secure.tls.certresolver: myresolver
|
||||
traefik.http.routers.api-secure.middlewares: api-stripprefix
|
||||
traefik.http.services.api-secure.loadbalancer.server.port: 3001
|
||||
# Middlewares
|
||||
traefik.http.middlewares.api-stripprefix.stripprefix.prefixes: /api
|
||||
|
||||
dashboard:
|
||||
image: meienberger/runtipi:${TIPI_VERSION}
|
||||
command: /bin/sh -c "cd /dashboard && node server.js"
|
||||
restart: unless-stopped
|
||||
container_name: dashboard
|
||||
networks:
|
||||
- tipi_main_network
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_started
|
||||
tipi-db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
INTERNAL_IP: ${INTERNAL_IP}
|
||||
TIPI_VERSION: ${TIPI_VERSION}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
NGINX_PORT: ${NGINX_PORT}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_USERNAME: ${POSTGRES_USERNAME}
|
||||
POSTGRES_DBNAME: ${POSTGRES_DBNAME}
|
||||
POSTGRES_HOST: ${POSTGRES_HOST}
|
||||
APPS_REPO_ID: ${APPS_REPO_ID}
|
||||
APPS_REPO_URL: ${APPS_REPO_URL}
|
||||
DOMAIN: ${DOMAIN}
|
||||
ARCHITECTURE: ${ARCHITECTURE}
|
||||
REDIS_HOST: ${REDIS_HOST}
|
||||
volumes:
|
||||
- ${PWD}/state:/runtipi/state
|
||||
- ${PWD}/repos:/runtipi/repos:ro
|
||||
- ${PWD}/apps:/runtipi/apps
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${STORAGE_PATH}:/app/storage
|
||||
labels:
|
||||
traefik.enable: true
|
||||
# Web
|
||||
|
|
40
esbuild.js
Normal file
40
esbuild.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
#!/usr/bin/env node
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const esbuild = require('esbuild');
|
||||
const { spawn } = require('child_process');
|
||||
const pkg = require('./package.json');
|
||||
|
||||
const isDev = process.argv[2] !== 'build';
|
||||
|
||||
process.env.NODE_ENV = isDev ? 'development' : 'production';
|
||||
|
||||
let server;
|
||||
const onRebuild = () => {
|
||||
if (isDev) {
|
||||
if (server) server.kill('SIGINT');
|
||||
server = spawn('node', ['dist/index.js'], { stdio: [0, 1, 2] });
|
||||
} else {
|
||||
spawn('pnpm', ['next', 'build'], { stdio: [0, 1, 2] });
|
||||
}
|
||||
};
|
||||
|
||||
const included = ['express', 'pg', '@runtipi/postgres-migrations'];
|
||||
const excluded = ['pg-native', '*required-server-files.json'];
|
||||
const external = Object.keys(pkg.dependencies || {}).filter((dep) => !included.includes(dep));
|
||||
external.push(...excluded);
|
||||
|
||||
esbuild
|
||||
.build({
|
||||
entryPoints: ['src/server/index.ts'],
|
||||
external,
|
||||
define: { 'process.env.NODE_ENV': `"${process.env.NODE_ENV}"` },
|
||||
platform: 'node',
|
||||
target: 'node14',
|
||||
outfile: 'dist/index.js',
|
||||
tsconfig: 'tsconfig.json',
|
||||
bundle: true,
|
||||
minify: isDev,
|
||||
sourcemap: isDev,
|
||||
watch: isDev,
|
||||
})
|
||||
.finally(onRebuild);
|
|
@ -1,7 +0,0 @@
|
|||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
testMatch: ["**/__tests__/**/*.test.ts"],
|
||||
testPathIgnorePatterns: ["/node_modules/", "/packages/"],
|
||||
};
|
39
jest.config.ts
Normal file
39
jest.config.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import nextJest from 'next/jest';
|
||||
|
||||
const createJestConfig = nextJest({
|
||||
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
|
||||
dir: './',
|
||||
});
|
||||
|
||||
const customClientConfig = {
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/client/jest.setup.tsx'],
|
||||
testMatch: ['<rootDir>/src/client/**/*.{spec,test}.{ts,tsx}', '!<rootDir>/src/server/**/*.{spec,test}.{ts,tsx}'],
|
||||
};
|
||||
|
||||
const customServerConfig = {
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['<rootDir>/src/server/**/*.test.ts'],
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/server/jest.setup.ts'],
|
||||
};
|
||||
|
||||
export default async () => {
|
||||
const clientConfig = await createJestConfig(customClientConfig)();
|
||||
const serverConfig = await createJestConfig(customServerConfig)();
|
||||
|
||||
return {
|
||||
verbose: true,
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['src/server/**/*.{ts,tsx}', 'src/client/**/*.{ts,tsx}', '!src/**/mocks/**/*.{ts,tsx}', '!**/*.{spec,test}.{ts,tsx}', '!**/index.{ts,tsx}'],
|
||||
projects: [
|
||||
{
|
||||
displayName: 'client',
|
||||
...clientConfig,
|
||||
},
|
||||
{
|
||||
displayName: 'server',
|
||||
...serverConfig,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
Binary file not shown.
23
next.config.mjs
Normal file
23
next.config.mjs
Normal file
|
@ -0,0 +1,23 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
swcMinify: true,
|
||||
output: 'standalone',
|
||||
reactStrictMode: true,
|
||||
serverRuntimeConfig: {
|
||||
INTERNAL_IP: process.env.INTERNAL_IP,
|
||||
TIPI_VERSION: process.env.TIPI_VERSION,
|
||||
JWT_SECRET: process.env.JWT_SECRET,
|
||||
POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD,
|
||||
POSTGRES_USERNAME: process.env.POSTGRES_USERNAME,
|
||||
POSTGRES_DBNAME: process.env.POSTGRES_DBNAME,
|
||||
POSTGRES_HOST: process.env.POSTGRES_HOST,
|
||||
APPS_REPO_ID: process.env.APPS_REPO_ID,
|
||||
APPS_REPO_URL: process.env.APPS_REPO_URL,
|
||||
DOMAIN: process.env.DOMAIN,
|
||||
ARCHITECTURE: process.env.ARCHITECTURE,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
REDIS_HOST: process.env.REDIS_HOST,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
129
package.json
129
package.json
|
@ -1,26 +1,126 @@
|
|||
{
|
||||
"name": "runtipi",
|
||||
"version": "0.8.1",
|
||||
"version": "1.0.0",
|
||||
"description": "A homeserver for everyone",
|
||||
"scripts": {
|
||||
"commit": "git-cz",
|
||||
"act:test-install": "act --container-architecture linux/amd64 -j test-install",
|
||||
"act:docker": "act --container-architecture linux/amd64 --secret-file github.secrets -j build-images",
|
||||
"copy:migrations": "mkdir -p dist/migrations && cp -r ./src/server/migrations dist",
|
||||
"prisma:pull": "prisma db pull",
|
||||
"test": "dotenv -e .env.test -- jest --colors",
|
||||
"test:client": "jest --colors --selectProjects client --",
|
||||
"test:server": "jest --colors --selectProjects server --",
|
||||
"postinstall": "prisma generate",
|
||||
"dev": "npm run copy:migrations && node ./esbuild.js dev",
|
||||
"start": "NODE_ENV=production node index.js",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint --fix",
|
||||
"build": "npm run copy:migrations && node ./esbuild.js build",
|
||||
"build:server": "node ./esbuild.js build",
|
||||
"build:next": "next build",
|
||||
"start:dev": "./scripts/start-dev.sh",
|
||||
"start:dev-container": "./.devcontainer/filewatcher.sh && npm run start:dev",
|
||||
"start:rc": "docker-compose -f docker-compose.rc.yml --env-file .env up --build",
|
||||
"start:prod": "docker-compose -f docker-compose.test.yml --env-file .env up --build",
|
||||
"start:pg": "docker run --name test-db -p 5433:5432 -d --rm -e POSTGRES_PASSWORD=postgres postgres",
|
||||
"start:pg": "docker run --name test-db -p 5433:5432 -d --rm -e POSTGRES_PASSWORD=postgres postgres:14",
|
||||
"version": "echo $npm_package_version",
|
||||
"release:rc": "./scripts/deploy/release-rc.sh",
|
||||
"test:build": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t meienberger/runtipi:test ."
|
||||
"test:build": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t meienberger/runtipi:test .",
|
||||
"test:build:arm64": "docker buildx build --platform linux/arm64 -t meienberger/runtipi:test .",
|
||||
"test:build:arm7": "docker buildx build --platform linux/arm/v7 -t meienberger/runtipi:test .",
|
||||
"test:build:amd64": "docker buildx build --platform linux/amd64 -t meienberger/runtipi:test ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^2.9.10",
|
||||
"@prisma/client": "^4.8.0",
|
||||
"@runtipi/postgres-migrations": "^5.3.0",
|
||||
"@tabler/core": "1.0.0-beta16",
|
||||
"@tabler/icons": "^1.109.0",
|
||||
"@tanstack/react-query": "^4.24.4",
|
||||
"@trpc/client": "^10.11.1",
|
||||
"@trpc/next": "^10.11.1",
|
||||
"@trpc/react-query": "^10.11.1",
|
||||
"@trpc/server": "^10.11.1",
|
||||
"argon2": "^0.29.1",
|
||||
"clsx": "^1.1.1",
|
||||
"express": "^4.17.3",
|
||||
"fs-extra": "^10.1.0",
|
||||
"graphql": "^15.8.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"next": "13.1.6",
|
||||
"node-cron": "^3.0.1",
|
||||
"node-fetch-commonjs": "^3.2.4",
|
||||
"pg": "^8.7.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.38.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-select": "^5.6.1",
|
||||
"react-tooltip": "^4.4.3",
|
||||
"redis": "^4.3.1",
|
||||
"remark-breaks": "^3.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sass": "^1.55.0",
|
||||
"semver": "^7.3.7",
|
||||
"sharp": "0.30.7",
|
||||
"superjson": "^1.12.0",
|
||||
"tslib": "^2.4.0",
|
||||
"uuid": "^9.0.0",
|
||||
"validator": "^13.7.0",
|
||||
"winston": "^3.7.2",
|
||||
"zod": "^3.19.1",
|
||||
"zustand": "^3.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.0.3",
|
||||
"@commitlint/config-conventional": "^17.0.3",
|
||||
"@commitlint/cz-commitlint": "^17.0.3",
|
||||
"commitizen": "^4.2.4",
|
||||
"inquirer": "8.2.4"
|
||||
"@babel/core": "^7.0.0",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@testing-library/dom": "^8.19.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/isomorphic-fetch": "^0.0.36",
|
||||
"@types/jest": "^29.2.4",
|
||||
"@types/jsonwebtoken": "^9.0.0",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/node-cron": "^3.0.2",
|
||||
"@types/pg": "^8.6.5",
|
||||
"@types/react": "18.0.8",
|
||||
"@types/react-dom": "18.0.3",
|
||||
"@types/semver": "^7.3.12",
|
||||
"@types/testing-library__jest-dom": "^5.14.5",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/validator": "^13.7.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.47.1",
|
||||
"@typescript-eslint/parser": "^5.47.1",
|
||||
"dotenv-cli": "^6.0.0",
|
||||
"esbuild": "^0.16.17",
|
||||
"eslint": "8.30.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-next": "13.1.1",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-jest": "^27.1.7",
|
||||
"eslint-plugin-jsdoc": "^39.6.9",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-react": "^7.31.10",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"jest": "^29.3.1",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
"msw": "^1.0.0",
|
||||
"next-router-mock": "^0.8.0",
|
||||
"prettier": "^2.8.4",
|
||||
"prisma": "^4.10.1",
|
||||
"ts-jest": "^29.0.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "4.9.4",
|
||||
"wait-for-expect": "^3.0.2",
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -31,10 +131,5 @@
|
|||
"bugs": {
|
||||
"url": "https://github.com/meienberger/runtipi/issues"
|
||||
},
|
||||
"homepage": "https://github.com/meienberger/runtipi#readme",
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "@commitlint/cz-commitlint"
|
||||
}
|
||||
}
|
||||
"homepage": "https://github.com/meienberger/runtipi#readme"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
node_modules/
|
||||
.next/
|
||||
dist/
|
||||
sessions/
|
||||
logs/
|
35
packages/dashboard/.gitignore
vendored
35
packages/dashboard/.gitignore
vendored
|
@ -1,35 +0,0 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
|
@ -1,34 +0,0 @@
|
|||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
|
||||
|
||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
|
@ -1,9 +0,0 @@
|
|||
overwrite: true
|
||||
schema: "http://localhost:3000/api/graphql"
|
||||
documents: "src/graphql/**/*.graphql"
|
||||
generates:
|
||||
src/generated/graphql.tsx:
|
||||
plugins:
|
||||
- "typescript"
|
||||
- "typescript-operations"
|
||||
- "typescript-react-apollo"
|
|
@ -1,18 +0,0 @@
|
|||
const nextJest = require('next/jest');
|
||||
|
||||
const createJestConfig = nextJest({
|
||||
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
|
||||
dir: './',
|
||||
});
|
||||
|
||||
// Add any custom config to be passed to Jest
|
||||
const customJestConfig = {
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/jest.setup.tsx'],
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/index.ts', '!**/src/pages/**/*.{ts,tsx}', '!**/src/mocks/**', '!**/src/core/apollo/**'],
|
||||
testMatch: ['<rootDir>/src/**/*.{spec,test}.{ts,tsx}'],
|
||||
};
|
||||
|
||||
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||
module.exports = createJestConfig(customJestConfig);
|
|
@ -1,8 +0,0 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
|
@ -1,85 +0,0 @@
|
|||
{
|
||||
"name": "dashboard",
|
||||
"version": "0.8.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "jest --colors",
|
||||
"dev": "next dev",
|
||||
"dev:msw": "NEXT_PUBLIC_API_MOCKING=enabled next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint --fix",
|
||||
"gen": "graphql-codegen --config codegen.yml"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.6.8",
|
||||
"@hookform/resolvers": "^2.9.10",
|
||||
"@tabler/core": "1.0.0-beta16",
|
||||
"@tabler/icons": "^1.109.0",
|
||||
"clsx": "^1.1.1",
|
||||
"graphql": "^15.8.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"next": "13.0.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.38.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-select": "^5.6.1",
|
||||
"react-tooltip": "^4.4.3",
|
||||
"remark-breaks": "^3.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-mdx": "^2.1.1",
|
||||
"sass": "^1.55.0",
|
||||
"semver": "^7.3.7",
|
||||
"sharp": "0.30.7",
|
||||
"swr": "^1.3.0",
|
||||
"tslib": "^2.4.0",
|
||||
"validator": "^13.7.0",
|
||||
"zod": "^3.19.1",
|
||||
"zustand": "^3.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
"@faker-js/faker": "^7.3.0",
|
||||
"@graphql-codegen/cli": "^2.6.2",
|
||||
"@graphql-codegen/typescript": "^2.5.1",
|
||||
"@graphql-codegen/typescript-operations": "^2.4.2",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.2.16",
|
||||
"@testing-library/dom": "^8.19.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/isomorphic-fetch": "^0.0.36",
|
||||
"@types/jest": "^27.5.0",
|
||||
"@types/node": "17.0.31",
|
||||
"@types/react": "18.0.8",
|
||||
"@types/react-dom": "18.0.3",
|
||||
"@types/semver": "^7.3.12",
|
||||
"@types/testing-library__jest-dom": "^5.14.5",
|
||||
"@types/validator": "^13.7.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.18.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"concurrently": "^7.1.0",
|
||||
"eslint": "8.12.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-next": "12.1.4",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-jest": "^27.1.6",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-react": "^7.31.10",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"jest": "^28.1.0",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
"msw": "^0.49.1",
|
||||
"next-router-mock": "^0.8.0",
|
||||
"ts-jest": "^28.0.2",
|
||||
"typescript": "4.6.4",
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": "public"
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import router from 'next/router';
|
||||
import { SystemStatus } from '../../../state/systemStore';
|
||||
import { StatusScreen } from '../../StatusScreen';
|
||||
|
||||
interface IProps {
|
||||
children: ReactElement;
|
||||
}
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||
|
||||
export const StatusProvider: React.FC<IProps> = ({ children }) => {
|
||||
const [s, setS] = useState<SystemStatus>(SystemStatus.RUNNING);
|
||||
const { data, isValidating } = useSWR<{ status: SystemStatus }>('/api/status', fetcher, { refreshInterval: 1000 });
|
||||
|
||||
useEffect(() => {
|
||||
// If previous was not running and current is running, we need to refresh the page
|
||||
if (data?.status === SystemStatus.RUNNING && s !== SystemStatus.RUNNING) {
|
||||
router.reload();
|
||||
}
|
||||
|
||||
if (data?.status === SystemStatus.RUNNING) {
|
||||
setS(SystemStatus.RUNNING);
|
||||
}
|
||||
if (data?.status === SystemStatus.RESTARTING) {
|
||||
setS(SystemStatus.RESTARTING);
|
||||
}
|
||||
if (data?.status === SystemStatus.UPDATING) {
|
||||
setS(SystemStatus.UPDATING);
|
||||
}
|
||||
}, [data?.status, s]);
|
||||
|
||||
if (isValidating && !data?.status) {
|
||||
return <StatusScreen title="" subtitle="" />;
|
||||
}
|
||||
|
||||
if (s === SystemStatus.RESTARTING) {
|
||||
return <StatusScreen title="Your system is restarting..." subtitle="Please do not refresh this page" />;
|
||||
}
|
||||
|
||||
if (s === SystemStatus.UPDATING) {
|
||||
return <StatusScreen title="Your system is updating..." subtitle="Please do not refresh this page" />;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
|
@ -1,11 +0,0 @@
|
|||
import { ApolloClient, from, InMemoryCache } from '@apollo/client';
|
||||
import links from './links';
|
||||
|
||||
export const createApolloClient = (): ApolloClient<unknown> => {
|
||||
const additiveLink = from([links.errorLink, links.authLink, links.httpLink]);
|
||||
|
||||
return new ApolloClient({
|
||||
link: additiveLink,
|
||||
cache: new InMemoryCache(),
|
||||
});
|
||||
};
|
|
@ -1,14 +0,0 @@
|
|||
import { setContext } from '@apollo/client/link/context';
|
||||
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
authorization: token ? `Bearer ${token}` : '',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default authLink;
|
|
@ -1,14 +0,0 @@
|
|||
import { onError } from '@apollo/client/link/error';
|
||||
|
||||
const errorLink = onError(({ graphQLErrors, networkError }) => {
|
||||
if (graphQLErrors)
|
||||
graphQLErrors.forEach(({ message, locations, path }) => {
|
||||
console.warn(`Error link [GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`);
|
||||
});
|
||||
|
||||
if (networkError) {
|
||||
console.warn(`Error link [Network error]: ${networkError}`);
|
||||
}
|
||||
});
|
||||
|
||||
export default errorLink;
|
|
@ -1,7 +0,0 @@
|
|||
import { HttpLink } from '@apollo/client';
|
||||
|
||||
const httpLink = new HttpLink({
|
||||
uri: '/api/graphql',
|
||||
});
|
||||
|
||||
export default httpLink;
|
|
@ -1,11 +0,0 @@
|
|||
import errorLink from './errorLink';
|
||||
import httpLink from './httpLink';
|
||||
import authLink from './authLink';
|
||||
|
||||
const links = {
|
||||
errorLink,
|
||||
httpLink,
|
||||
authLink,
|
||||
};
|
||||
|
||||
export default links;
|
|
@ -1,18 +0,0 @@
|
|||
import { AppCategoriesEnum } from '../generated/graphql';
|
||||
|
||||
export const APP_CATEGORIES = [
|
||||
{ name: 'Network', id: AppCategoriesEnum.Network, icon: 'FaNetworkWired' },
|
||||
{ name: 'Media', id: AppCategoriesEnum.Media, icon: 'FaVideo' },
|
||||
{ name: 'Development', id: AppCategoriesEnum.Development, icon: 'FaCode' },
|
||||
{ name: 'Automation', id: AppCategoriesEnum.Automation, icon: 'FaRobot' },
|
||||
{ name: 'Social', id: AppCategoriesEnum.Social, icon: 'FaUserFriends' },
|
||||
{ name: 'Utilities', id: AppCategoriesEnum.Utilities, icon: 'FaWrench' },
|
||||
{ name: 'Photography', id: AppCategoriesEnum.Photography, icon: 'FaCamera' },
|
||||
{ name: 'Security', id: AppCategoriesEnum.Security, icon: 'FaShieldAlt' },
|
||||
{ name: 'Featured', id: AppCategoriesEnum.Featured, icon: 'FaStar' },
|
||||
{ name: 'Books', id: AppCategoriesEnum.Books, icon: 'FaBook' },
|
||||
{ name: 'Data', id: AppCategoriesEnum.Data, icon: 'FaDatabase' },
|
||||
{ name: 'Music', id: AppCategoriesEnum.Music, icon: 'FaMusic' },
|
||||
{ name: 'Finance', id: AppCategoriesEnum.Finance, icon: 'FaMoneyBillAlt' },
|
||||
{ name: 'Gaming', id: AppCategoriesEnum.Gaming, icon: 'FaGamepad' },
|
||||
];
|
|
@ -1,4 +0,0 @@
|
|||
export interface IUser {
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,7 +0,0 @@
|
|||
mutation InstallApp($input: AppInputType!) {
|
||||
installApp(input: $input) {
|
||||
id
|
||||
status
|
||||
__typename
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
mutation Login($input: UsernamePasswordInput!) {
|
||||
login(input: $input) {
|
||||
token
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
mutation Logout {
|
||||
logout
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
mutation Register($input: UsernamePasswordInput!) {
|
||||
register(input: $input) {
|
||||
token
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
mutation Restart {
|
||||
restart
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
mutation StartApp($id: String!) {
|
||||
startApp(id: $id) {
|
||||
id
|
||||
status
|
||||
__typename
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
mutation StopApp($id: String!) {
|
||||
stopApp(id: $id) {
|
||||
id
|
||||
status
|
||||
__typename
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
mutation UninstallApp($id: String!) {
|
||||
uninstallApp(id: $id) {
|
||||
id
|
||||
status
|
||||
__typename
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
mutation Update {
|
||||
update
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
mutation UpdateApp($id: String!) {
|
||||
updateApp(id: $id) {
|
||||
id
|
||||
status
|
||||
__typename
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
mutation UpdateAppConfig($input: AppInputType!) {
|
||||
updateAppConfig(input: $input) {
|
||||
id
|
||||
status
|
||||
__typename
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
query GetApp($appId: String!) {
|
||||
getApp(id: $appId) {
|
||||
id
|
||||
status
|
||||
config
|
||||
version
|
||||
exposed
|
||||
domain
|
||||
updateInfo {
|
||||
current
|
||||
latest
|
||||
dockerVersion
|
||||
}
|
||||
info {
|
||||
id
|
||||
port
|
||||
name
|
||||
description
|
||||
available
|
||||
version
|
||||
tipi_version
|
||||
short_desc
|
||||
author
|
||||
source
|
||||
categories
|
||||
url_suffix
|
||||
https
|
||||
exposable
|
||||
no_gui
|
||||
form_fields {
|
||||
type
|
||||
label
|
||||
max
|
||||
min
|
||||
hint
|
||||
placeholder
|
||||
required
|
||||
env_variable
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
query InstalledApps {
|
||||
installedApps {
|
||||
id
|
||||
status
|
||||
config
|
||||
version
|
||||
updateInfo {
|
||||
current
|
||||
latest
|
||||
dockerVersion
|
||||
}
|
||||
info {
|
||||
id
|
||||
name
|
||||
description
|
||||
tipi_version
|
||||
short_desc
|
||||
https
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
query Configured {
|
||||
isConfigured
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
# Write your query or mutation here
|
||||
query ListApps {
|
||||
listAppsInfo {
|
||||
apps {
|
||||
id
|
||||
available
|
||||
tipi_version
|
||||
port
|
||||
name
|
||||
version
|
||||
short_desc
|
||||
author
|
||||
categories
|
||||
https
|
||||
}
|
||||
total
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
query Me {
|
||||
me {
|
||||
id
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
query RefreshToken {
|
||||
refreshToken {
|
||||
token
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
query SystemInfo {
|
||||
systemInfo {
|
||||
cpu {
|
||||
load
|
||||
}
|
||||
disk {
|
||||
available
|
||||
used
|
||||
total
|
||||
}
|
||||
memory {
|
||||
available
|
||||
used
|
||||
total
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
query Version {
|
||||
version {
|
||||
current
|
||||
latest
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { ApolloClient } from '@apollo/client';
|
||||
import { createApolloClient } from '../core/apollo/client';
|
||||
|
||||
interface IReturnProps {
|
||||
client?: ApolloClient<unknown>;
|
||||
isLoadingComplete?: boolean;
|
||||
}
|
||||
|
||||
export default function useCachedResources(): IReturnProps {
|
||||
const [isLoadingComplete, setLoadingComplete] = useState(false);
|
||||
const [client, setClient] = useState<ApolloClient<unknown>>();
|
||||
|
||||
async function loadResourcesAndDataAsync() {
|
||||
try {
|
||||
const restoredClient = createApolloClient();
|
||||
|
||||
setClient(restoredClient);
|
||||
} catch (error) {
|
||||
// We might want to provide this error information to an error reporting service
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingComplete(true);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadResourcesAndDataAsync();
|
||||
}, []);
|
||||
|
||||
return { client, isLoadingComplete };
|
||||
}
|
|
@ -1,133 +0,0 @@
|
|||
import { graphql, rest } from 'msw';
|
||||
import {
|
||||
ConfiguredQuery,
|
||||
LoginMutation,
|
||||
LogoutMutationResult,
|
||||
MeQuery,
|
||||
RefreshTokenQuery,
|
||||
RegisterMutation,
|
||||
RegisterMutationVariables,
|
||||
UsernamePasswordInput,
|
||||
VersionQuery,
|
||||
SystemInfoQuery,
|
||||
} from '../generated/graphql';
|
||||
import appHandlers from './handlers/appHandlers';
|
||||
|
||||
const restHandlers = [
|
||||
rest.get('/api/status', (req, res, ctx) =>
|
||||
res(
|
||||
ctx.delay(200),
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'RUNNING',
|
||||
}),
|
||||
),
|
||||
),
|
||||
];
|
||||
const graphqlHandlers = [
|
||||
// Handles a "Login" mutation
|
||||
graphql.mutation('Login', (req, res, ctx) => {
|
||||
const { username } = req.variables as UsernamePasswordInput;
|
||||
sessionStorage.setItem('is-authenticated', username);
|
||||
|
||||
const result: LoginMutation = {
|
||||
login: { token: 'token' },
|
||||
};
|
||||
|
||||
return res(ctx.delay(), ctx.data(result));
|
||||
}),
|
||||
|
||||
// Handles a "Logout" mutation
|
||||
graphql.mutation('Logout', (req, res, ctx) => {
|
||||
sessionStorage.removeItem('is-authenticated');
|
||||
|
||||
const result: LogoutMutationResult['data'] = {
|
||||
logout: true,
|
||||
};
|
||||
|
||||
return res(ctx.delay(), ctx.data(result));
|
||||
}),
|
||||
|
||||
// Handles me query
|
||||
graphql.query('Me', (req, res, ctx) => {
|
||||
const isAuthenticated = sessionStorage.getItem('is-authenticated');
|
||||
if (!isAuthenticated) {
|
||||
return res(ctx.errors([{ message: 'Not authenticated' }]));
|
||||
}
|
||||
const result: MeQuery = {
|
||||
me: { id: '1' },
|
||||
};
|
||||
|
||||
return res(ctx.delay(), ctx.data(result));
|
||||
}),
|
||||
|
||||
graphql.query('RefreshToken', (req, res, ctx) => {
|
||||
const result: RefreshTokenQuery = {
|
||||
refreshToken: { token: 'token' },
|
||||
};
|
||||
|
||||
return res(ctx.delay(), ctx.data(result));
|
||||
}),
|
||||
|
||||
graphql.mutation('Register', (req, res, ctx) => {
|
||||
const {
|
||||
input: { username },
|
||||
} = req.variables as RegisterMutationVariables;
|
||||
|
||||
const result: RegisterMutation = {
|
||||
register: { token: 'token' },
|
||||
};
|
||||
|
||||
if (username === 'error@error.com') {
|
||||
return res(ctx.errors([{ message: 'Username is already taken' }]));
|
||||
}
|
||||
|
||||
return res(ctx.data(result));
|
||||
}),
|
||||
appHandlers.listApps,
|
||||
appHandlers.getApp,
|
||||
appHandlers.installedApps,
|
||||
appHandlers.installApp,
|
||||
graphql.query('Version', (req, res, ctx) => {
|
||||
const result: VersionQuery = {
|
||||
version: {
|
||||
current: '1.0.0',
|
||||
latest: '1.0.0',
|
||||
},
|
||||
};
|
||||
|
||||
return res(ctx.data(result));
|
||||
}),
|
||||
|
||||
graphql.query('Configured', (req, res, ctx) => {
|
||||
const result: ConfiguredQuery = {
|
||||
isConfigured: true,
|
||||
};
|
||||
|
||||
return res(ctx.data(result));
|
||||
}),
|
||||
|
||||
graphql.query('SystemInfo', (req, res, ctx) => {
|
||||
const result: SystemInfoQuery = {
|
||||
systemInfo: {
|
||||
cpu: {
|
||||
load: 50,
|
||||
},
|
||||
disk: {
|
||||
available: 1000000000,
|
||||
total: 2000000000,
|
||||
used: 1000000000,
|
||||
},
|
||||
memory: {
|
||||
available: 1000000000,
|
||||
total: 2000000000,
|
||||
used: 1000000000,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return res(ctx.data(result));
|
||||
}),
|
||||
];
|
||||
|
||||
export const handlers = [...graphqlHandlers, ...restHandlers];
|
|
@ -1,173 +0,0 @@
|
|||
import { graphql } from 'msw';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { createAppsRandomly } from '../fixtures/app.fixtures';
|
||||
import { AppInputType, AppStatusEnum, GetAppQuery, InstallAppMutation, InstalledAppsQuery, ListAppsQuery } from '../../generated/graphql';
|
||||
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
export const notEmpty = <TValue>(value: TValue | null | undefined): value is TValue => value !== null && value !== undefined;
|
||||
|
||||
const removeDuplicates = <T extends { id: string }>(array: T[]) =>
|
||||
array.filter((a, i) => {
|
||||
const index = array.findIndex((_a) => _a.id === a.id);
|
||||
return index === i;
|
||||
});
|
||||
|
||||
export const mockedApps = removeDuplicates(createAppsRandomly(faker.datatype.number({ min: 20, max: 30 })));
|
||||
|
||||
export const mockInstalledAppIds = mockedApps.slice(0, faker.datatype.number({ min: 5, max: 8 })).map((a) => a.id);
|
||||
const stoppedAppsIds = mockInstalledAppIds.slice(0, faker.datatype.number({ min: 1, max: 3 }));
|
||||
|
||||
/**
|
||||
* GetApp handler
|
||||
*/
|
||||
const getApp = graphql.query('GetApp', (req, res, ctx) => {
|
||||
const { appId } = req.variables as { appId: string };
|
||||
|
||||
const app = mockedApps.find((a) => a.id === appId);
|
||||
|
||||
if (!app) {
|
||||
return res(ctx.errors([{ message: 'App not found' }]));
|
||||
}
|
||||
|
||||
const isInstalled = mockInstalledAppIds.includes(appId);
|
||||
|
||||
let status = AppStatusEnum.Missing;
|
||||
if (isInstalled) {
|
||||
status = AppStatusEnum.Running;
|
||||
}
|
||||
if (isInstalled && stoppedAppsIds.includes(appId)) {
|
||||
status = AppStatusEnum.Stopped;
|
||||
}
|
||||
|
||||
const result: GetAppQuery = {
|
||||
getApp: {
|
||||
id: app.id,
|
||||
status,
|
||||
info: app,
|
||||
__typename: 'App',
|
||||
config: {},
|
||||
exposed: false,
|
||||
updateInfo: null,
|
||||
domain: null,
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
|
||||
return res(ctx.data(result));
|
||||
});
|
||||
|
||||
const getAppError = graphql.query('GetApp', (req, res, ctx) => res(ctx.errors([{ message: 'test-error' }])));
|
||||
|
||||
/**
|
||||
* ListApps handler
|
||||
*/
|
||||
const listApps = graphql.query('ListApps', async (req, res, ctx) => {
|
||||
const result: ListAppsQuery = {
|
||||
listAppsInfo: {
|
||||
apps: mockedApps,
|
||||
total: mockedApps.length,
|
||||
},
|
||||
};
|
||||
|
||||
await wait(100);
|
||||
|
||||
return res(ctx.data(result));
|
||||
});
|
||||
|
||||
const listAppsEmpty = graphql.query('ListApps', (req, res, ctx) => {
|
||||
const result: ListAppsQuery = {
|
||||
listAppsInfo: {
|
||||
apps: [],
|
||||
total: 0,
|
||||
},
|
||||
};
|
||||
return res(ctx.data(result));
|
||||
});
|
||||
|
||||
const listAppsError = graphql.query('ListApps', (req, res, ctx) => res(ctx.errors([{ message: 'test-error' }])));
|
||||
|
||||
/**
|
||||
* InstalledApps handler
|
||||
*/
|
||||
const installedApps = graphql.query('InstalledApps', (req, res, ctx) => {
|
||||
const apps: InstalledAppsQuery['installedApps'] = mockInstalledAppIds
|
||||
.map((id) => {
|
||||
const app = mockedApps.find((a) => a.id === id);
|
||||
if (!app) return null;
|
||||
|
||||
let status = AppStatusEnum.Running;
|
||||
if (stoppedAppsIds.includes(id)) {
|
||||
status = AppStatusEnum.Stopped;
|
||||
}
|
||||
|
||||
return {
|
||||
__typename: 'App' as const,
|
||||
id: app.id,
|
||||
status,
|
||||
config: {},
|
||||
info: app,
|
||||
version: 1,
|
||||
updateInfo: null,
|
||||
};
|
||||
})
|
||||
.filter(notEmpty);
|
||||
|
||||
const result: InstalledAppsQuery = {
|
||||
installedApps: apps,
|
||||
};
|
||||
|
||||
return res(ctx.data(result));
|
||||
});
|
||||
|
||||
const installedAppsEmpty = graphql.query('InstalledApps', (req, res, ctx) => {
|
||||
const result: InstalledAppsQuery = {
|
||||
installedApps: [],
|
||||
};
|
||||
|
||||
return res(ctx.data(result));
|
||||
});
|
||||
|
||||
const installedAppsError = graphql.query('InstalledApps', (req, res, ctx) => res(ctx.errors([{ message: 'test-error' }])));
|
||||
|
||||
const installedAppsNoInfo = graphql.query('InstalledApps', (req, res, ctx) => {
|
||||
const result: InstalledAppsQuery = {
|
||||
installedApps: [
|
||||
{
|
||||
__typename: 'App' as const,
|
||||
id: 'app-id',
|
||||
status: AppStatusEnum.Running,
|
||||
config: {},
|
||||
info: null,
|
||||
version: 1,
|
||||
updateInfo: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
return res(ctx.data(result));
|
||||
});
|
||||
|
||||
/**
|
||||
* Install app handler
|
||||
*/
|
||||
const installApp = graphql.mutation('InstallApp', (req, res, ctx) => {
|
||||
const { input } = req.variables as { input: AppInputType };
|
||||
|
||||
const app = mockedApps.find((a) => a.id === input.id);
|
||||
|
||||
if (!app) {
|
||||
return res(ctx.errors([{ message: 'App not found' }]));
|
||||
}
|
||||
|
||||
const result: InstallAppMutation = {
|
||||
installApp: {
|
||||
__typename: 'App' as const,
|
||||
id: app.id,
|
||||
status: AppStatusEnum.Running,
|
||||
},
|
||||
};
|
||||
|
||||
return res(ctx.data(result));
|
||||
});
|
||||
|
||||
export default { getApp, getAppError, listApps, listAppsEmpty, listAppsError, installedApps, installedAppsEmpty, installedAppsError, installedAppsNoInfo, installApp };
|
|
@ -1,22 +0,0 @@
|
|||
import React from 'react';
|
||||
import { InstallForm } from '../InstallForm';
|
||||
import { AppInfo } from '../../../../generated/graphql';
|
||||
import { Modal, ModalBody, ModalHeader } from '../../../../components/ui/Modal';
|
||||
|
||||
interface IProps {
|
||||
app: Pick<AppInfo, 'name' | 'form_fields' | 'exposable'>;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (values: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
export const InstallModal: React.FC<IProps> = ({ app, isOpen, onClose, onSubmit }) => (
|
||||
<Modal onClose={onClose} isOpen={isOpen}>
|
||||
<ModalHeader>
|
||||
<h5 className="modal-title">Install {app.name}</h5>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<InstallForm onSubmit={onSubmit} formFields={app.form_fields} exposable={app.exposable} />
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
|
@ -1,25 +0,0 @@
|
|||
import React from 'react';
|
||||
import { InstallForm } from './InstallForm';
|
||||
import { App, AppInfo } from '../../../generated/graphql';
|
||||
import { Modal, ModalBody, ModalHeader } from '../../../components/ui/Modal';
|
||||
|
||||
interface IProps {
|
||||
app: AppInfo;
|
||||
config: App['config'];
|
||||
isOpen: boolean;
|
||||
exposed?: boolean;
|
||||
domain?: string;
|
||||
onClose: () => void;
|
||||
onSubmit: (values: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
export const UpdateSettingsModal: React.FC<IProps> = ({ app, config, isOpen, onClose, onSubmit, exposed, domain }) => (
|
||||
<Modal onClose={onClose} isOpen={isOpen}>
|
||||
<ModalHeader>
|
||||
<h5 className="modal-title">Update {app.name} config</h5>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<InstallForm onSubmit={onSubmit} formFields={app.form_fields} exposable={app.exposable} initalValues={{ ...config, exposed, domain }} />
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
|
@ -1,153 +0,0 @@
|
|||
import { graphql } from 'msw';
|
||||
import React from 'react';
|
||||
import { fireEvent, render, renderHook, screen, waitFor } from '../../../../../tests/test-utils';
|
||||
import { AppStatusEnum } from '../../../../generated/graphql';
|
||||
import { createAppEntity } from '../../../../mocks/fixtures/app.fixtures';
|
||||
import { server } from '../../../../mocks/server';
|
||||
import { useToastStore } from '../../../../state/toastStore';
|
||||
import { AppDetailsContainer } from './AppDetailsContainer';
|
||||
|
||||
describe('Test: AppDetailsContainer', () => {
|
||||
describe('Test: UI', () => {
|
||||
it('should render', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({});
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(app.info.short_desc)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display update button when update is available', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({ overrides: { updateInfo: { current: 2, latest: 3 } } });
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('action-button-update')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display install button when app is not installed', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({ overrides: { status: AppStatusEnum.Missing } });
|
||||
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('action-button-install')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display uninstall and start button when app is stopped', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({ overrides: { status: AppStatusEnum.Stopped } });
|
||||
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('action-button-remove')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('action-button-start')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display stop, open and settings buttons when app is running', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({ overrides: { status: AppStatusEnum.Running } });
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('action-button-stop')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('action-button-open')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('action-button-settings')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not display update button when update is not available', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({ overrides: { updateInfo: { current: 3, latest: 3 } } });
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId('action-button-update')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not display open button when app has no_gui set to true', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({ overridesInfo: { no_gui: true } });
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId('action-button-open')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test: Open app', () => {
|
||||
it('should call window.open with the correct url when open button is clicked', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({});
|
||||
const spy = jest.spyOn(window, 'open').mockImplementation(() => null);
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
|
||||
// Act
|
||||
const openButton = screen.getByTestId('action-button-open');
|
||||
openButton.click();
|
||||
|
||||
// Assert
|
||||
expect(spy).toHaveBeenCalledWith(`http://localhost:${app.info.port}`, '_blank', 'noreferrer');
|
||||
});
|
||||
|
||||
it('should open with https when app info has https set to true', async () => {
|
||||
// Arrange
|
||||
const app = createAppEntity({ overridesInfo: { https: true } });
|
||||
const spy = jest.spyOn(window, 'open').mockImplementation(() => null);
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
|
||||
// Act
|
||||
const openButton = screen.getByTestId('action-button-open');
|
||||
openButton.click();
|
||||
|
||||
// Assert
|
||||
expect(spy).toHaveBeenCalledWith(`https://localhost:${app.info.port}`, '_blank', 'noreferrer');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test: Install app', () => {
|
||||
const installFn = jest.fn();
|
||||
const fakeInstallHandler = graphql.mutation('InstallApp', (req, res, ctx) => {
|
||||
installFn(req.variables);
|
||||
return res(ctx.data({ installApp: { id: 'id', status: '', __typename: '' } }));
|
||||
});
|
||||
|
||||
it('should call install mutation when install form is submitted', async () => {
|
||||
// Arrange
|
||||
server.use(fakeInstallHandler);
|
||||
const app = createAppEntity({ overrides: { status: AppStatusEnum.Missing } });
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
|
||||
// Act
|
||||
const installForm = screen.getByTestId('install-form');
|
||||
fireEvent.submit(installForm);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(installFn).toHaveBeenCalledWith({
|
||||
input: { id: app.id, form: {}, exposed: false, domain: '' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should display a toast error when install mutation fails', async () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
server.use(graphql.mutation('InstallApp', (req, res, ctx) => res(ctx.errors([{ message: 'my big error' }]))));
|
||||
const app = createAppEntity({ overrides: { status: AppStatusEnum.Missing } });
|
||||
render(<AppDetailsContainer app={app} info={app.info} />);
|
||||
|
||||
// Act
|
||||
const installForm = screen.getByTestId('install-form');
|
||||
fireEvent.submit(installForm);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.toasts).toHaveLength(1);
|
||||
expect(result.current.toasts[0].description).toEqual('my big error');
|
||||
expect(result.current.toasts[0].status).toEqual('error');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,196 +0,0 @@
|
|||
import React from 'react';
|
||||
import { useDisclosure } from '../../../../hooks/useDisclosure';
|
||||
import { useToastStore } from '../../../../state/toastStore';
|
||||
import { AppLogo } from '../../../../components/AppLogo/AppLogo';
|
||||
import { AppStatus } from '../../../../components/AppStatus';
|
||||
import {
|
||||
App,
|
||||
AppInfo,
|
||||
AppStatusEnum,
|
||||
GetAppDocument,
|
||||
InstalledAppsDocument,
|
||||
useInstallAppMutation,
|
||||
useStartAppMutation,
|
||||
useStopAppMutation,
|
||||
useUninstallAppMutation,
|
||||
useUpdateAppConfigMutation,
|
||||
useUpdateAppMutation,
|
||||
} from '../../../../generated/graphql';
|
||||
import { AppActions } from '../../components/AppActions';
|
||||
import { AppDetailsTabs } from '../../components/AppDetailsTabs';
|
||||
import { InstallModal } from '../../components/InstallModal';
|
||||
import { StopModal } from '../../components/StopModal';
|
||||
import { UninstallModal } from '../../components/UninstallModal';
|
||||
import { UpdateModal } from '../../components/UpdateModal';
|
||||
import { UpdateSettingsModal } from '../../components/UpdateSettingsModal';
|
||||
import { FormValues } from '../../components/InstallForm/InstallForm';
|
||||
|
||||
interface IProps {
|
||||
app: Pick<App, 'id' | 'updateInfo' | 'config' | 'exposed' | 'domain' | 'status'>;
|
||||
info: AppInfo;
|
||||
}
|
||||
|
||||
export const AppDetailsContainer: React.FC<IProps> = ({ app, info }) => {
|
||||
const { addToast } = useToastStore();
|
||||
const installDisclosure = useDisclosure();
|
||||
const uninstallDisclosure = useDisclosure();
|
||||
const stopDisclosure = useDisclosure();
|
||||
const updateDisclosure = useDisclosure();
|
||||
const updateSettingsDisclosure = useDisclosure();
|
||||
|
||||
// Mutations
|
||||
const [install] = useInstallAppMutation({ refetchQueries: [{ query: GetAppDocument, variables: { appId: info.id } }, { query: InstalledAppsDocument }] });
|
||||
const [update] = useUpdateAppMutation({ refetchQueries: [{ query: GetAppDocument, variables: { appId: info.id } }] });
|
||||
const [uninstall] = useUninstallAppMutation({ refetchQueries: [{ query: GetAppDocument, variables: { appId: info.id } }, { query: InstalledAppsDocument }] });
|
||||
const [stop] = useStopAppMutation({ refetchQueries: [{ query: GetAppDocument, variables: { appId: info.id } }] });
|
||||
const [start] = useStartAppMutation({ refetchQueries: [{ query: GetAppDocument, variables: { appId: info.id } }] });
|
||||
const [updateConfig] = useUpdateAppConfigMutation({ refetchQueries: [{ query: GetAppDocument, variables: { appId: info.id } }] });
|
||||
|
||||
const updateAvailable = Number(app?.updateInfo?.current || 0) < Number(app?.updateInfo?.latest);
|
||||
|
||||
const handleError = (error: unknown) => {
|
||||
if (error instanceof Error) {
|
||||
addToast({
|
||||
title: 'Error',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
position: 'top',
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleInstallSubmit = async (values: FormValues) => {
|
||||
installDisclosure.close();
|
||||
const { exposed, domain, ...form } = values;
|
||||
|
||||
try {
|
||||
await install({
|
||||
variables: { input: { form, id: info.id, exposed: exposed || false, domain: domain || '' } },
|
||||
optimisticResponse: { installApp: { id: info.id, status: AppStatusEnum.Installing, __typename: 'App' } },
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnistallSubmit = async () => {
|
||||
uninstallDisclosure.close();
|
||||
try {
|
||||
await uninstall({ variables: { id: info.id }, optimisticResponse: { uninstallApp: { id: info.id, status: AppStatusEnum.Uninstalling, __typename: 'App' } } });
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStopSubmit = async () => {
|
||||
stopDisclosure.close();
|
||||
try {
|
||||
await stop({ variables: { id: info.id }, optimisticResponse: { stopApp: { id: info.id, status: AppStatusEnum.Stopping, __typename: 'App' } } });
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartSubmit = async () => {
|
||||
try {
|
||||
await start({ variables: { id: info.id }, optimisticResponse: { startApp: { id: info.id, status: AppStatusEnum.Starting, __typename: 'App' } } });
|
||||
} catch (e: unknown) {
|
||||
handleError(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateSettingsSubmit = async (values: FormValues) => {
|
||||
try {
|
||||
const { exposed, domain, ...form } = values;
|
||||
await updateConfig({ variables: { input: { form, id: info.id, exposed: exposed || false, domain: domain || '' } } });
|
||||
addToast({
|
||||
title: 'Success',
|
||||
description: 'App config updated successfully. Restart the app to apply the changes.',
|
||||
position: 'top',
|
||||
status: 'success',
|
||||
isClosable: true,
|
||||
});
|
||||
updateSettingsDisclosure.close();
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateSubmit = async () => {
|
||||
updateDisclosure.close();
|
||||
try {
|
||||
await update({ variables: { id: info.id }, optimisticResponse: { updateApp: { id: info.id, status: AppStatusEnum.Updating, __typename: 'App' } } });
|
||||
addToast({
|
||||
title: 'Success',
|
||||
description: 'App updated successfully',
|
||||
position: 'top',
|
||||
status: 'success',
|
||||
isClosable: true,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
const { https } = info;
|
||||
const protocol = https ? 'https' : 'http';
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
// Current domain
|
||||
const domain = window.location.hostname;
|
||||
window.open(`${protocol}://${domain}:${info.port}${info.url_suffix || ''}`, '_blank', 'noreferrer');
|
||||
}
|
||||
};
|
||||
|
||||
const newVersion = [app?.updateInfo?.dockerVersion ? `${app?.updateInfo?.dockerVersion}` : '', `(${String(app?.updateInfo?.latest)})`].join(' ');
|
||||
|
||||
return (
|
||||
<div className="card" data-testid="app-details">
|
||||
<InstallModal onSubmit={handleInstallSubmit} isOpen={installDisclosure.isOpen} onClose={installDisclosure.close} app={info} />
|
||||
<StopModal onConfirm={handleStopSubmit} isOpen={stopDisclosure.isOpen} onClose={stopDisclosure.close} app={info} />
|
||||
<UninstallModal onConfirm={handleUnistallSubmit} isOpen={uninstallDisclosure.isOpen} onClose={uninstallDisclosure.close} app={info} />
|
||||
<UpdateModal onConfirm={handleUpdateSubmit} isOpen={updateDisclosure.isOpen} onClose={updateDisclosure.close} app={info} newVersion={newVersion} />
|
||||
<UpdateSettingsModal
|
||||
onSubmit={handleUpdateSettingsSubmit}
|
||||
isOpen={updateSettingsDisclosure.isOpen}
|
||||
onClose={updateSettingsDisclosure.close}
|
||||
app={info}
|
||||
config={app?.config}
|
||||
exposed={app?.exposed}
|
||||
domain={app?.domain || ''}
|
||||
/>
|
||||
<div className="card-header d-flex flex-column flex-md-row">
|
||||
<AppLogo id={info.id} size={130} alt={info.name} />
|
||||
<div className="w-100 d-flex flex-column ms-md-3 align-items-center align-items-md-start">
|
||||
<div>
|
||||
<span className="mt-1 me-1">Version: </span>
|
||||
<span className="badge bg-gray mt-2">{info?.version}</span>
|
||||
</div>
|
||||
{app.domain && (
|
||||
<a target="_blank" rel="noreferrer" className="mt-1" href={`https://${app.domain}`}>
|
||||
https://{app.domain}
|
||||
</a>
|
||||
)}
|
||||
<span className="mt-1 text-muted text-center mb-2">{info.short_desc}</span>
|
||||
<div className="mb-1">{app && app?.status !== AppStatusEnum.Missing && <AppStatus status={app.status} />}</div>
|
||||
<AppActions
|
||||
updateAvailable={updateAvailable}
|
||||
onUpdate={updateDisclosure.open}
|
||||
onUpdateSettings={updateSettingsDisclosure.open}
|
||||
onStop={stopDisclosure.open}
|
||||
onCancel={stopDisclosure.open}
|
||||
onUninstall={uninstallDisclosure.open}
|
||||
onInstall={installDisclosure.open}
|
||||
onOpen={handleOpen}
|
||||
onStart={handleStartSubmit}
|
||||
app={info}
|
||||
status={app?.status}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AppDetailsTabs info={info} />
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,51 +0,0 @@
|
|||
import { useApolloClient } from '@apollo/client';
|
||||
import React, { useState } from 'react';
|
||||
import { useLoginMutation } from '../../../../generated/graphql';
|
||||
import { useToastStore } from '../../../../state/toastStore';
|
||||
import { AuthFormLayout } from '../../components/AuthFormLayout';
|
||||
import { LoginForm } from '../../components/LoginForm';
|
||||
|
||||
type FormValues = { email: string; password: string };
|
||||
|
||||
export const LoginContainer: React.FC = () => {
|
||||
const client = useApolloClient();
|
||||
const [login] = useLoginMutation({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { addToast } = useToastStore();
|
||||
|
||||
const handleError = (error: unknown) => {
|
||||
localStorage.removeItem('token');
|
||||
if (error instanceof Error) {
|
||||
addToast({
|
||||
title: 'Error',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
position: 'top',
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogin = async (values: FormValues) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await login({ variables: { input: { username: values.email, password: values.password } } });
|
||||
|
||||
if (data?.login?.token) {
|
||||
localStorage.setItem('token', data.login.token);
|
||||
}
|
||||
|
||||
await client.refetchQueries({ include: ['Me'] });
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthFormLayout>
|
||||
<LoginForm onSubmit={handleLogin} loading={loading} />
|
||||
</AuthFormLayout>
|
||||
);
|
||||
};
|
|
@ -1,48 +0,0 @@
|
|||
import router from 'next/router';
|
||||
import React, { useState } from 'react';
|
||||
import { useRegisterMutation } from '../../../../generated/graphql';
|
||||
import { useToastStore } from '../../../../state/toastStore';
|
||||
import { AuthFormLayout } from '../../components/AuthFormLayout';
|
||||
import { RegisterForm } from '../../components/RegisterForm';
|
||||
|
||||
export const RegisterContainer: React.FC = () => {
|
||||
const { addToast } = useToastStore();
|
||||
const [register] = useRegisterMutation({ refetchQueries: ['Me'] });
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleError = (error: unknown) => {
|
||||
if (error instanceof Error) {
|
||||
addToast({
|
||||
title: 'Error',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
position: 'top',
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegister = async (values: { email: string; password: string }) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await register({ variables: { input: { username: values.email, password: values.password } } });
|
||||
|
||||
if (data?.register?.token) {
|
||||
localStorage.setItem('token', data.register.token);
|
||||
router.reload();
|
||||
} else {
|
||||
setLoading(false);
|
||||
handleError(new Error('Something went wrong'));
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthFormLayout>
|
||||
<RegisterForm onSubmit={handleRegister} loading={loading} />
|
||||
</AuthFormLayout>
|
||||
);
|
||||
};
|
|
@ -1,14 +0,0 @@
|
|||
import React from 'react';
|
||||
import type { NextPage } from 'next';
|
||||
import { Layout } from '../../../../components/Layout';
|
||||
import Dashboard from '../../containers/Dashboard';
|
||||
import { useSystemInfoQuery } from '../../../../generated/graphql';
|
||||
|
||||
export const DashboardPage: NextPage = () => {
|
||||
const { data, loading } = useSystemInfoQuery({ pollInterval: 10000 });
|
||||
return (
|
||||
<Layout title="Dashboard" loading={loading && !data}>
|
||||
{data?.systemInfo && <Dashboard data={data.systemInfo} />}
|
||||
</Layout>
|
||||
);
|
||||
};
|
|
@ -1,122 +0,0 @@
|
|||
import { faker } from '@faker-js/faker';
|
||||
import { graphql } from 'msw';
|
||||
import React from 'react';
|
||||
import { act, fireEvent, render, renderHook, screen, waitFor } from '../../../../../tests/test-utils';
|
||||
import { server } from '../../../../mocks/server';
|
||||
import { useToastStore } from '../../../../state/toastStore';
|
||||
import { SettingsContainer } from './SettingsContainer';
|
||||
|
||||
describe('Test: SettingsContainer', () => {
|
||||
it('renders without crashing', () => {
|
||||
const currentVersion = faker.system.semver();
|
||||
render(<SettingsContainer currentVersion={currentVersion} latestVersion={currentVersion} />);
|
||||
|
||||
expect(screen.getByText('Tipi settings')).toBeInTheDocument();
|
||||
expect(screen.getByText('Already up to date')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should make update button disable if current version is equal to latest version', () => {
|
||||
const currentVersion = faker.system.semver();
|
||||
render(<SettingsContainer currentVersion={currentVersion} latestVersion={currentVersion} />);
|
||||
|
||||
expect(screen.getByText('Already up to date')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should make update button disabled if current version is greater than latest version', () => {
|
||||
const currentVersion = '1.0.0';
|
||||
const latestVersion = '0.0.1';
|
||||
render(<SettingsContainer currentVersion={currentVersion} latestVersion={latestVersion} />);
|
||||
|
||||
expect(screen.getByText('Already up to date')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should display update button if current version is less than latest version', () => {
|
||||
const currentVersion = '0.0.1';
|
||||
const latestVersion = '1.0.0';
|
||||
|
||||
render(<SettingsContainer currentVersion={currentVersion} latestVersion={latestVersion} />);
|
||||
expect(screen.getByText(`Update to ${latestVersion}`)).toBeInTheDocument();
|
||||
expect(screen.getByText(`Update to ${latestVersion}`)).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('should call update mutation when update button is clicked', async () => {
|
||||
// Arrange
|
||||
|
||||
localStorage.setItem('token', 'token');
|
||||
const currentVersion = '0.0.1';
|
||||
const latestVersion = '1.0.0';
|
||||
const updateFn = jest.fn();
|
||||
server.use(
|
||||
graphql.mutation('Update', async (req, res, ctx) => {
|
||||
updateFn();
|
||||
return res(ctx.data({ update: true }));
|
||||
}),
|
||||
);
|
||||
render(<SettingsContainer currentVersion={currentVersion} latestVersion={latestVersion} />);
|
||||
|
||||
// Act
|
||||
act(() => screen.getByText(`Update to ${latestVersion}`).click());
|
||||
|
||||
fireEvent.click(screen.getByText('Update'));
|
||||
waitFor(() => expect(updateFn).toHaveBeenCalled());
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
await act(() => new Promise((resolve) => setTimeout(resolve, 1500)));
|
||||
|
||||
// Assert
|
||||
const token = localStorage.getItem('token');
|
||||
expect(token).toBe(null);
|
||||
});
|
||||
|
||||
it('should display error toast if update mutation fails', async () => {
|
||||
// Arrange
|
||||
const { result, unmount } = renderHook(() => useToastStore());
|
||||
const currentVersion = '0.0.1';
|
||||
const latestVersion = '1.0.0';
|
||||
const errorMessage = 'My error';
|
||||
server.use(graphql.mutation('Update', async (req, res, ctx) => res(ctx.errors([{ message: errorMessage }]))));
|
||||
render(<SettingsContainer currentVersion={currentVersion} latestVersion={latestVersion} />);
|
||||
|
||||
// Act
|
||||
act(() => screen.getByText(`Update to ${latestVersion}`).click());
|
||||
fireEvent.click(screen.getByText('Update'));
|
||||
|
||||
// Assert
|
||||
await waitFor(() => expect(result.current.toasts[0].description).toBe(errorMessage));
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should call restart mutation when restart button is clicked', async () => {
|
||||
// Arrange
|
||||
const restartFn = jest.fn();
|
||||
server.use(
|
||||
graphql.mutation('Restart', async (req, res, ctx) => {
|
||||
restartFn();
|
||||
return res(ctx.data({ restart: true }));
|
||||
}),
|
||||
);
|
||||
render(<SettingsContainer currentVersion="1.0.0" latestVersion="1.0.0" />);
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('settings-modal-restart-button'));
|
||||
waitFor(() => expect(restartFn).toHaveBeenCalled());
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
|
||||
// Assert
|
||||
const token = localStorage.getItem('token');
|
||||
expect(token).toBe(null);
|
||||
});
|
||||
|
||||
it('should display error toast if restart mutation fails', async () => {
|
||||
// Arrange
|
||||
const { result } = renderHook(() => useToastStore());
|
||||
const errorMessage = 'Update error';
|
||||
server.use(graphql.mutation('Restart', async (req, res, ctx) => res(ctx.errors([{ message: errorMessage }]))));
|
||||
render(<SettingsContainer currentVersion="1.0.0" latestVersion="1.0.0" />);
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('settings-modal-restart-button'));
|
||||
|
||||
// Assert
|
||||
await waitFor(() => expect(result.current.toasts[0].description).toBe(errorMessage));
|
||||
});
|
||||
});
|
|
@ -1,21 +0,0 @@
|
|||
import { graphql } from 'msw';
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from '../../../../../tests/test-utils';
|
||||
import { server } from '../../../../mocks/server';
|
||||
import { SettingsPage } from './SettingsPage';
|
||||
|
||||
describe('Test: SettingsPage', () => {
|
||||
it('should render', async () => {
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Tipi settings')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should render error page if version query fails', async () => {
|
||||
server.use(graphql.query('Version', (req, res, ctx) => res(ctx.errors([{ message: 'My error' }]))));
|
||||
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => expect(screen.getByText('My error')).toBeInTheDocument());
|
||||
});
|
||||
});
|
|
@ -1,54 +0,0 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import type { AppProps } from 'next/app';
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
import Head from 'next/head';
|
||||
import useCachedResources from '../hooks/useCachedRessources';
|
||||
import '../styles/global.css';
|
||||
import '../styles/global.scss';
|
||||
import { useUIStore } from '../state/uiStore';
|
||||
import { ToastProvider } from '../components/hoc/ToastProvider';
|
||||
import { StatusProvider } from '../components/hoc/StatusProvider';
|
||||
import { AuthProvider } from '../components/hoc/AuthProvider';
|
||||
import { StatusScreen } from '../components/StatusScreen';
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
const { setDarkMode } = useUIStore();
|
||||
|
||||
// check theme on component mount
|
||||
useEffect(() => {
|
||||
const themeCheck = () => {
|
||||
if (localStorage.darkMode === 'true' || (!('darkMode' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.body.classList.add('theme-dark');
|
||||
setDarkMode(true);
|
||||
} else {
|
||||
document.body.classList.remove('theme-light');
|
||||
setDarkMode(false);
|
||||
}
|
||||
};
|
||||
themeCheck();
|
||||
}, [setDarkMode]);
|
||||
|
||||
const { client } = useCachedResources();
|
||||
if (!client) {
|
||||
return <StatusScreen title="" subtitle="" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="h-100">
|
||||
<ApolloProvider client={client}>
|
||||
<Head>
|
||||
<title>Tipi</title>
|
||||
</Head>
|
||||
<ToastProvider>
|
||||
<StatusProvider>
|
||||
<AuthProvider>
|
||||
<Component {...pageProps} />
|
||||
</AuthProvider>
|
||||
</StatusProvider>
|
||||
</ToastProvider>
|
||||
</ApolloProvider>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
|
@ -1 +0,0 @@
|
|||
export { AppDetailsPage as default } from '../../modules/Apps/pages/AppDetailsPage';
|
|
@ -1 +0,0 @@
|
|||
export { AppStorePage as default } from '../../modules/AppStore/pages/AppStorePage';
|
|
@ -1 +0,0 @@
|
|||
export { AppDetailsPage as default } from '../../modules/Apps/pages/AppDetailsPage';
|
|
@ -1 +0,0 @@
|
|||
export { AppsPage as default } from '../../modules/Apps/pages/AppsPage';
|
|
@ -1 +0,0 @@
|
|||
export { DashboardPage as default } from '../modules/Dashboard/pages/DashboardPage';
|
|
@ -1 +0,0 @@
|
|||
export { SettingsPage as default } from '../modules/Settings/pages/SettingsPage';
|
|
@ -1,17 +0,0 @@
|
|||
import create from 'zustand';
|
||||
|
||||
export enum SystemStatus {
|
||||
RUNNING = 'RUNNING',
|
||||
RESTARTING = 'RESTARTING',
|
||||
UPDATING = 'UPDATING',
|
||||
}
|
||||
|
||||
type Store = {
|
||||
status: SystemStatus;
|
||||
setStatus: (status: SystemStatus) => void;
|
||||
};
|
||||
|
||||
export const useSystemStore = create<Store>((set) => ({
|
||||
status: SystemStatus.RUNNING,
|
||||
setStatus: (status: SystemStatus) => set((state) => ({ ...state, status })),
|
||||
}));
|
|
@ -1,31 +0,0 @@
|
|||
import React, { FC, ReactElement } from 'react';
|
||||
import { render, RenderOptions, renderHook } from '@testing-library/react';
|
||||
import { ApolloClient, ApolloProvider, HttpLink, InMemoryCache } from '@apollo/client';
|
||||
import fetch from 'isomorphic-fetch';
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
const link = new HttpLink({
|
||||
uri: 'http://localhost:3000/graphql',
|
||||
// Use explicit `window.fetch` so tha outgoing requests
|
||||
// are captured and deferred until the Service Worker is ready.
|
||||
fetch: (...args) => fetch(...args),
|
||||
});
|
||||
|
||||
// create a mock of Apollo Client
|
||||
export const mockApolloClient = new ApolloClient({
|
||||
cache: new InMemoryCache({}),
|
||||
link,
|
||||
});
|
||||
|
||||
const AllTheProviders: FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
<SWRConfig value={{ dedupingInterval: 0, provider: () => new Map() }}>
|
||||
<ApolloProvider client={mockApolloClient}>{children}</ApolloProvider>
|
||||
</SWRConfig>
|
||||
);
|
||||
|
||||
const customRender = (ui: ReactElement, options?: Omit<RenderOptions, 'wrapper'>) => render(ui, { wrapper: AllTheProviders, ...options });
|
||||
const customRenderHook = (callback: () => any, options?: Omit<RenderOptions, 'wrapper'>) => renderHook(callback, { wrapper: AllTheProviders, ...options });
|
||||
|
||||
export * from '@testing-library/react';
|
||||
export { customRender as render };
|
||||
export { customRenderHook as renderHook };
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"strictNullChecks": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": ["jest", "@testing-library/jest-dom"]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
node_modules/
|
||||
dist/
|
||||
sessions/
|
||||
logs/
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue