Sync 3.5.2 in develop (#1589)

* chore: bump version to 3.5.2

* fix(container): use env variables instead of parsing before ready

* chore: run biome safe autofix

* fix(db): use env for username and db instead of hardcoded value
This commit is contained in:
Nicolas Meienberger 2024-08-10 15:14:08 +02:00 committed by GitHub
parent ad1bbc5fbd
commit 84ea5dcb7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
268 changed files with 1001 additions and 1129 deletions

View File

@ -6,16 +6,9 @@
"moby": true
}
},
"extensions": [
"ms-azuretools.vscode-docker",
"ms-vscode.vscode-typescript-next",
"waderyan.gitblame"
],
"extensions": ["ms-azuretools.vscode-docker", "ms-vscode.vscode-typescript-next", "waderyan.gitblame"],
"postCreateCommand": "./.devcontainer/postCreateCommand.sh",
"forwardPorts": [
80,
3000
],
"forwardPorts": [80, 3000],
"portsAttributes": {
"3000": {
"label": "Runtipi"

View File

@ -1,13 +1,7 @@
{
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
"files": {
"include": [
"**/*.js",
"**/*.jsx",
"**/*.ts",
"**/*.tsx",
"**/*.json"
],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "**/*.json"],
"ignore": [
"node_modules/**",
"dist/**",
@ -21,7 +15,8 @@
"repos/**",
"state/**",
"traefik/**",
"user-config/**"
"user-config/**",
"playwright-report/**"
]
},
"formatter": {

View File

@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { testUser } from './helpers/constants';
import { clearDatabase } from './helpers/db';

View File

@ -1,5 +1,5 @@
import { test, expect } from '@playwright/test';
import { loginUser, createTestUser } from './fixtures/fixtures';
import { expect, test } from '@playwright/test';
import { createTestUser, loginUser } from './fixtures/fixtures';
import { testUser } from './helpers/constants';
import { clearDatabase } from './helpers/db';
import { setPassowrdChangeRequest, unsetPasswordChangeRequest } from './helpers/settings';

View File

@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { loginUser } from './fixtures/fixtures';
import { clearDatabase } from './helpers/db';

View File

@ -1,8 +1,8 @@
import { test, expect } from '@playwright/test';
import fs from 'fs';
import { expect, test } from '@playwright/test';
import { loginUser } from './fixtures/fixtures';
import { clearDatabase } from './helpers/db';
import { testUser } from './helpers/constants';
import { clearDatabase } from './helpers/db';
import { setSettings } from './helpers/settings';
test.beforeEach(async ({ page, context }) => {

View File

@ -1,8 +1,8 @@
import { test, expect } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { appTable } from '@runtipi/db';
import { setSettings } from './helpers/settings';
import { loginUser } from './fixtures/fixtures';
import { clearDatabase, db } from './helpers/db';
import { setSettings } from './helpers/settings';
test.beforeEach(async () => {
test.fixme(true, 'This test is flaky due to incorrect revalidation of the guest dashboard');

View File

@ -1,8 +1,8 @@
import * as argon2 from 'argon2';
import { type BrowserContext, expect, type Page } from '@playwright/test';
import { type BrowserContext, type Page, expect } from '@playwright/test';
import { userTable } from '@runtipi/db';
import { db } from '../helpers/db';
import * as argon2 from 'argon2';
import { testUser } from '../helpers/constants';
import { db } from '../helpers/db';
export const createTestUser = async () => {
// Create user in database

View File

@ -1,6 +1,6 @@
import { Pool } from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';
import * as schema from '@runtipi/db';
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
const connectionString = `postgresql://tipi:${process.env.POSTGRES_PASSWORD}@${process.env.SERVER_IP}:5432/tipi?connect_timeout=300`;

View File

@ -1,8 +1,8 @@
import { promises } from 'fs';
import { z } from 'zod';
import { settingsSchema } from '@runtipi/shared';
import { pathExists } from '@runtipi/shared/node';
import path from 'path';
import type { settingsSchema } from '@runtipi/shared';
import { pathExists } from '@runtipi/shared/node';
import type { z } from 'zod';
import { BASE_PATH } from './constants';
export const setSettings = async (settings: z.infer<typeof settingsSchema>) => {

View File

@ -1,6 +1,6 @@
{
"name": "runtipi",
"version": "3.5.1",
"version": "3.5.2",
"description": "A homeserver for everyone",
"packageManager": "pnpm@9.4.0",
"scripts": {

View File

@ -1,7 +1,7 @@
import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres';
import type { ILogger } from '@runtipi/shared/node';
import { type NodePgDatabase, drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './schema';
import type { ILogger } from '@runtipi/shared/node';
export type IDatabase = NodePgDatabase<typeof schema>;
@ -34,7 +34,11 @@ export class DbClient {
});
pool.on('connect', () => {
this.logger.info('Connected to the database successfully.');
this.logger.debug('Connected to the database successfully.');
});
pool.on('remove', () => {
this.logger.debug('Client removed from the pool.');
});
this.db = drizzle(pool, { schema });

View File

@ -1,7 +1,7 @@
import path from 'node:path';
import pg from 'pg';
import { migrate } from '@runtipi/postgres-migrations';
import type { ILogger } from '@runtipi/shared/node';
import pg from 'pg';
type MigrationParams = {
host: string;

View File

@ -1,5 +1,5 @@
import type { InferInsertModel, InferSelectModel } from 'drizzle-orm';
import { pgTable, pgEnum, integer, varchar, timestamp, serial, boolean, text, jsonb } from 'drizzle-orm/pg-core';
import { boolean, integer, jsonb, pgEnum, pgTable, serial, text, timestamp, varchar } from 'drizzle-orm/pg-core';
const updateStatusEnum = pgEnum('update_status_enum', ['SUCCESS', 'FAILED']);
const appStatusEnum = pgEnum('app_status_enum', [

View File

@ -13,18 +13,8 @@
"noImplicitOverride": true,
"module": "preserve",
"noEmit": true,
"lib": [
"es2022"
]
"lib": ["es2022"]
},
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.mjs",
"**/*.js",
"**/*.jsx"
],
"exclude": [
"node_modules"
]
"include": ["**/*.ts", "**/*.tsx", "**/*.mjs", "**/*.js", "**/*.jsx"],
"exclude": ["node_modules"]
}

View File

@ -1,12 +1,7 @@
module.exports = {
root: true,
plugins: ['@typescript-eslint', 'import'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'prettier',
],
extends: ['plugin:@typescript-eslint/recommended', 'plugin:import/recommended', 'plugin:import/typescript', 'prettier'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',

View File

@ -18,11 +18,7 @@
},
"./package.json": "./package.json"
},
"files": [
"src",
"node",
"dist"
],
"files": ["src", "node", "dist"],
"scripts": {
"lint": "eslint --ext .ts src",
"tsc": "tsc --noEmit",

View File

@ -1,7 +1,7 @@
import fs from 'fs';
import path from 'path';
import { newLogger as createLogger } from './Logger';
import { ILogger } from './Logger.interface';
import type { ILogger } from './Logger.interface';
export class Logger implements ILogger {
private winstonLogger;
@ -16,10 +16,7 @@ export class Logger implements ILogger {
streamLogToHistory(logFile: string) {
return new Promise((resolve, reject) => {
const appLogReadStream = fs.createReadStream(path.join(this.logsFolder, logFile), 'utf-8');
const appLogHistoryWriteStream = fs.createWriteStream(
path.join(this.logsFolder, `${logFile}.history`),
{ flags: 'a' },
);
const appLogHistoryWriteStream = fs.createWriteStream(path.join(this.logsFolder, `${logFile}.history`), { flags: 'a' });
appLogReadStream
.pipe(appLogHistoryWriteStream)

View File

@ -29,9 +29,7 @@ export const newLogger = (id: string, logsFolder: string) => {
);
exceptionHandlers = [new transports.File({ filename: path.join(logsFolder, 'error.log') })];
tr.push(
new transports.Console({ level: process.env.NODE_ENV === 'development' ? 'debug' : 'info' }),
);
tr.push(new transports.Console({ level: process.env.NODE_ENV === 'development' ? 'debug' : 'info' }));
} catch (error) {}
return createLogger({

View File

@ -1,9 +1,9 @@
import fs from 'fs-extra';
import path from 'path';
import { Logger } from '../../../logger/FileLogger';
import fs from 'fs-extra';
import { sanitizePath } from '../../../../helpers/sanitizers';
import { pathExists } from '../../../helpers/fs-helpers';
import { appInfoSchema } from '../../../../schemas/app-schemas';
import { pathExists } from '../../../helpers/fs-helpers';
import { Logger } from '../../../logger/FileLogger';
// Lower level data access class for apps
export class DataAccessApp {
@ -34,17 +34,11 @@ export class DataAccessApp {
const repoAppFolder = path.join(this.getAppsRepoFolder(), sanitizePath(id));
if (await pathExists(path.join(repoAppFolder, 'config.json'))) {
const configFile = await fs.promises.readFile(
path.join(repoAppFolder, 'config.json'),
'utf8',
);
const configFile = await fs.promises.readFile(path.join(repoAppFolder, 'config.json'), 'utf8');
const parsedConfig = appInfoSchema.safeParse(JSON.parse(configFile));
if (parsedConfig.success && parsedConfig.data.available) {
const description = await fs.promises.readFile(
path.join(repoAppFolder, 'metadata', 'description.md'),
'utf8',
);
const description = await fs.promises.readFile(path.join(repoAppFolder, 'metadata', 'description.md'), 'utf8');
return { ...parsedConfig.data, description };
}
}
@ -62,17 +56,11 @@ export class DataAccessApp {
const installedAppFolder = path.join(this.getInstalledAppsFolder(), sanitizePath(id));
if (await pathExists(path.join(installedAppFolder, 'config.json'))) {
const configFile = await fs.promises.readFile(
path.join(installedAppFolder, 'config.json'),
'utf8',
);
const configFile = await fs.promises.readFile(path.join(installedAppFolder, 'config.json'), 'utf8');
const parsedConfig = appInfoSchema.safeParse(JSON.parse(configFile));
if (parsedConfig.success && parsedConfig.data.available) {
const description = await fs.promises.readFile(
path.join(installedAppFolder, 'metadata', 'description.md'),
'utf8',
);
const description = await fs.promises.readFile(path.join(installedAppFolder, 'metadata', 'description.md'), 'utf8');
return { ...parsedConfig.data, description };
}
}
@ -107,9 +95,7 @@ export class DataAccessApp {
const appsRepoFolder = this.getAppsRepoFolder();
if (!(await pathExists(appsRepoFolder))) {
this.logger.error(
`Apps repo ${this.appsRepoId} not found. Make sure your repo is configured correctly.`,
);
this.logger.error(`Apps repo ${this.appsRepoId} not found. Make sure your repo is configured correctly.`);
return [];
}

View File

@ -47,16 +47,14 @@ export class AppDataService {
}),
);
return apps
.filter(notEmpty)
.map(({ id, categories, name, short_desc, deprecated, supported_architectures }) => ({
id,
categories,
name,
short_desc,
deprecated,
supported_architectures,
}));
return apps.filter(notEmpty).map(({ id, categories, name, short_desc, deprecated, supported_architectures }) => ({
id,
categories,
name,
short_desc,
deprecated,
supported_architectures,
}));
}
public async getAppBackups(params: { appId: string; pageSize: number; page: number }) {

View File

@ -65,11 +65,7 @@ const systemCommandSchema = z.object({
command: z.literal('system_info'),
});
export const eventSchema = appEventSchema
.or(restoreAppCommandSchema)
.or(repoCommandSchema)
.or(updateAppCommandSchema)
.or(systemCommandSchema);
export const eventSchema = appEventSchema.or(restoreAppCommandSchema).or(repoCommandSchema).or(updateAppCommandSchema).or(systemCommandSchema);
export const eventResultSchema = z.object({
success: z.boolean(),

View File

@ -3,11 +3,7 @@
"target": "es2017",
"baseUrl": ".",
"paths": {},
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@ -22,19 +18,9 @@
"strictNullChecks": true,
"allowSyntheticDefaultImports": true,
"noUncheckedIndexedAccess": true,
"types": [
"node"
],
"types": ["node"],
"experimentalDecorators": true
},
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.mjs",
"**/*.js",
"**/*.jsx"
],
"exclude": [
"node_modules"
]
"include": ["**/*.ts", "**/*.tsx", "**/*.mjs", "**/*.js", "**/*.jsx"],
"exclude": ["node_modules"]
}

View File

@ -1,10 +1,10 @@
import type { Hono } from 'hono';
import { jwt } from 'hono/jwt';
import { prettyJSON } from 'hono/pretty-json';
import { secureHeaders } from 'hono/secure-headers';
import type { Hono } from 'hono';
import { container } from './inversify.config';
import { getEnv } from './lib/environment';
import { SystemExecutors } from './services';
import { container } from './inversify.config';
import type { IAppExecutors } from './services/app/app.executors';
const system = new SystemExecutors();

View File

@ -1,7 +1,7 @@
/* eslint-disable no-template-curly-in-string */
import { it, describe, expect } from 'vitest';
import { faker } from '@faker-js/faker';
import { ServiceInput, getDockerCompose } from '../docker-templates';
/* eslint-disable no-template-curly-in-string */
import { describe, expect, it } from 'vitest';
import { type ServiceInput, getDockerCompose } from '../docker-templates';
describe('getDockerCompose', async () => {
it('should return correct docker-compose config', async () => {

View File

@ -2,8 +2,8 @@ import { DockerComposeBuilder } from '@/lib/docker/builders/docker-compose-build
import { serviceSchema } from '@/lib/docker/builders/schemas';
import { ServiceBuilder } from '@/lib/docker/builders/service-builder';
import { TraefikLabelsBuilder } from '@/lib/docker/builders/traefik-labels-builder';
import { AppEventForm } from '@runtipi/shared';
import { z } from 'zod';
import type { AppEventForm } from '@runtipi/shared';
import type { z } from 'zod';
export type ServiceInput = z.input<typeof serviceSchema>;
export type Service = z.output<typeof serviceSchema>;

View File

@ -4,24 +4,24 @@ import 'source-map-support/register';
import { type SystemEvent, cleanseErrorData } from '@runtipi/shared';
import path from 'node:path';
import Redis from 'ioredis';
import dotenv from 'dotenv';
import { Queue } from 'bullmq';
import * as Sentry from '@sentry/node';
import { extraErrorDataIntegration } from '@sentry/integrations';
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { copySystemFiles, generateSystemEnvFile, generateTlsCertificates } from '@/lib/system';
import { startWorker } from './watcher/watcher';
import { logger } from '@/lib/logger';
import { RepoExecutors } from './services';
import { copySystemFiles, generateSystemEnvFile, generateTlsCertificates } from '@/lib/system';
import { serve } from '@hono/node-server';
import type { IMigrator } from '@runtipi/db';
import type { ILogger } from '@runtipi/shared/node';
import { extraErrorDataIntegration } from '@sentry/integrations';
import * as Sentry from '@sentry/node';
import { Queue } from 'bullmq';
import dotenv from 'dotenv';
import { Hono } from 'hono';
import Redis from 'ioredis';
import { setupRoutes } from './api';
import { APP_DIR, DATA_DIR } from './config';
import { socketManager } from './lib/socket';
import { container } from './inversify.config';
import { socketManager } from './lib/socket';
import { RepoExecutors } from './services';
import type { IAppExecutors } from './services/app/app.executors';
import type { ILogger } from '@runtipi/shared/node';
import type { IMigrator } from '@runtipi/db';
import { startWorker } from './watcher/watcher';
const envFile = path.join(DATA_DIR, '.env');

View File

@ -1,45 +1,47 @@
import { type ILogger, Logger } from '@runtipi/shared/node';
import { type IDbClient, type IMigrator, DbClient, Migrator } from '@runtipi/db';
import { Container } from 'inversify';
import path from 'node:path';
import { DbClient, type IDbClient, type IMigrator, Migrator } from '@runtipi/db';
import { type ILogger, Logger } from '@runtipi/shared/node';
import { Container } from 'inversify';
import { DATA_DIR } from './config';
import { type ISocketManager, SocketManager } from './lib/socket/SocketManager';
import { getEnv } from './lib/environment';
import { AppExecutors, type IAppExecutors } from './services/app/app.executors';
export function createContainer() {
const { postgresHost, postgresPort, postgresDatabase, postgresPassword, postgresUsername } = getEnv();
try {
const container = new Container();
const container = new Container();
container.bind<ILogger>('ILogger').toDynamicValue(() => {
return new Logger('worker', path.join(DATA_DIR, 'logs'));
});
container.bind<ISocketManager>('ISocketManager').to(SocketManager).inSingletonScope();
container.bind<ILogger>('ILogger').toDynamicValue(() => {
return new Logger('worker', path.join(DATA_DIR, 'logs'));
});
container.bind<ISocketManager>('ISocketManager').to(SocketManager).inSingletonScope();
container
.bind<IDbClient>('IDbClient')
.toDynamicValue((context) => {
return new DbClient(
{
host: String(process.env.POSTGRES_HOST),
port: Number(process.env.POSTGRES_PORT),
password: String(process.env.POSTGRES_PASSWORD),
database: String(process.env.POSTGRES_DBNAME),
username: String(process.env.POSTGRES_USERNAME),
},
context.container.get<ILogger>('ILogger'),
);
})
.inSingletonScope();
container
.bind<IDbClient>('IDbClient')
.toDynamicValue((context) => {
return new DbClient(
{
host: postgresHost,
port: Number(postgresPort),
database: postgresDatabase,
password: postgresPassword,
username: postgresUsername,
},
context.container.get<ILogger>('ILogger'),
);
})
.inSingletonScope();
container.bind<IMigrator>('IMigrator').toDynamicValue((context) => {
return new Migrator(context.container.get<ILogger>('ILogger'));
});
container.bind<IMigrator>('IMigrator').toDynamicValue((context) => {
return new Migrator(context.container.get<ILogger>('ILogger'));
});
container.bind<IAppExecutors>('IAppExecutors').to(AppExecutors);
container.bind<IAppExecutors>('IAppExecutors').to(AppExecutors);
return container;
return container;
} catch (error) {
console.error('Error creating container', error);
throw error;
}
}
export const container = createContainer();

View File

@ -1,25 +1,17 @@
import fs from 'fs';
import zlib from 'zlib';
import { pack, extract } from 'tar-fs';
import { extract, pack } from 'tar-fs';
export class ArchiveManager {
createTarGz = async (sourceDir: string, destinationFile: string) => {
return new Promise<void>((resolve, reject) => {
pack(sourceDir)
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream(destinationFile))
.on('finish', resolve)
.on('error', reject);
pack(sourceDir).pipe(zlib.createGzip()).pipe(fs.createWriteStream(destinationFile)).on('finish', resolve).on('error', reject);
});
};
extractTarGz = async (sourceFile: string, destinationDir: string) => {
return new Promise<void>((resolve, reject) => {
fs.createReadStream(sourceFile)
.pipe(zlib.createGunzip())
.pipe(extract(destinationDir))
.on('finish', resolve)
.on('error', reject);
fs.createReadStream(sourceFile).pipe(zlib.createGunzip()).pipe(extract(destinationDir)).on('finish', resolve).on('error', reject);
});
};
}

View File

@ -1,5 +1,5 @@
import * as yaml from 'yaml';
import { BuiltService } from './service-builder';
import type { BuiltService } from './service-builder';
interface Network {
key?: string;

View File

@ -1,4 +1,4 @@
import { DependsOn } from './schemas';
import type { DependsOn } from './schemas';
interface ServicePort {
containerPort: number;

View File

@ -1,10 +1,10 @@
// const spy = vi.spyOn(dockerHelpers, 'compose').mockImplementation(() => Promise.resolve({ stdout: '', stderr: randomError }));
import { vi, it, describe, beforeEach, expect } from 'vitest';
import { faker } from '@faker-js/faker';
import fs from 'fs';
import { compose } from './docker-helpers';
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
import { faker } from '@faker-js/faker';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { compose } from './docker-helpers';
const execAsync = vi.fn().mockImplementation(() => Promise.resolve({ stdout: '', stderr: '' }));

View File

@ -1,10 +1,10 @@
import path from 'node:path';
import { spawn } from 'node:child_process';
import { execAsync, pathExists } from '@runtipi/shared/node';
import { type SocketEvent, sanitizePath, socketEventSchema } from '@runtipi/shared';
import { logger } from '@/lib/logger';
import { getEnv } from '@/lib/environment';
import path from 'node:path';
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
import { getEnv } from '@/lib/environment';
import { logger } from '@/lib/logger';
import { type SocketEvent, sanitizePath, socketEventSchema } from '@runtipi/shared';
import { execAsync, pathExists } from '@runtipi/shared/node';
import type { Socket } from 'socket.io';
import { getRepoHash } from 'src/services/repo/repo.helpers';
import { DEFAULT_REPO_URL } from '../system/system.helpers';

View File

@ -1,5 +1,5 @@
import { z } from 'zod';
import dotenv from 'dotenv';
import { z } from 'zod';
if (process.env.NODE_ENV === 'development') {
dotenv.config({ path: '.env.dev', override: true });

View File

@ -1,5 +1,5 @@
import { Logger } from '@runtipi/shared/node';
import path from 'node:path';
import { DATA_DIR } from '@/config/constants';
import { Logger } from '@runtipi/shared/node';
export const logger = new Logger('worker', path.join(DATA_DIR, 'logs'));

View File

@ -21,12 +21,8 @@ export class SocketManager implements ISocketManager {
io.on('connection', async (socket) => {
this.logger.debug('Client connected to socket', socket.id);
socket.on('app-logs-init', (event) =>
handleViewAppLogsEvent(socket, event, this.emit.bind(this)),
);
socket.on('runtipi-logs-init', (event) =>
handleViewRuntipiLogsEvent(socket, event, this.emit.bind(this)),
);
socket.on('app-logs-init', (event) => handleViewAppLogsEvent(socket, event, this.emit.bind(this)));
socket.on('runtipi-logs-init', (event) => handleViewRuntipiLogsEvent(socket, event, this.emit.bind(this)));
socket.on('disconnect', () => {});
});

View File

@ -1,4 +1,4 @@
import { container } from 'src/inversify.config';
import { ISocketManager } from './SocketManager';
import type { ISocketManager } from './SocketManager';
export const socketManager = container.get<ISocketManager>('ISocketManager');

View File

@ -1,10 +1,10 @@
import fs from 'fs';
import { it, describe, expect, beforeEach } from 'vitest';
import path from 'path';
import { envMapToString } from '@runtipi/shared';
import { faker } from '@faker-js/faker';
import { generateSystemEnvFile } from './system.helpers';
import { DATA_DIR } from '@/config/constants';
import { faker } from '@faker-js/faker';
import { envMapToString } from '@runtipi/shared';
import { beforeEach, describe, expect, it } from 'vitest';
import { generateSystemEnvFile } from './system.helpers';
const envMap = new Map();
@ -29,10 +29,7 @@ describe('generateSystemEnvFile()', async () => {
expect(envFileExists).toBe(true);
const settingsFileExists = fs.existsSync(path.join(DATA_DIR, 'state', 'settings.json'));
const settingsFileContent = fs.readFileSync(
path.join(DATA_DIR, 'state', 'settings.json'),
'utf8',
);
const settingsFileContent = fs.readFileSync(path.join(DATA_DIR, 'state', 'settings.json'), 'utf8');
expect(settingsFileExists).toBe(true);
expect(settingsFileContent).toBe('{}');
});
@ -67,9 +64,7 @@ describe('generateSystemEnvFile()', async () => {
const generated = await generateSystemEnvFile();
// assert
expect(generated.get('APPS_REPO_ID')).toBe(
'29ca930bfdaffa1dfabf5726336380ede7066bc53297e3c0c868b27c97282903',
);
expect(generated.get('APPS_REPO_ID')).toBe('29ca930bfdaffa1dfabf5726336380ede7066bc53297e3c0c868b27c97282903');
expect(generated.get('APPS_REPO_URL')).toBe('https://github.com/runtipi/runtipi-appstore');
expect(generated.get('JWT_SECRET')).toBeTruthy();
expect(generated.get('DOMAIN')).toBe('example.com');

View File

@ -2,13 +2,13 @@
/* eslint-disable no-restricted-syntax */
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import os from 'os';
import path from 'path';
import { APP_DATA_DIR, APP_DIR, DATA_DIR } from '@/config/constants';
import { envMapToString, envStringToMap, settingsSchema } from '@runtipi/shared';
import { execAsync, pathExists } from '@runtipi/shared/node';
import { logger } from '../logger/logger';
import { getRepoHash } from '../../services/repo/repo.helpers';
import { APP_DATA_DIR, APP_DIR, DATA_DIR } from '@/config/constants';
import { logger } from '../logger/logger';
type EnvKeys =
| 'APPS_REPO_ID'

View File

@ -1,14 +1,14 @@
import fs from 'node:fs';
import { describe, it, expect, vi } from 'vitest';
import path from 'node:path';
import { faker } from '@faker-js/faker';
import * as sharedNode from '@runtipi/shared/node';
import type { IAppExecutors } from '../app.executors';
import { createAppConfig } from '@/tests/apps.factory';
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
import * as dockerHelpers from '@/lib/docker';
import { getEnv } from '@/lib/environment';
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
import { createAppConfig } from '@/tests/apps.factory';
import { faker } from '@faker-js/faker';
import * as sharedNode from '@runtipi/shared/node';
import { container } from 'src/inversify.config';
import { describe, expect, it, vi } from 'vitest';
import type { IAppExecutors } from '../app.executors';
const { pathExists } = sharedNode;

View File

@ -1,11 +1,11 @@
import fs from 'fs';
import { describe, it, expect } from 'vitest';
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
import { createAppConfig } from '@/tests/apps.factory';
import { faker } from '@faker-js/faker';
import { pathExists } from '@runtipi/shared/node';
import { describe, expect, it } from 'vitest';
import { copyDataDir, generateEnvFile } from '../app.helpers';
import { createAppConfig } from '@/tests/apps.factory';
import { getAppEnvMap } from '../env.helpers';
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
describe('app helpers', () => {
describe('Test: generateEnvFile()', () => {
@ -25,9 +25,7 @@ describe('app helpers', () => {
expect(envmap.get('APP_PORT')).toBe(String(appConfig.port));
expect(envmap.get('APP_ID')).toBe(appConfig.id);
expect(envmap.get('ROOT_FOLDER_HOST')).toBe(process.env.ROOT_FOLDER_HOST);
expect(envmap.get('APP_DATA_DIR')).toBe(
`${process.env.RUNTIPI_APP_DATA_PATH}/app-data/${appConfig.id}`,
);
expect(envmap.get('APP_DATA_DIR')).toBe(`${process.env.RUNTIPI_APP_DATA_PATH}/app-data/${appConfig.id}`);
expect(envmap.get('APP_DOMAIN')).toBe(`localhost:${appConfig.port}`);
expect(envmap.get('APP_HOST')).toBe(`localhost`);
expect(envmap.get('APP_PROTOCOL')).toBe(`http`);
@ -39,9 +37,7 @@ describe('app helpers', () => {
await fs.promises.writeFile(`${DATA_DIR}/apps/${appConfig.id}/config.json`, '{}');
// act & assert
await expect(generateEnvFile(appConfig.id, {})).rejects.toThrowError(
`App ${appConfig.id} has invalid config.json file`,
);
await expect(generateEnvFile(appConfig.id, {})).rejects.toThrowError(`App ${appConfig.id} has invalid config.json file`);
});
it('Should automatically generate value for random field', async () => {
@ -84,10 +80,7 @@ describe('app helpers', () => {
});
const randomField = faker.string.alphanumeric(32);
await fs.promises.mkdir(`${APP_DATA_DIR}/${appConfig.id}`, { recursive: true });
await fs.promises.writeFile(
`${APP_DATA_DIR}/${appConfig.id}/app.env`,
`RANDOM_FIELD=${randomField}`,
);
await fs.promises.writeFile(`${APP_DATA_DIR}/${appConfig.id}/app.env`, `RANDOM_FIELD=${randomField}`);
// act
await generateEnvFile(appConfig.id, {});
@ -259,9 +252,7 @@ describe('app helpers', () => {
// assert
const appDataDir = `${APP_DATA_DIR}/${appConfig.id}`;
expect(
await fs.promises.readFile(`${appDataDir}/data/subdir/subsubdir/test.txt`, 'utf8'),
).toBe('test');
expect(await fs.promises.readFile(`${appDataDir}/data/subdir/subsubdir/test.txt`, 'utf8')).toBe('test');
expect(await fs.promises.readFile(`${appDataDir}/data/test.txt`, 'utf8')).toBe('test');
});

View File

@ -1,18 +1,18 @@
import fs from 'node:fs';
import path from 'node:path';
import * as Sentry from '@sentry/node';
import { execAsync, type ILogger, pathExists } from '@runtipi/shared/node';
import { type AppEventForm, type SocketEvent, appInfoSchema, sanitizePath } from '@runtipi/shared';
import { copyDataDir, generateEnvFile } from './app.helpers';
import { compose } from '@/lib/docker';
import { getEnv } from '@/lib/environment';
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
import { getDockerCompose } from '@/config/docker-templates';
import { ArchiveManager } from '@/lib/archive/ArchiveManager';
import { type App, appTable, type IDbClient } from '@runtipi/db';
import { compose } from '@/lib/docker';
import { getEnv } from '@/lib/environment';
import type { ISocketManager } from '@/lib/socket/SocketManager';
import { type App, type IDbClient, appTable } from '@runtipi/db';
import { type AppEventForm, type SocketEvent, appInfoSchema, sanitizePath } from '@runtipi/shared';
import { type ILogger, execAsync, pathExists } from '@runtipi/shared/node';
import * as Sentry from '@sentry/node';
import { and, eq, ne } from 'drizzle-orm';
import { inject, injectable } from 'inversify';
import type { ISocketManager } from '@/lib/socket/SocketManager';
import { copyDataDir, generateEnvFile } from './app.helpers';
export interface IAppExecutors {
regenerateAppEnv(appId: string, form: AppEventForm): Promise<{ success: boolean; message: string }>;

View File

@ -1,18 +1,12 @@
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import {
AppEventForm,
appInfoSchema,
envMapToString,
envStringToMap,
sanitizePath,
} from '@runtipi/shared';
import { pathExists, execAsync } from '@runtipi/shared/node';
import { generateVapidKeys, getAppEnvMap } from './env.helpers';
import { getEnv } from '@/lib/environment';
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
import { getEnv } from '@/lib/environment';
import { getMainEnvMap } from '@/lib/system/system.helpers';
import { type AppEventForm, appInfoSchema, envMapToString, envStringToMap, sanitizePath } from '@runtipi/shared';
import { execAsync, pathExists } from '@runtipi/shared/node';
import { generateVapidKeys, getAppEnvMap } from './env.helpers';
/**
* This function generates a random string of the provided length by using the SHA-256 hash algorithm.
@ -45,9 +39,7 @@ const getEntropy = async (name: string, length: number) => {
export const generateEnvFile = async (appId: string, form: AppEventForm) => {
const { internalIp, appDataPath, rootFolderHost } = getEnv();
const configFile = await fs.promises.readFile(
path.join(DATA_DIR, 'apps', sanitizePath(appId), 'config.json'),
);
const configFile = await fs.promises.readFile(path.join(DATA_DIR, 'apps', sanitizePath(appId), 'config.json'));
const parsedConfig = appInfoSchema.safeParse(JSON.parse(configFile.toString()));
if (!parsedConfig.success) {
@ -115,17 +107,12 @@ export const generateEnvFile = async (appId: string, form: AppEventForm) => {
}
// Create app-data folder if it doesn't exist
const appDataDirectoryExists = await fs.promises
.stat(path.join(APP_DATA_DIR, sanitizePath(appId)))
.catch(() => false);
const appDataDirectoryExists = await fs.promises.stat(path.join(APP_DATA_DIR, sanitizePath(appId))).catch(() => false);
if (!appDataDirectoryExists) {
await fs.promises.mkdir(path.join(APP_DATA_DIR, sanitizePath(appId)), { recursive: true });
}
await fs.promises.writeFile(
path.join(APP_DATA_DIR, sanitizePath(appId), 'app.env'),
envMapToString(envMap),
);
await fs.promises.writeFile(path.join(APP_DATA_DIR, sanitizePath(appId), 'app.env'), envMapToString(envMap));
};
/**
@ -169,16 +156,10 @@ export const copyDataDir = async (id: string) => {
const processFile = async (file: string) => {
if (file.endsWith('.template')) {
const template = await fs.promises.readFile(
path.join(DATA_DIR, 'apps', sanitizePath(id), 'data', file),
'utf-8',
);
const template = await fs.promises.readFile(path.join(DATA_DIR, 'apps', sanitizePath(id), 'data', file), 'utf-8');
const renderedTemplate = renderTemplate(template, envMap);
await fs.promises.writeFile(
path.join(APP_DATA_DIR, sanitizePath(id), 'data', file.replace('.template', '')),
renderedTemplate,
);
await fs.promises.writeFile(path.join(APP_DATA_DIR, sanitizePath(id), 'data', file.replace('.template', '')), renderedTemplate);
} else {
await fs.promises.copyFile(
path.join(DATA_DIR, 'apps', sanitizePath(id), 'data', file),
@ -192,9 +173,7 @@ export const copyDataDir = async (id: string) => {
recursive: true,
});
const files = await fs.promises.readdir(
path.join(DATA_DIR, 'apps', sanitizePath(id), 'data', p),
);
const files = await fs.promises.readdir(path.join(DATA_DIR, 'apps', sanitizePath(id), 'data', p));
await Promise.all(
files.map(async (file) => {
@ -223,8 +202,6 @@ export const copyDataDir = async (id: string) => {
// Remove any .gitkeep files from the app-data folder at any level
if (await pathExists(path.join(APP_DATA_DIR, sanitizePath(id), 'data'))) {
await execAsync(`find ${APP_DATA_DIR}/${sanitizePath(id)}/data -name .gitkeep -delete`).catch(
() => {},
);
await execAsync(`find ${APP_DATA_DIR}/${sanitizePath(id)}/data -name .gitkeep -delete`).catch(() => {});
}
};

View File

@ -1,8 +1,8 @@
import webpush from 'web-push';
import fs from 'fs';
import path from 'path';
import { sanitizePath } from '@runtipi/shared';
import { APP_DATA_DIR } from '@/config/constants';
import { sanitizePath } from '@runtipi/shared';
import webpush from 'web-push';
/**
* This function reads the env file for the app with the provided id and returns a Map containing the key-value pairs of the environment variables.

View File

@ -1,10 +1,10 @@
import path from 'node:path';
import { DATA_DIR } from '@/config/constants';
import { logger } from '@/lib/logger';
import { sanitizePath } from '@runtipi/shared';
import { execAsync, pathExists } from '@runtipi/shared/node';
import * as Sentry from '@sentry/node';
import { sanitizePath } from '@runtipi/shared';
import { getRepoHash, getRepoBaseUrlAndBranch } from './repo.helpers';
import { logger } from '@/lib/logger';
import { DATA_DIR } from '@/config/constants';
import { getRepoBaseUrlAndBranch, getRepoHash } from './repo.helpers';
export class RepoExecutors {
private readonly logger;

View File

@ -1,7 +1,7 @@
import fs from 'fs';
import si from 'systeminformation';
import * as Sentry from '@sentry/node';
import { logger } from '@/lib/logger';
import * as Sentry from '@sentry/node';
import si from 'systeminformation';
export class SystemExecutors {
private readonly logger;

View File

@ -1,8 +1,8 @@
import { getEnv } from '@/lib/environment';
import { logger } from '@/lib/logger';
import { RepoExecutors } from '@/services';
import { eventSchema } from '@runtipi/shared';
import { Worker } from 'bullmq';
import { RepoExecutors } from '@/services';
import { logger } from '@/lib/logger';
import { getEnv } from '@/lib/environment';
import { container } from 'src/inversify.config';
import type { IAppExecutors } from 'src/services/app/app.executors';

View File

@ -1,7 +1,7 @@
import { faker } from '@faker-js/faker';
import fs from 'fs';
import { APP_CATEGORIES, AppInfo, appInfoSchema } from '@runtipi/shared';
import { DATA_DIR, APP_DATA_DIR } from '@/config/constants';
import { APP_DATA_DIR, DATA_DIR } from '@/config/constants';
import { faker } from '@faker-js/faker';
import { APP_CATEGORIES, type AppInfo, appInfoSchema } from '@runtipi/shared';
export const createAppConfig = (props?: Partial<AppInfo>, isInstalled = true) => {
const appInfo = appInfoSchema.parse({

View File

@ -1,8 +1,8 @@
import 'reflect-metadata';
import fs from 'node:fs';
import path from 'node:path';
import { vi, beforeEach } from 'vitest';
import { DATA_DIR } from '@/config/constants';
import { beforeEach, vi } from 'vitest';
vi.mock('@runtipi/shared/node', async (importOriginal) => {
const mod = (await importOriginal()) as object;

View File

@ -1,5 +1,5 @@
import { UserWorkspaceConfig, defineConfig } from 'vitest/config';
import tsconfigPaths from 'vite-tsconfig-paths';
import { type UserWorkspaceConfig, defineConfig } from 'vitest/config';
type Plugin = Exclude<UserWorkspaceConfig['plugins'], undefined>[number];

View File

@ -1,5 +1,5 @@
import { cleanseErrorData, settingsSchema } from '@runtipi/shared';
import * as Sentry from '@sentry/nextjs';
import { settingsSchema, cleanseErrorData } from '@runtipi/shared';
const getClientConfig = () => {
if (typeof window === 'undefined') {

View File

@ -1,8 +1,8 @@
import React from 'react';
import Image from 'next/image';
import { getCurrentLocale } from 'src/utils/getCurrentLocale';
import { getLogo } from '@/lib/themes';
import { TipiConfig } from '@/server/core/TipiConfig';
import Image from 'next/image';
import type React from 'react';
import { getCurrentLocale } from 'src/utils/getCurrentLocale';
import { LanguageSelector } from '../components/LanguageSelector';
export default async function AuthLayout({ children }: { children: React.ReactNode }) {

View File

@ -1,11 +1,11 @@
'use client';
import { useAction } from 'next-safe-action/hooks';
import React, { useState } from 'react';
import { toast } from 'react-hot-toast';
import { loginAction } from '@/actions/login/login-action';
import { verifyTotpAction } from '@/actions/verify-totp/verify-totp-action';
import { useAction } from 'next-safe-action/hooks';
import { useRouter } from 'next/navigation';
import React, { useState } from 'react';
import { toast } from 'react-hot-toast';
import { LoginForm } from '../LoginForm';
import { TotpForm } from '../TotpForm';

View File

@ -1,11 +1,11 @@
import React from 'react';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { zodResolver } from '@hookform/resolvers/zod';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import type React from 'react';
import { useForm } from 'react-hook-form';
import z from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import Link from 'next/link';
import { useTranslations } from 'next-intl';
import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
type FormValues = { email: string; password: string };

View File

@ -1,9 +1,9 @@
import React from 'react';
import { redirect } from 'next/navigation';
import { getUserFromCookie } from '@/server/common/session.helpers';
import type { IAuthQueries } from '@/server/queries/auth/auth.queries';
import { LoginContainer } from './components/LoginContainer';
import { redirect } from 'next/navigation';
import React from 'react';
import { container } from 'src/inversify.config';
import { LoginContainer } from './components/LoginContainer';
export default async function LoginPage() {
const authQueries = container.get<IAuthQueries>('IAuthQueries');

View File

@ -1,10 +1,10 @@
'use client';
import React from 'react';
import { useAction } from 'next-safe-action/hooks';
import { toast } from 'react-hot-toast';
import { useRouter } from 'next/navigation';
import { registerAction } from '@/actions/register/register-action';
import { useAction } from 'next-safe-action/hooks';
import { useRouter } from 'next/navigation';
import type React from 'react';
import { toast } from 'react-hot-toast';
import { RegisterForm } from '../RegisterForm';
export const RegisterContainer: React.FC = () => {

View File

@ -1,10 +1,10 @@
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { zodResolver } from '@hookform/resolvers/zod';
import React from 'react';
import { useTranslations } from 'next-intl';
import type React from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { useTranslations } from 'next-intl';
import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
interface IProps {
onSubmit: (values: FormValues) => void;

View File

@ -1,9 +1,9 @@
import React from 'react';
import { redirect } from 'next/navigation';
import { getUserFromCookie } from '@/server/common/session.helpers';
import type { IAuthQueries } from '@/server/queries/auth/auth.queries';
import { RegisterContainer } from './components/RegisterContainer';
import { redirect } from 'next/navigation';
import React from 'react';
import { container } from 'src/inversify.config';
import { RegisterContainer } from './components/RegisterContainer';
export default async function LoginPage() {
const user = await getUserFromCookie();

View File

@ -1,13 +1,13 @@
'use client';
import React from 'react';
import { useAction } from 'next-safe-action/hooks';
import { toast } from 'react-hot-toast';
import { useRouter } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { Button } from '@/components/ui/Button';
import { resetPasswordAction } from '@/actions/reset-password/reset-password-action';
import { cancelResetPasswordAction } from '@/actions/cancel-reset-password/cancel-reset-password-action';
import { resetPasswordAction } from '@/actions/reset-password/reset-password-action';
import { Button } from '@/components/ui/Button';
import { useTranslations } from 'next-intl';
import { useAction } from 'next-safe-action/hooks';
import { useRouter } from 'next/navigation';
import type React from 'react';
import { toast } from 'react-hot-toast';
import { ResetPasswordForm } from '../ResetPasswordForm';
export const ResetPasswordContainer: React.FC = () => {

View File

@ -1,10 +1,10 @@
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { zodResolver } from '@hookform/resolvers/zod';
import React from 'react';
import { useTranslations } from 'next-intl';
import type React from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { useTranslations } from 'next-intl';
import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
interface IProps {
onSubmit: (values: FormValues) => void;

View File

@ -1,8 +1,8 @@
import React from 'react';
import { getTranslator } from '@/lib/get-translator';
import { ResetPasswordContainer } from './components/ResetPasswordContainer';
import { container } from 'src/inversify.config';
import type { IAuthService } from '@/server/services/auth/auth.service';
import React from 'react';
import { container } from 'src/inversify.config';
import { ResetPasswordContainer } from './components/ResetPasswordContainer';
export default async function ResetPasswordPage() {
const authService = container.get<IAuthService>('IAuthService');

View File

@ -1,8 +1,8 @@
import type { GetAppCommand } from '@/server/services/app-catalog/commands';
import React from 'react';
import { vi, afterEach, describe, it, expect } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render, screen, userEvent, waitFor } from '../../../../../../../tests/test-utils';
import { AppActions } from './AppActions';
import { cleanup, render, screen, waitFor, userEvent } from '../../../../../../../tests/test-utils';
import { GetAppCommand } from '@/server/services/app-catalog/commands';
afterEach(cleanup);

View File

@ -7,15 +7,17 @@ import {
IconLockOff,
IconPlayerPause,
IconPlayerPlay,
IconRotateClockwise,
IconSettings,
IconTrash,
IconX,
IconRotateClockwise,
} from '@tabler/icons-react';
import type React from 'react';
import { Fragment } from 'react';
import { useTranslations } from 'next-intl';
import { startAppAction } from '@/actions/app-actions/start-app-action';
import { useDisclosure } from '@/client/hooks/useDisclosure';
import { Button, type ButtonProps } from '@/components/ui/Button';
import {
DropdownMenu,
DropdownMenuContent,
@ -24,22 +26,20 @@ import {
DropdownMenuLabel,
DropdownMenuTrigger,
} from '@/components/ui/DropdownMenu';
import { Button, type ButtonProps } from '@/components/ui/Button';
import { useAppStatus } from '@/hooks/useAppStatus';
import { castAppConfig } from '@/lib/helpers/castAppConfig';
import type { GetAppCommand } from '@/server/services/app-catalog/commands';
import { InstallModal } from '../InstallModal';
import { useDisclosure } from '@/client/hooks/useDisclosure';
import { useTranslations } from 'next-intl';
import { useAction } from 'next-safe-action/hooks';
import { startAppAction } from '@/actions/app-actions/start-app-action';
import toast from 'react-hot-toast';
import { StopModal } from '../StopModal';
import { InstallModal } from '../InstallModal';
import { ResetAppModal } from '../ResetAppModal';
import { RestartModal } from '../RestartModal';
import { StopModal } from '../StopModal';
import { UninstallModal } from '../UninstallModal';
import { UpdateModal } from '../UpdateModal';
import { ResetAppModal } from '../ResetAppModal';
import { castAppConfig } from '@/lib/helpers/castAppConfig';
import { UpdateSettingsModal } from '../UpdateSettingsModal/UpdateSettingsModal';
import styles from './AppActions.module.scss';
import { useAppStatus } from '@/hooks/useAppStatus';
interface IProps {
app: Awaited<ReturnType<GetAppCommand['execute']>>;

View File

@ -1,11 +1,11 @@
import React from 'react';
import { useTranslations } from 'next-intl';
import type { AppBackupsApiResponse } from '@/api/app-backups/route';
import { AppLogo } from '@/components/AppLogo';
import { AppStatus } from '@/components/AppStatus';
import type { GetAppCommand } from '@/server/services/app-catalog/commands';
import { useTranslations } from 'next-intl';
import type React from 'react';
import { AppActions } from '../AppActions';
import { AppDetailsTabs } from '../AppDetailsTabs';
import { GetAppCommand } from '@/server/services/app-catalog/commands';
import { AppBackupsApiResponse } from '@/api/app-backups/route';
type AppDetailsContainerProps = {
app: Awaited<ReturnType<GetAppCommand['execute']>>;

View File

@ -1,23 +1,23 @@
import { createAppBackupAction } from '@/actions/backup/create-app-backup-action';
import { deleteAppBackupAction } from '@/actions/backup/delete-app-backup';
import { restoreAppBackupAction } from '@/actions/backup/restore-app-backup-action';
import type { AppBackup, AppBackupsApiResponse } from '@/api/app-backups/route';
import { useDisclosure } from '@/client/hooks/useDisclosure';
import { DateFormat } from '@/components/DateFormat/DateFormat';
import { FileSize } from '@/components/FileSize/FileSize';
import { Button } from '@/components/ui/Button';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/Table';
import React from 'react';
import { useAppStatus } from '@/hooks/useAppStatus';
import type { AppInfo } from '@runtipi/shared';
import { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query';
import { useTranslations } from 'next-intl';
import { useAction } from 'next-safe-action/hooks';
import React from 'react';
import toast from 'react-hot-toast';
import { TablePagination } from 'src/app/components/TablePagination/TablePagination';
import { BackupModal } from '../BackupModal';
import { AppInfo } from '@runtipi/shared';
import { useDisclosure } from '@/client/hooks/useDisclosure';
import { Button } from '@/components/ui/Button';
import { useAction } from 'next-safe-action/hooks';
import toast from 'react-hot-toast';
import { RestoreModal } from '../RestoreModal';
import { FileSize } from '@/components/FileSize/FileSize';
import { useAppStatus } from '@/hooks/useAppStatus';
import { DateFormat } from '@/components/DateFormat/DateFormat';
import { useTranslations } from 'next-intl';
import { DeleteBackupModal } from '../DeleteBackupModal/DeleteBackupModal';
import { createAppBackupAction } from '@/actions/backup/create-app-backup-action';
import { restoreAppBackupAction } from '@/actions/backup/restore-app-backup-action';
import { deleteAppBackupAction } from '@/actions/backup/delete-app-backup';
import { RestoreModal } from '../RestoreModal';
type Props = {
info: AppInfo;

View File

@ -1,8 +1,8 @@
import React from 'react';
import { TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useTranslations } from 'next-intl';
import type { AppStatus } from '@runtipi/db';
import { useTranslations } from 'next-intl';
import { useRouter } from 'next/navigation';
import React from 'react';
interface IProps {
status: AppStatus;

View File

@ -1,18 +1,18 @@
'use client';
import { IconAlertCircle, IconExternalLink } from '@tabler/icons-react';
import React from 'react';
import { Tabs, TabsContent } from '@/components/ui/tabs';
import { useTranslations } from 'next-intl';
import { AppInfo } from '@runtipi/shared';
import { DataGrid, DataGridItem } from '@/components/ui/DataGrid';
import { AppLogs } from './AppLogs';
import { useAppStatus } from '@/hooks/useAppStatus';
import { AppBackups } from './AppBackups';
import type { AppBackupsApiResponse } from '@/api/app-backups/route';
import { Markdown } from '@/components/Markdown';
import { DataGrid, DataGridItem } from '@/components/ui/DataGrid';
import { Tabs, TabsContent } from '@/components/ui/tabs';
import { useAppStatus } from '@/hooks/useAppStatus';
import type { AppInfo } from '@runtipi/shared';
import { IconAlertCircle, IconExternalLink } from '@tabler/icons-react';
import { useTranslations } from 'next-intl';
import { useSearchParams } from 'next/navigation';
import React from 'react';
import { AppBackups } from './AppBackups';
import { AppDetailsTabTriggers } from './AppDetailsTabTriggers';
import { AppLogs } from './AppLogs';
interface IProps {
info: AppInfo;

View File

@ -1,9 +1,9 @@
'use client';
import React, { useState, useRef } from 'react';
import { useSocket } from '@/lib/socket/useSocket';
import { LogsTerminal } from 'src/app/components/LogsTerminal/LogsTerminal';
import { ClientOnly } from '@/components/ClientOnly/ClientOnly';
import { useSocket } from '@/lib/socket/useSocket';
import React, { useState, useRef } from 'react';
import { LogsTerminal } from 'src/app/components/LogsTerminal/LogsTerminal';
export const AppLogs = ({ appId }: { appId: string }) => {
let nextId = 0;

View File

@ -1,8 +1,8 @@
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
import { useTranslations } from 'next-intl';
import { AppInfo } from '@runtipi/shared';
import { Button } from '@/components/ui/Button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
import type { AppInfo } from '@runtipi/shared';
import { useTranslations } from 'next-intl';
import type React from 'react';
interface IProps {
info: AppInfo;

View File

@ -1,10 +1,10 @@
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
import { useTranslations } from 'next-intl';
import { Button } from '@/components/ui/Button';
import type { AppBackup } from '@/api/app-backups/route';
import { IconAlertTriangle } from '@tabler/icons-react';
import { useDateFormat } from '@/components/DateFormat/DateFormat';
import { Button } from '@/components/ui/Button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
import { IconAlertTriangle } from '@tabler/icons-react';
import { useTranslations } from 'next-intl';
import type React from 'react';
interface IProps {
backup?: AppBackup | null;

View File

@ -1,8 +1,8 @@
import React from 'react';
import { faker } from '@faker-js/faker';
import type { FormField } from '@runtipi/shared';
import { fromPartial } from '@total-typescript/shoehorn';
import { FormField } from '@runtipi/shared';
import { vi, it, beforeEach, describe, expect } from 'vitest';
import React from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen, waitFor } from '../../../../../../../tests/test-utils';
import { InstallForm } from './InstallForm';

View File

@ -2,18 +2,18 @@ import type React from 'react';
import { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { Tooltip } from 'react-tooltip';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Switch } from '@/components/ui/Switch';
import { useClientSettings } from '@/hooks/useClientSettings';
import type { AppStatus } from '@runtipi/db';
import type { AppInfo, FormField } from '@runtipi/shared';
import clsx from 'clsx';
import { useTranslations } from 'next-intl';
import type { FormField, AppInfo } from '@runtipi/shared';
import { Switch } from '@/components/ui/Switch';
import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import type { AppStatus } from '@runtipi/db';
import { Tooltip } from 'react-tooltip';
import { validateAppConfig } from '../../utils/validators';
import { InstallFormField } from './InstallFormField';
import type { FormValues } from './InstallForm.types';
import { useClientSettings } from '@/hooks/useClientSettings';
import { InstallFormField } from './InstallFormField';
interface IProps {
formFields: FormField[];

View File

@ -1,13 +1,13 @@
import React from 'react';
import { Input } from '@/components/ui/Input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select';
import { Switch } from '@/components/ui/Switch';
import { FormField } from '@runtipi/shared';
import { Tooltip } from 'react-tooltip';
import type { FormField } from '@runtipi/shared';
import clsx from 'clsx';
import { Control, Controller, UseFormRegister } from 'react-hook-form';
import { useTranslations } from 'next-intl';
import { FormValues } from './InstallForm.types';
import React from 'react';
import { type Control, Controller, type UseFormRegister } from 'react-hook-form';
import { Tooltip } from 'react-tooltip';
import type { FormValues } from './InstallForm.types';
type IProps = {
field: FormField;

View File

@ -1,8 +1,8 @@
import type { AppInfo } from '@runtipi/shared';
import React from 'react';
import { AppInfo } from '@runtipi/shared';
import { vi, describe, it, expect } from 'vitest';
import { InstallModal } from './InstallModal';
import { describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen } from '../../../../../../../tests/test-utils';
import { InstallModal } from './InstallModal';
describe('InstallModal', () => {
const app = {

View File

@ -1,13 +1,13 @@
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader } from '@/components/ui/Dialog';
import { useTranslations } from 'next-intl';
import { AppInfo } from '@runtipi/shared';
import { ScrollArea } from '@/components/ui/ScrollArea';
import { InstallForm } from '../InstallForm';
import { useAction } from 'next-safe-action/hooks';
import { installAppAction } from '@/actions/app-actions/install-app-action';
import toast from 'react-hot-toast';
import { Dialog, DialogContent, DialogDescription, DialogHeader } from '@/components/ui/Dialog';
import { ScrollArea } from '@/components/ui/ScrollArea';
import { useAppStatus } from '@/hooks/useAppStatus';
import type { AppInfo } from '@runtipi/shared';
import { useTranslations } from 'next-intl';
import { useAction } from 'next-safe-action/hooks';
import type React from 'react';
import toast from 'react-hot-toast';
import { InstallForm } from '../InstallForm';
interface IProps {
info: AppInfo;

View File

@ -1,13 +1,13 @@
import { IconAlertTriangle } from '@tabler/icons-react';
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
import { useTranslations } from 'next-intl';
import { AppInfo } from '@runtipi/shared';
import { Button } from '@/components/ui/Button';
import { useAction } from 'next-safe-action/hooks';
import { resetAppAction } from '@/actions/app-actions/reset-app-action';
import toast from 'react-hot-toast';
import { Button } from '@/components/ui/Button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
import { useAppStatus } from '@/hooks/useAppStatus';
import type { AppInfo } from '@runtipi/shared';
import { IconAlertTriangle } from '@tabler/icons-react';
import { useTranslations } from 'next-intl';
import { useAction } from 'next-safe-action/hooks';
import type React from 'react';
import toast from 'react-hot-toast';
interface IProps {
info: AppInfo;

View File

@ -1,12 +1,12 @@
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
import { useTranslations } from 'next-intl';
import { AppInfo } from '@runtipi/shared';
import { Button } from '@/components/ui/Button';
import { useAction } from 'next-safe-action/hooks';
import { restartAppAction } from '@/actions/app-actions/restart-app-action';
import toast from 'react-hot-toast';
import { Button } from '@/components/ui/Button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
import { useAppStatus } from '@/hooks/useAppStatus';
import type { AppInfo } from '@runtipi/shared';
import { useTranslations } from 'next-intl';
import { useAction } from 'next-safe-action/hooks';
import type React from 'react';
import toast from 'react-hot-toast';
interface IProps {
info: AppInfo;

View File

@ -1,10 +1,10 @@
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
import { useTranslations } from 'next-intl';
import { Button } from '@/components/ui/Button';
import type { AppBackup } from '@/api/app-backups/route';
import { useDateFormat } from '@/components/DateFormat/DateFormat';
import { Button } from '@/components/ui/Button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
import { IconAlertTriangle } from '@tabler/icons-react';
import { useTranslations } from 'next-intl';
import type React from 'react';
interface IProps {
backup?: AppBackup | null;

View File

@ -1,12 +1,12 @@
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
import { useTranslations } from 'next-intl';
import { AppInfo } from '@runtipi/shared';
import { Button } from '@/components/ui/Button';
import { useAction } from 'next-safe-action/hooks';
import { stopAppAction } from '@/actions/app-actions/stop-app-action';
import toast from 'react-hot-toast';
import { Button } from '@/components/ui/Button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
import { useAppStatus } from '@/hooks/useAppStatus';
import type { AppInfo } from '@runtipi/shared';
import { useTranslations } from 'next-intl';
import { useAction } from 'next-safe-action/hooks';
import type React from 'react';
import toast from 'react-hot-toast';
interface IProps {
info: AppInfo;

View File

@ -1,13 +1,13 @@
import { IconAlertTriangle } from '@tabler/icons-react';
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
import { useTranslations } from 'next-intl';
import { AppInfo } from '@runtipi/shared';
import { Button } from '@/components/ui/Button';
import { useAction } from 'next-safe-action/hooks';
import { uninstallAppAction } from '@/actions/app-actions/uninstall-app-action';
import toast from 'react-hot-toast';
import { Button } from '@/components/ui/Button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
import { useAppStatus } from '@/hooks/useAppStatus';
import type { AppInfo } from '@runtipi/shared';
import { IconAlertTriangle } from '@tabler/icons-react';
import { useTranslations } from 'next-intl';
import { useAction } from 'next-safe-action/hooks';
import type React from 'react';
import toast from 'react-hot-toast';
interface IProps {
info: AppInfo;

View File

@ -1,5 +1,5 @@
import React from 'react';
import { vi, expect, describe, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen } from '../../../../../../../tests/test-utils';
import { UpdateModal } from './UpdateModal';

View File

@ -1,13 +1,14 @@
import React, { useState } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
import { useTranslations } from 'next-intl';
import { AppInfo } from '@runtipi/shared';
import { Button } from '@/components/ui/Button';
import { useAction } from 'next-safe-action/hooks';
import { updateAppAction } from '@/actions/app-actions/update-app-action';
import toast from 'react-hot-toast';
import { useAppStatus } from '@/hooks/useAppStatus';
import { Button } from '@/components/ui/Button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
import { Switch } from '@/components/ui/Switch';
import { useAppStatus } from '@/hooks/useAppStatus';
import type { AppInfo } from '@runtipi/shared';
import { useTranslations } from 'next-intl';
import { useAction } from 'next-safe-action/hooks';
import type React from 'react';
import { useState } from 'react';
import toast from 'react-hot-toast';
interface IProps {
newVersion: string;

View File

@ -1,13 +1,13 @@
import type React from 'react';
import { updateAppConfigAction } from '@/actions/app-actions/update-app-config-action';
import { Dialog, DialogContent, DialogDescription, DialogHeader } from '@/components/ui/Dialog';
import { useTranslations } from 'next-intl';
import type { AppInfo } from '@runtipi/shared';
import { ScrollArea } from '@/components/ui/ScrollArea';
import type { AppStatus } from '@runtipi/db';
import { InstallForm, type FormValues } from '../InstallForm';
import type { AppInfo } from '@runtipi/shared';
import { useTranslations } from 'next-intl';
import { useAction } from 'next-safe-action/hooks';
import { updateAppConfigAction } from '@/actions/app-actions/update-app-config-action';
import type React from 'react';
import toast from 'react-hot-toast';
import { type FormValues, InstallForm } from '../InstallForm';
interface IProps {
info: AppInfo;

View File

@ -1,12 +1,12 @@
import React from 'react';
import { Metadata } from 'next';
import { TipiConfig } from '@/server/core/TipiConfig';
import { ErrorPage } from '@/components/ui/ErrorPage';
import { getTranslator } from '@/lib/get-translator';
import { MessageKey, TranslatedError } from '@/server/utils/errors';
import { appCatalog } from '@/server/services/app-catalog/app-catalog.service';
import { AppDetailsContainer } from './components/AppDetailsContainer/AppDetailsContainer';
import { TipiConfig } from '@/server/core/TipiConfig';
import { appBackupService } from '@/server/services/app-backup/app-backup.service';
import { appCatalog } from '@/server/services/app-catalog/app-catalog.service';
import { type MessageKey, TranslatedError } from '@/server/utils/errors';
import type { Metadata } from 'next';
import React from 'react';
import { AppDetailsContainer } from './components/AppDetailsContainer/AppDetailsContainer';
export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> {
return {

View File

@ -1,5 +1,5 @@
import { FormField } from '@runtipi/shared';
import { describe, it, expect } from 'vitest';
import type { FormField } from '@runtipi/shared';
import { describe, expect, it } from 'vitest';
import { validateAppConfig, validateField } from './validators';
describe('Test: validateField', () => {

View File

@ -1,6 +1,6 @@
import validator from 'validator';
import { useUIStore } from '@/client/state/uiStore';
import type { FormField } from '@runtipi/shared';
import validator from 'validator';
export const validateField = (field: FormField, value: string | undefined | boolean): string | undefined => {
const { translator } = useUIStore.getState();

View File

@ -1,12 +1,12 @@
'use client';
import React from 'react';
import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query';
import type { AppStoreApiResponse } from '@/api/app-store/route';
import { useInfiniteScroll } from '@/client/hooks/useInfiniteScroll';
import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query';
import type React from 'react';
import { EmptyPage } from '../../../../components/EmptyPage';
import { AppStoreTile } from '../AppStoreTile';
import { useAppStoreState } from '../../state/appStoreState';
import { AppStoreTile } from '../AppStoreTile';
interface IProps {
initialData: AppStoreApiResponse;
@ -47,7 +47,7 @@ export const AppStoreTable: React.FC<IProps> = ({ initialData }) => {
placeholderData: keepPreviousData,
});
const apps = data?.pages.map((page) => page.data).flat();
const apps = data?.pages.flatMap((page) => page.data);
const { lastElementRef } = useInfiniteScroll({
fetchNextPage,

View File

@ -1,12 +1,13 @@
'use client';
import clsx from 'clsx';
import React, { useState } from 'react';
import { Input } from '@/components/ui/Input';
import clsx from 'clsx';
import { useTranslations } from 'next-intl';
import styles from './AppStoreTableActions.module.scss';
import type React from 'react';
import { useState } from 'react';
import { useAppStoreState } from '../../state/appStoreState';
import { CategorySelector } from '../CategorySelector';
import styles from './AppStoreTableActions.module.scss';
export const AppStoreTableActions = () => {
const { setCategory, category, search: initialSearch, setSearch } = useAppStoreState();

View File

@ -1,14 +1,14 @@
'use client';
import clsx from 'clsx';
import Link from 'next/link';
import React from 'react';
import { useTranslations } from 'next-intl';
import { AppCategory } from '@runtipi/shared';
import { AppLogo } from '@/components/AppLogo';
import { limitText } from '@/lib/helpers/text-helpers';
import styles from './AppStoreTile.module.scss';
import type { AppCategory } from '@runtipi/shared';
import clsx from 'clsx';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import type React from 'react';
import { colorSchemeForCategory } from '../../helpers/table.helpers';
import styles from './AppStoreTile.module.scss';
type App = {
id: string;

View File

@ -1,7 +1,7 @@
import React from 'react';
import { vi, describe, it, expect } from 'vitest';
import { CategorySelector } from './CategorySelector';
import { describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen } from '../../../../../../tests/test-utils';
import { CategorySelector } from './CategorySelector';
describe('Test: CategorySelector', () => {
it('should render without crashing', () => {

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Select, SelectItem, SelectValue, SelectContent, SelectTrigger } from '@/components/ui/Select';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select';
import type { AppCategory } from '@runtipi/shared';
import { useTranslations } from 'next-intl';
import { AppCategory } from '@runtipi/shared';
import React, { useState } from 'react';
import { iconForCategory } from '../../helpers/table.helpers';
interface Props {

View File

@ -1,8 +1,8 @@
import { limitText } from '@/lib/helpers/text-helpers';
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import { createAppConfig } from '../../../../../server/tests/apps.factory';
import { sortTable } from '../table.helpers';
import { AppTableData } from '../table.types';
import type { AppTableData } from '../table.types';
describe('sortTable function', () => {
const app = createAppConfig({ id: 'a', name: 'a', categories: ['social'] });

View File

@ -1,4 +1,4 @@
import { AppCategory, AppInfo } from '@runtipi/shared';
import type { AppCategory, AppInfo } from '@runtipi/shared';
import {
IconBook,
IconBrain,
@ -16,7 +16,7 @@ import {
IconTool,
IconUsers,
} from '@tabler/icons-react';
import { AppTableData } from './table.types';
import type { AppTableData } from './table.types';
type SortParams = {
data: AppTableData;

View File

@ -1,4 +1,4 @@
import { AppInfo } from '@runtipi/shared';
import type { AppInfo } from '@runtipi/shared';
export type SortableColumns = keyof Pick<AppInfo, 'id'>;

View File

@ -1,8 +1,8 @@
import React from 'react';
import { Metadata } from 'next';
import { getTranslator } from '@/lib/get-translator';
import { AppStoreTable } from './components/AppStoreTable';
import { appCatalog } from '@/server/services/app-catalog/app-catalog.service';
import type { Metadata } from 'next';
import React from 'react';
import { AppStoreTable } from './components/AppStoreTable';
export async function generateMetadata(): Promise<Metadata> {
const translator = await getTranslator();

View File

@ -1,10 +1,10 @@
'use client';
import React from 'react';
import { useDisclosure } from '@/client/hooks/useDisclosure';
import clsx from 'clsx';
import { IconNewSection } from '@tabler/icons-react';
import clsx from 'clsx';
import { useTranslations } from 'next-intl';
import React from 'react';
import { AddLinkModal } from './AddLinkModal';
import styles from './addLink.module.css';

View File

@ -1,17 +1,18 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { addLinkAction } from '@/actions/custom-links/add-link-action';
import { editLinkAction } from '@/actions/custom-links/edit-link-action';
import { Button } from '@/components/ui/Button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
import { Input } from '@/components/ui/Input';
import React, { useEffect } from 'react';
import { addLinkAction } from '@/actions/custom-links/add-link-action';
import { editLinkAction } from '@/actions/custom-links/edit-link-action';
import toast from 'react-hot-toast';
import { useRouter } from 'next/navigation';
import { z } from 'zod';
import { useAction } from 'next-safe-action/hooks';
import { LinkInfo } from '@runtipi/shared';
import { zodResolver } from '@hookform/resolvers/zod';
import type { LinkInfo } from '@runtipi/shared';
import { useTranslations } from 'next-intl';
import { useAction } from 'next-safe-action/hooks';
import { useRouter } from 'next/navigation';
import type React from 'react';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import toast from 'react-hot-toast';
import { z } from 'zod';
type FormValues = { title: string; url: string; description: string | null; iconUrl: string | null };

View File

@ -1,11 +1,11 @@
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
import { deleteLinkAction } from '@/actions/custom-links/delete-link-action';
import { Button } from '@/components/ui/Button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
import { useTranslations } from 'next-intl';
import { useAction } from 'next-safe-action/hooks';
import { useRouter } from 'next/navigation';
import { deleteLinkAction } from '@/actions/custom-links/delete-link-action';
import type React from 'react';
import toast from 'react-hot-toast';
import { useTranslations } from 'next-intl';
type DeleteLinkModalProps = {
isOpen: boolean;

View File

@ -1,12 +1,12 @@
'use client';
import { useDisclosure } from '@/client/hooks/useDisclosure';
import { Button } from '@/components/ui/Button';
import { IconAlertTriangle } from '@tabler/icons-react';
import { useCookies } from 'next-client-cookies';
import { useTranslations } from 'next-intl';
import React from 'react';
import { OffCanvas } from 'src/app/components/OffCanvas/OffCanvas';
import { Button } from '@/components/ui/Button';
import { useTranslations } from 'next-intl';
import { useDisclosure } from '@/client/hooks/useDisclosure';
import { useCookies } from 'next-client-cookies';
import { IconAlertTriangle } from '@tabler/icons-react';
export const AtRiskBanner = ({ isInsecure }: { isInsecure: boolean }) => {
const { isOpen, close } = useDisclosure(isInsecure);

View File

@ -1,21 +1,21 @@
'use client';
import React from 'react';
import { IconBrandGithub, IconHeart, IconLogin, IconLogout, IconMoon, IconSun } from '@tabler/icons-react';
import Image from 'next/image';
import clsx from 'clsx';
import Image from 'next/image';
import Link from 'next/link';
import type React from 'react';
import { Tooltip } from 'react-tooltip';
import { useTranslations } from 'next-intl';
import { useUIStore } from '@/client/state/uiStore';
import { useAction } from 'next-safe-action/hooks';
import { logoutAction } from '@/actions/logout/logout-action';
import Script from 'next/script';
import { useRouter } from 'next/navigation';
import { getLogo } from '@/lib/themes';
import { NavBar } from '../NavBar';
import { useUIStore } from '@/client/state/uiStore';
import { useClientSettings } from '@/hooks/useClientSettings';
import { getLogo } from '@/lib/themes';
import { useTranslations } from 'next-intl';
import { useAction } from 'next-safe-action/hooks';
import { useRouter } from 'next/navigation';
import Script from 'next/script';
import { Tooltip } from 'react-tooltip';
import { NavBar } from '../NavBar';
interface IProps {
isUpdateAvailable?: boolean;

Some files were not shown because too many files have changed in this diff Show More