refactor: unify backend and frontend servers (#3)

* refactor: unify backend and frontend servers

* refactor: correct paths for openapi & drizzle

* refactor: move api-client to client

* fix: drizzle paths

* chore: fix linting issues

* fix: form reset issue
This commit is contained in:
Nico
2025-11-13 20:11:46 +01:00
committed by GitHub
parent 8d7e50508d
commit 95a0d44b45
240 changed files with 5171 additions and 5875 deletions

View File

@@ -12,12 +12,9 @@
!**/build.ts !**/build.ts
!**/components.json !**/components.json
!apps/**/src/** !src/**
!apps/**/drizzle/** !app/**
!apps/**/app/** !public/**
!apps/**/public/**
!packages/**/src/**
# License files and attributions # License files and attributions
!LICENSE !LICENSE

48
.gitignore vendored
View File

@@ -1,47 +1,11 @@
# If you prefer the allow list template instead of the deny list, see community template: .DS_Store
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore /node_modules/
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c` # React Router
*.test /.react-router/
/build/
/dist/
# Code coverage profiles and other test artifacts
*.out
coverage.*
*.coverprofile
profile.cov
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env .env
# Editor/IDE
# .idea/
# .vscode/
ironmount
out/
*.db
tmp/
node_modules/
.env*
.turbo .turbo
mutagen.yml.lock
data/
CLAUDE.md CLAUDE.md

View File

@@ -45,15 +45,12 @@ WORKDIR /app
COPY --from=deps /deps/restic /usr/local/bin/restic COPY --from=deps /deps/restic /usr/local/bin/restic
COPY --from=deps /deps/rclone /usr/local/bin/rclone COPY --from=deps /deps/rclone /usr/local/bin/rclone
COPY ./package.json ./bun.lock ./ COPY ./package.json ./bun.lock ./
COPY ./packages/schemas/package.json ./packages/schemas/package.json
COPY ./apps/client/package.json ./apps/client/package.json
COPY ./apps/server/package.json ./apps/server/package.json
RUN bun install --frozen-lockfile RUN bun install --frozen-lockfile
COPY . . COPY . .
EXPOSE 3000 EXPOSE 4096
CMD ["bun", "run", "dev"] CMD ["bun", "run", "dev"]
@@ -65,13 +62,12 @@ FROM oven/bun:${BUN_VERSION} AS builder
WORKDIR /app WORKDIR /app
COPY ./package.json ./bun.lock ./ COPY ./package.json ./bun.lock ./
RUN bun install --frozen-lockfile
COPY ./packages/schemas/package.json ./packages/schemas/package.json COPY ./packages/schemas/package.json ./packages/schemas/package.json
COPY ./apps/client/package.json ./apps/client/package.json COPY ./apps/client/package.json ./apps/client/package.json
COPY ./apps/server/package.json ./apps/server/package.json COPY ./apps/server/package.json ./apps/server/package.json
RUN bun install --frozen-lockfile
COPY . . COPY . .
RUN bun run build RUN bun run build
@@ -82,16 +78,19 @@ ENV NODE_ENV="production"
WORKDIR /app WORKDIR /app
COPY --from=builder /app/package.json ./
RUN bun install --production --frozen-lockfile
COPY --from=deps /deps/restic /usr/local/bin/restic COPY --from=deps /deps/restic /usr/local/bin/restic
COPY --from=deps /deps/rclone /usr/local/bin/rclone COPY --from=deps /deps/rclone /usr/local/bin/rclone
COPY --from=builder /app/apps/server/dist ./ COPY --from=builder /app/dist/client ./dist/client
COPY --from=builder /app/apps/server/drizzle ./assets/migrations COPY --from=builder /app/dist/server ./dist/server
COPY --from=builder /app/apps/client/dist/client ./assets/frontend COPY --from=builder /app/app/drizzle ./assets/migrations
# Include third-party licenses and attribution # Include third-party licenses and attribution
COPY ./LICENSES ./LICENSES COPY ./LICENSES ./LICENSES
COPY ./NOTICES.md ./NOTICES.md COPY ./NOTICES.md ./NOTICES.md
COPY ./LICENSE ./LICENSE.md COPY ./LICENSE ./LICENSE.md
CMD ["bun", "./index.js"] CMD ["bun", "run", "start"]

View File

@@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
import type { ClientOptions } from "./types.gen"; import { type ClientOptions, type Config, createClient, createConfig } from "./client";
import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from "./client"; import type { ClientOptions as ClientOptions2 } from "./types.gen";
/** /**
* The `createClientConfig()` function will be called on client initialization * The `createClientConfig()` function will be called on client initialization
@@ -11,12 +11,12 @@ import { type Config, type ClientOptions as DefaultClientOptions, createClient,
* `setConfig()`. This is useful for example if you're using Next.js * `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values. * to ensure your client always has the correct values.
*/ */
export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> = ( export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (
override?: Config<DefaultClientOptions & T>, override?: Config<ClientOptions & T>,
) => Config<Required<DefaultClientOptions> & T>; ) => Config<Required<ClientOptions> & T>;
export const client = createClient( export const client = createClient(
createConfig<ClientOptions>({ createConfig<ClientOptions2>({
baseUrl: "http://192.168.2.42:4096", baseUrl: "http://192.168.2.42:4096",
}), }),
); );

View File

@@ -1,6 +1,9 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
import type { Client, Config, ResolvedRequestOptions } from "./types.gen"; import { createSseClient } from "../core/serverSentEvents.gen";
import type { HttpMethod } from "../core/types.gen";
import { getValidRequestBody } from "../core/utils.gen";
import type { Client, Config, RequestOptions, ResolvedRequestOptions } from "./types.gen";
import { import {
buildUrl, buildUrl,
createConfig, createConfig,
@@ -28,7 +31,7 @@ export const createClient = (config: Config = {}): Client => {
const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>(); const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>();
const request: Client["request"] = async (options) => { const beforeRequest = async (options: RequestOptions) => {
const opts = { const opts = {
..._config, ..._config,
...options, ...options,
@@ -48,25 +51,32 @@ export const createClient = (config: Config = {}): Client => {
await opts.requestValidator(opts); await opts.requestValidator(opts);
} }
if (opts.body && opts.bodySerializer) { if (opts.body !== undefined && opts.bodySerializer) {
opts.serializedBody = opts.bodySerializer(opts.body); opts.serializedBody = opts.bodySerializer(opts.body);
} }
// remove Content-Type header if body is empty to avoid sending invalid requests // remove Content-Type header if body is empty to avoid sending invalid requests
if (opts.serializedBody === undefined || opts.serializedBody === "") { if (opts.body === undefined || opts.serializedBody === "") {
opts.headers.delete("Content-Type"); opts.headers.delete("Content-Type");
} }
const url = buildUrl(opts); const url = buildUrl(opts);
return { opts, url };
};
const request: Client["request"] = async (options) => {
// @ts-expect-error
const { opts, url } = await beforeRequest(options);
const requestInit: ReqInit = { const requestInit: ReqInit = {
redirect: "follow", redirect: "follow",
...opts, ...opts,
body: opts.serializedBody, body: getValidRequestBody(opts),
}; };
let request = new Request(url, requestInit); let request = new Request(url, requestInit);
for (const fn of interceptors.request._fns) { for (const fn of interceptors.request.fns) {
if (fn) { if (fn) {
request = await fn(request, opts); request = await fn(request, opts);
} }
@@ -75,9 +85,37 @@ export const createClient = (config: Config = {}): Client => {
// fetch must be assigned here, otherwise it would throw the error: // fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = opts.fetch!; const _fetch = opts.fetch!;
let response = await _fetch(request); let response: Response;
for (const fn of interceptors.response._fns) { try {
response = await _fetch(request);
} catch (error) {
// Handle fetch exceptions (AbortError, network errors, etc.)
let finalError = error;
for (const fn of interceptors.error.fns) {
if (fn) {
finalError = (await fn(error, undefined as any, request, opts)) as unknown;
}
}
finalError = finalError || ({} as unknown);
if (opts.throwOnError) {
throw finalError;
}
// Return error response
return opts.responseStyle === "data"
? undefined
: {
error: finalError,
request,
response: undefined as any,
};
}
for (const fn of interceptors.response.fns) {
if (fn) { if (fn) {
response = await fn(response, request, opts); response = await fn(response, request, opts);
} }
@@ -89,18 +127,36 @@ export const createClient = (config: Config = {}): Client => {
}; };
if (response.ok) { if (response.ok) {
const parseAs =
(opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json";
if (response.status === 204 || response.headers.get("Content-Length") === "0") { if (response.status === 204 || response.headers.get("Content-Length") === "0") {
let emptyData: any;
switch (parseAs) {
case "arrayBuffer":
case "blob":
case "text":
emptyData = await response[parseAs]();
break;
case "formData":
emptyData = new FormData();
break;
case "stream":
emptyData = response.body;
break;
case "json":
default:
emptyData = {};
break;
}
return opts.responseStyle === "data" return opts.responseStyle === "data"
? {} ? emptyData
: { : {
data: {}, data: emptyData,
...result, ...result,
}; };
} }
const parseAs =
(opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json";
let data: any; let data: any;
switch (parseAs) { switch (parseAs) {
case "arrayBuffer": case "arrayBuffer":
@@ -149,7 +205,7 @@ export const createClient = (config: Config = {}): Client => {
const error = jsonError ?? textError; const error = jsonError ?? textError;
let finalError = error; let finalError = error;
for (const fn of interceptors.error._fns) { for (const fn of interceptors.error.fns) {
if (fn) { if (fn) {
finalError = (await fn(error, response, request, opts)) as string; finalError = (await fn(error, response, request, opts)) as string;
} }
@@ -170,20 +226,53 @@ export const createClient = (config: Config = {}): Client => {
}; };
}; };
const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) => request({ ...options, method });
const makeSseFn = (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
const { opts, url } = await beforeRequest(options);
return createSseClient({
...opts,
body: opts.body as BodyInit | null | undefined,
headers: opts.headers as unknown as Record<string, string>,
method,
onRequest: async (url, init) => {
let request = new Request(url, init);
for (const fn of interceptors.request.fns) {
if (fn) {
request = await fn(request, opts);
}
}
return request;
},
url,
});
};
return { return {
buildUrl, buildUrl,
connect: (options) => request({ ...options, method: "CONNECT" }), connect: makeMethodFn("CONNECT"),
delete: (options) => request({ ...options, method: "DELETE" }), delete: makeMethodFn("DELETE"),
get: (options) => request({ ...options, method: "GET" }), get: makeMethodFn("GET"),
getConfig, getConfig,
head: (options) => request({ ...options, method: "HEAD" }), head: makeMethodFn("HEAD"),
interceptors, interceptors,
options: (options) => request({ ...options, method: "OPTIONS" }), options: makeMethodFn("OPTIONS"),
patch: (options) => request({ ...options, method: "PATCH" }), patch: makeMethodFn("PATCH"),
post: (options) => request({ ...options, method: "POST" }), post: makeMethodFn("POST"),
put: (options) => request({ ...options, method: "PUT" }), put: makeMethodFn("PUT"),
request, request,
setConfig, setConfig,
trace: (options) => request({ ...options, method: "TRACE" }), sse: {
}; connect: makeSseFn("CONNECT"),
delete: makeSseFn("DELETE"),
get: makeSseFn("GET"),
head: makeSseFn("HEAD"),
options: makeSseFn("OPTIONS"),
patch: makeSseFn("PATCH"),
post: makeSseFn("POST"),
put: makeSseFn("PUT"),
trace: makeSseFn("TRACE"),
},
trace: makeMethodFn("TRACE"),
} as Client;
}; };

View File

@@ -8,6 +8,7 @@ export {
urlSearchParamsBodySerializer, urlSearchParamsBodySerializer,
} from "../core/bodySerializer.gen"; } from "../core/bodySerializer.gen";
export { buildClientParams } from "../core/params.gen"; export { buildClientParams } from "../core/params.gen";
export { serializeQueryKeyValue } from "../core/queryKeySerializer.gen";
export { createClient } from "./client.gen"; export { createClient } from "./client.gen";
export type { export type {
Client, Client,
@@ -15,7 +16,6 @@ export type {
Config, Config,
CreateClientConfig, CreateClientConfig,
Options, Options,
OptionsLegacyParser,
RequestOptions, RequestOptions,
RequestResult, RequestResult,
ResolvedRequestOptions, ResolvedRequestOptions,

View File

@@ -1,6 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
import type { Auth } from "../core/auth.gen"; import type { Auth } from "../core/auth.gen";
import type { ServerSentEventsOptions, ServerSentEventsResult } from "../core/serverSentEvents.gen";
import type { Client as CoreClient, Config as CoreConfig } from "../core/types.gen"; import type { Client as CoreClient, Config as CoreConfig } from "../core/types.gen";
import type { Middleware } from "./utils.gen"; import type { Middleware } from "./utils.gen";
@@ -19,7 +20,7 @@ export interface Config<T extends ClientOptions = ClientOptions>
* *
* @default globalThis.fetch * @default globalThis.fetch
*/ */
fetch?: (request: Request) => ReturnType<typeof fetch>; fetch?: typeof fetch;
/** /**
* Please don't use the Fetch client for Next.js applications. The `next` * Please don't use the Fetch client for Next.js applications. The `next`
* options won't have any effect. * options won't have any effect.
@@ -51,13 +52,18 @@ export interface Config<T extends ClientOptions = ClientOptions>
} }
export interface RequestOptions< export interface RequestOptions<
TData = unknown,
TResponseStyle extends ResponseStyle = "fields", TResponseStyle extends ResponseStyle = "fields",
ThrowOnError extends boolean = boolean, ThrowOnError extends boolean = boolean,
Url extends string = string, Url extends string = string,
> extends Config<{ > extends Config<{
responseStyle: TResponseStyle; responseStyle: TResponseStyle;
throwOnError: ThrowOnError; throwOnError: ThrowOnError;
}> { }>,
Pick<
ServerSentEventsOptions<TData>,
"onSseError" | "onSseEvent" | "sseDefaultRetryDelay" | "sseMaxRetryAttempts" | "sseMaxRetryDelay"
> {
/** /**
* Any body that you want to add to your request. * Any body that you want to add to your request.
* *
@@ -77,7 +83,7 @@ export interface ResolvedRequestOptions<
TResponseStyle extends ResponseStyle = "fields", TResponseStyle extends ResponseStyle = "fields",
ThrowOnError extends boolean = boolean, ThrowOnError extends boolean = boolean,
Url extends string = string, Url extends string = string,
> extends RequestOptions<TResponseStyle, ThrowOnError, Url> { > extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
serializedBody?: string; serializedBody?: string;
} }
@@ -128,17 +134,26 @@ type MethodFn = <
ThrowOnError extends boolean = false, ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = "fields", TResponseStyle extends ResponseStyle = "fields",
>( >(
options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, "method">, options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type SseFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = "fields",
>(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
) => Promise<ServerSentEventsResult<TData, TError>>;
type RequestFn = < type RequestFn = <
TData = unknown, TData = unknown,
TError = unknown, TError = unknown,
ThrowOnError extends boolean = false, ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = "fields", TResponseStyle extends ResponseStyle = "fields",
>( >(
options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, "method"> & options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method"> &
Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, "method">, Pick<Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, "method">,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type BuildUrlFn = < type BuildUrlFn = <
@@ -149,10 +164,10 @@ type BuildUrlFn = <
url: string; url: string;
}, },
>( >(
options: Pick<TData, "url"> & Options<TData>, options: TData & Options<TData>,
) => string; ) => string;
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & { export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn> & {
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>; interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>;
}; };
@@ -181,21 +196,7 @@ type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
export type Options< export type Options<
TData extends TDataShape = TDataShape, TData extends TDataShape = TDataShape,
ThrowOnError extends boolean = boolean, ThrowOnError extends boolean = boolean,
TResponse = unknown,
TResponseStyle extends ResponseStyle = "fields", TResponseStyle extends ResponseStyle = "fields",
> = OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "path" | "query" | "url"> & Omit<TData, "url">; > = OmitKeys<RequestOptions<TResponse, TResponseStyle, ThrowOnError>, "body" | "path" | "query" | "url"> &
([TData] extends [never] ? unknown : Omit<TData, "url">);
export type OptionsLegacyParser<
TData = unknown,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = "fields",
> = TData extends { body?: any }
? TData extends { headers?: any }
? OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "headers" | "url"> & TData
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "url"> &
TData &
Pick<RequestOptions<TResponseStyle, ThrowOnError>, "headers">
: TData extends { headers?: any }
? OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "headers" | "url"> &
TData &
Pick<RequestOptions<TResponseStyle, ThrowOnError>, "body">
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "url"> & TData;

View File

@@ -1,88 +1,13 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
import { getAuthToken } from "../core/auth.gen"; import { getAuthToken } from "../core/auth.gen";
import type { QuerySerializer, QuerySerializerOptions } from "../core/bodySerializer.gen"; import type { QuerySerializerOptions } from "../core/bodySerializer.gen";
import { jsonBodySerializer } from "../core/bodySerializer.gen"; import { jsonBodySerializer } from "../core/bodySerializer.gen";
import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.gen"; import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.gen";
import { getUrl } from "../core/utils.gen";
import type { Client, ClientOptions, Config, RequestOptions } from "./types.gen"; import type { Client, ClientOptions, Config, RequestOptions } from "./types.gen";
interface PathSerializer { export const createQuerySerializer = <T = unknown>({ parameters = {}, ...args }: QuerySerializerOptions = {}) => {
path: Record<string, unknown>;
url: string;
}
const PATH_PARAM_RE = /\{[^{}]+\}/g;
type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited";
type MatrixStyle = "label" | "matrix" | "simple";
type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
let url = _url;
const matches = _url.match(PATH_PARAM_RE);
if (matches) {
for (const match of matches) {
let explode = false;
let name = match.substring(1, match.length - 1);
let style: ArraySeparatorStyle = "simple";
if (name.endsWith("*")) {
explode = true;
name = name.substring(0, name.length - 1);
}
if (name.startsWith(".")) {
name = name.substring(1);
style = "label";
} else if (name.startsWith(";")) {
name = name.substring(1);
style = "matrix";
}
const value = path[name];
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
url = url.replace(match, serializeArrayParam({ explode, name, style, value }));
continue;
}
if (typeof value === "object") {
url = url.replace(
match,
serializeObjectParam({
explode,
name,
style,
value: value as Record<string, unknown>,
valueOnly: true,
}),
);
continue;
}
if (style === "matrix") {
url = url.replace(
match,
`;${serializePrimitiveParam({
name,
value: value as string,
})}`,
);
continue;
}
const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string));
url = url.replace(match, replaceValue);
}
}
return url;
};
export const createQuerySerializer = <T = unknown>({ allowReserved, array, object }: QuerySerializerOptions = {}) => {
const querySerializer = (queryParams: T) => { const querySerializer = (queryParams: T) => {
const search: string[] = []; const search: string[] = [];
if (queryParams && typeof queryParams === "object") { if (queryParams && typeof queryParams === "object") {
@@ -93,29 +18,31 @@ export const createQuerySerializer = <T = unknown>({ allowReserved, array, objec
continue; continue;
} }
const options = parameters[name] || args;
if (Array.isArray(value)) { if (Array.isArray(value)) {
const serializedArray = serializeArrayParam({ const serializedArray = serializeArrayParam({
allowReserved, allowReserved: options.allowReserved,
explode: true, explode: true,
name, name,
style: "form", style: "form",
value, value,
...array, ...options.array,
}); });
if (serializedArray) search.push(serializedArray); if (serializedArray) search.push(serializedArray);
} else if (typeof value === "object") { } else if (typeof value === "object") {
const serializedObject = serializeObjectParam({ const serializedObject = serializeObjectParam({
allowReserved, allowReserved: options.allowReserved,
explode: true, explode: true,
name, name,
style: "deepObject", style: "deepObject",
value: value as Record<string, unknown>, value: value as Record<string, unknown>,
...object, ...options.object,
}); });
if (serializedObject) search.push(serializedObject); if (serializedObject) search.push(serializedObject);
} else { } else {
const serializedPrimitive = serializePrimitiveParam({ const serializedPrimitive = serializePrimitiveParam({
allowReserved, allowReserved: options.allowReserved,
name, name,
value: value as string, value: value as string,
}); });
@@ -216,8 +143,8 @@ export const setAuthParams = async ({
} }
}; };
export const buildUrl: Client["buildUrl"] = (options) => { export const buildUrl: Client["buildUrl"] = (options) =>
const url = getUrl({ getUrl({
baseUrl: options.baseUrl as string, baseUrl: options.baseUrl as string,
path: options.path, path: options.path,
query: options.query, query: options.query,
@@ -227,36 +154,6 @@ export const buildUrl: Client["buildUrl"] = (options) => {
: createQuerySerializer(options.querySerializer), : createQuerySerializer(options.querySerializer),
url: options.url, url: options.url,
}); });
return url;
};
export const getUrl = ({
baseUrl,
path,
query,
querySerializer,
url: _url,
}: {
baseUrl?: string;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
querySerializer: QuerySerializer;
url: string;
}) => {
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
let url = (baseUrl ?? "") + pathUrl;
if (path) {
url = defaultPathSerializer({ path, url });
}
let search = query ? querySerializer(query) : "";
if (search.startsWith("?")) {
search = search.substring(1);
}
if (search) {
url += `?${search}`;
}
return url;
};
export const mergeConfigs = (a: Config, b: Config): Config => { export const mergeConfigs = (a: Config, b: Config): Config => {
const config = { ...a, ...b }; const config = { ...a, ...b };
@@ -267,14 +164,22 @@ export const mergeConfigs = (a: Config, b: Config): Config => {
return config; return config;
}; };
const headersEntries = (headers: Headers): Array<[string, string]> => {
const entries: Array<[string, string]> = [];
headers.forEach((value, key) => {
entries.push([key, value]);
});
return entries;
};
export const mergeHeaders = (...headers: Array<Required<Config>["headers"] | undefined>): Headers => { export const mergeHeaders = (...headers: Array<Required<Config>["headers"] | undefined>): Headers => {
const mergedHeaders = new Headers(); const mergedHeaders = new Headers();
for (const header of headers) { for (const header of headers) {
if (!header || typeof header !== "object") { if (!header) {
continue; continue;
} }
const iterator = header instanceof Headers ? header.entries() : Object.entries(header); const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header);
for (const [key, value] of iterator) { for (const [key, value] of iterator) {
if (value === null) { if (value === null) {
@@ -305,61 +210,53 @@ type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Pr
type ResInterceptor<Res, Req, Options> = (response: Res, request: Req, options: Options) => Res | Promise<Res>; type ResInterceptor<Res, Req, Options> = (response: Res, request: Req, options: Options) => Res | Promise<Res>;
class Interceptors<Interceptor> { class Interceptors<Interceptor> {
_fns: (Interceptor | null)[]; fns: Array<Interceptor | null> = [];
constructor() { clear(): void {
this._fns = []; this.fns = [];
} }
clear() { eject(id: number | Interceptor): void {
this._fns = []; const index = this.getInterceptorIndex(id);
if (this.fns[index]) {
this.fns[index] = null;
}
}
exists(id: number | Interceptor): boolean {
const index = this.getInterceptorIndex(id);
return Boolean(this.fns[index]);
} }
getInterceptorIndex(id: number | Interceptor): number { getInterceptorIndex(id: number | Interceptor): number {
if (typeof id === "number") { if (typeof id === "number") {
return this._fns[id] ? id : -1; return this.fns[id] ? id : -1;
} else {
return this._fns.indexOf(id);
} }
} return this.fns.indexOf(id);
exists(id: number | Interceptor) {
const index = this.getInterceptorIndex(id);
return !!this._fns[index];
} }
eject(id: number | Interceptor) { update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false {
const index = this.getInterceptorIndex(id); const index = this.getInterceptorIndex(id);
if (this._fns[index]) { if (this.fns[index]) {
this._fns[index] = null; this.fns[index] = fn;
}
}
update(id: number | Interceptor, fn: Interceptor) {
const index = this.getInterceptorIndex(id);
if (this._fns[index]) {
this._fns[index] = fn;
return id; return id;
} else { }
return false; return false;
} }
}
use(fn: Interceptor) { use(fn: Interceptor): number {
this._fns = [...this._fns, fn]; this.fns.push(fn);
return this._fns.length - 1; return this.fns.length - 1;
} }
} }
// `createInterceptors()` response, meant for external use as it does not
// expose internals
export interface Middleware<Req, Res, Err, Options> { export interface Middleware<Req, Res, Err, Options> {
error: Pick<Interceptors<ErrInterceptor<Err, Res, Req, Options>>, "eject" | "use">; error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>;
request: Pick<Interceptors<ReqInterceptor<Req, Options>>, "eject" | "use">; request: Interceptors<ReqInterceptor<Req, Options>>;
response: Pick<Interceptors<ResInterceptor<Res, Req, Options>>, "eject" | "use">; response: Interceptors<ResInterceptor<Res, Req, Options>>;
} }
// do not add `Middleware` as return type so we can use _fns internally export const createInterceptors = <Req, Res, Err, Options>(): Middleware<Req, Res, Err, Options> => ({
export const createInterceptors = <Req, Res, Err, Options>() => ({
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(), error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
request: new Interceptors<ReqInterceptor<Req, Options>>(), request: new Interceptors<ReqInterceptor<Req, Options>>(),
response: new Interceptors<ResInterceptor<Res, Req, Options>>(), response: new Interceptors<ResInterceptor<Res, Req, Options>>(),

View File

@@ -6,11 +6,19 @@ export type QuerySerializer = (query: Record<string, unknown>) => string;
export type BodySerializer = (body: any) => any; export type BodySerializer = (body: any) => any;
export interface QuerySerializerOptions { type QuerySerializerOptionsObject = {
allowReserved?: boolean; allowReserved?: boolean;
array?: SerializerOptions<ArrayStyle>; array?: Partial<SerializerOptions<ArrayStyle>>;
object?: SerializerOptions<ObjectStyle>; object?: Partial<SerializerOptions<ObjectStyle>>;
} };
export type QuerySerializerOptions = QuerySerializerOptionsObject & {
/**
* Per-parameter serialization overrides. When provided, these settings
* override the global array/object settings for specific parameter names.
*/
parameters?: Record<string, QuerySerializerOptionsObject>;
};
const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => {
if (typeof value === "string" || value instanceof Blob) { if (typeof value === "string" || value instanceof Blob) {

View File

@@ -22,6 +22,17 @@ export type Field =
*/ */
key?: string; key?: string;
map?: string; map?: string;
}
| {
/**
* Field name. This is the name we want the user to see and use.
*/
key: string;
/**
* Field mapped name. This is the name we want to use in the request.
* If `in` is omitted, `map` aliases `key` to the transport layer.
*/
map: Slot;
}; };
export interface Fields { export interface Fields {
@@ -41,10 +52,14 @@ const extraPrefixes = Object.entries(extraPrefixesMap);
type KeyMap = Map< type KeyMap = Map<
string, string,
{ | {
in: Slot; in: Slot;
map?: string; map?: string;
} }
| {
in?: never;
map: Slot;
}
>; >;
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
@@ -60,6 +75,10 @@ const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
map: config.map, map: config.map,
}); });
} }
} else if ("key" in config) {
map.set(config.key, {
map: config.map,
});
} else if (config.args) { } else if (config.args) {
buildKeyMap(config.args, map); buildKeyMap(config.args, map);
} }
@@ -108,7 +127,9 @@ export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsCo
if (config.key) { if (config.key) {
const field = map.get(config.key)!; const field = map.get(config.key)!;
const name = field.map || config.key; const name = field.map || config.key;
if (field.in) {
(params[field.in] as Record<string, unknown>)[name] = arg; (params[field.in] as Record<string, unknown>)[name] = arg;
}
} else { } else {
params.body = arg; params.body = arg;
} }
@@ -117,16 +138,20 @@ export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsCo
const field = map.get(key); const field = map.get(key);
if (field) { if (field) {
if (field.in) {
const name = field.map || key; const name = field.map || key;
(params[field.in] as Record<string, unknown>)[name] = value; (params[field.in] as Record<string, unknown>)[name] = value;
} else {
params[field.map] = value;
}
} else { } else {
const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix));
if (extra) { if (extra) {
const [prefix, slot] = extra; const [prefix, slot] = extra;
(params[slot] as Record<string, unknown>)[key.slice(prefix.length)] = value; (params[slot] as Record<string, unknown>)[key.slice(prefix.length)] = value;
} else { } else if ("allowExtra" in config && config.allowExtra) {
for (const [slot, allowed] of Object.entries(config.allowExtra ?? {})) { for (const [slot, allowed] of Object.entries(config.allowExtra)) {
if (allowed) { if (allowed) {
(params[slot as Slot] as Record<string, unknown>)[key] = value; (params[slot as Slot] as Record<string, unknown>)[key] = value;
break; break;

View File

@@ -0,0 +1,111 @@
// This file is auto-generated by @hey-api/openapi-ts
/**
* JSON-friendly union that mirrors what Pinia Colada can hash.
*/
export type JsonValue = null | string | number | boolean | JsonValue[] | { [key: string]: JsonValue };
/**
* Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
*/
export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
if (value === undefined || typeof value === "function" || typeof value === "symbol") {
return undefined;
}
if (typeof value === "bigint") {
return value.toString();
}
if (value instanceof Date) {
return value.toISOString();
}
return value;
};
/**
* Safely stringifies a value and parses it back into a JsonValue.
*/
export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
try {
const json = JSON.stringify(input, queryKeyJsonReplacer);
if (json === undefined) {
return undefined;
}
return JSON.parse(json) as JsonValue;
} catch {
return undefined;
}
};
/**
* Detects plain objects (including objects with a null prototype).
*/
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
if (value === null || typeof value !== "object") {
return false;
}
const prototype = Object.getPrototypeOf(value as object);
return prototype === Object.prototype || prototype === null;
};
/**
* Turns URLSearchParams into a sorted JSON object for deterministic keys.
*/
const serializeSearchParams = (params: URLSearchParams): JsonValue => {
const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b));
const result: Record<string, JsonValue> = {};
for (const [key, value] of entries) {
const existing = result[key];
if (existing === undefined) {
result[key] = value;
continue;
}
if (Array.isArray(existing)) {
(existing as string[]).push(value);
} else {
result[key] = [existing, value];
}
}
return result;
};
/**
* Normalizes any accepted value into a JSON-friendly shape for query keys.
*/
export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => {
if (value === null) {
return null;
}
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
return value;
}
if (value === undefined || typeof value === "function" || typeof value === "symbol") {
return undefined;
}
if (typeof value === "bigint") {
return value.toString();
}
if (value instanceof Date) {
return value.toISOString();
}
if (Array.isArray(value)) {
return stringifyToJsonValue(value);
}
if (typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams) {
return serializeSearchParams(value);
}
if (isPlainObject(value)) {
return stringifyToJsonValue(value);
}
return undefined;
};

View File

@@ -0,0 +1,237 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Config } from "./types.gen";
export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, "method"> &
Pick<Config, "method" | "responseTransformer" | "responseValidator"> & {
/**
* Fetch API implementation. You can use this option to provide a custom
* fetch instance.
*
* @default globalThis.fetch
*/
fetch?: typeof fetch;
/**
* Implementing clients can call request interceptors inside this hook.
*/
onRequest?: (url: string, init: RequestInit) => Promise<Request>;
/**
* Callback invoked when a network or parsing error occurs during streaming.
*
* This option applies only if the endpoint returns a stream of events.
*
* @param error The error that occurred.
*/
onSseError?: (error: unknown) => void;
/**
* Callback invoked when an event is streamed from the server.
*
* This option applies only if the endpoint returns a stream of events.
*
* @param event Event streamed from the server.
* @returns Nothing (void).
*/
onSseEvent?: (event: StreamEvent<TData>) => void;
serializedBody?: RequestInit["body"];
/**
* Default retry delay in milliseconds.
*
* This option applies only if the endpoint returns a stream of events.
*
* @default 3000
*/
sseDefaultRetryDelay?: number;
/**
* Maximum number of retry attempts before giving up.
*/
sseMaxRetryAttempts?: number;
/**
* Maximum retry delay in milliseconds.
*
* Applies only when exponential backoff is used.
*
* This option applies only if the endpoint returns a stream of events.
*
* @default 30000
*/
sseMaxRetryDelay?: number;
/**
* Optional sleep function for retry backoff.
*
* Defaults to using `setTimeout`.
*/
sseSleepFn?: (ms: number) => Promise<void>;
url: string;
};
export interface StreamEvent<TData = unknown> {
data: TData;
event?: string;
id?: string;
retry?: number;
}
export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = {
stream: AsyncGenerator<TData extends Record<string, unknown> ? TData[keyof TData] : TData, TReturn, TNext>;
};
export const createSseClient = <TData = unknown>({
onRequest,
onSseError,
onSseEvent,
responseTransformer,
responseValidator,
sseDefaultRetryDelay,
sseMaxRetryAttempts,
sseMaxRetryDelay,
sseSleepFn,
url,
...options
}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
let lastEventId: string | undefined;
const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
const createStream = async function* () {
let retryDelay: number = sseDefaultRetryDelay ?? 3000;
let attempt = 0;
const signal = options.signal ?? new AbortController().signal;
while (true) {
if (signal.aborted) break;
attempt++;
const headers =
options.headers instanceof Headers
? options.headers
: new Headers(options.headers as Record<string, string> | undefined);
if (lastEventId !== undefined) {
headers.set("Last-Event-ID", lastEventId);
}
try {
const requestInit: RequestInit = {
redirect: "follow",
...options,
body: options.serializedBody,
headers,
signal,
};
let request = new Request(url, requestInit);
if (onRequest) {
request = await onRequest(url, requestInit);
}
// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = options.fetch ?? globalThis.fetch;
const response = await _fetch(request);
if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
if (!response.body) throw new Error("No body in SSE response");
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
const abortHandler = () => {
try {
reader.cancel();
} catch {
// noop
}
};
signal.addEventListener("abort", abortHandler);
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += value;
const chunks = buffer.split("\n\n");
buffer = chunks.pop() ?? "";
for (const chunk of chunks) {
const lines = chunk.split("\n");
const dataLines: Array<string> = [];
let eventName: string | undefined;
for (const line of lines) {
if (line.startsWith("data:")) {
dataLines.push(line.replace(/^data:\s*/, ""));
} else if (line.startsWith("event:")) {
eventName = line.replace(/^event:\s*/, "");
} else if (line.startsWith("id:")) {
lastEventId = line.replace(/^id:\s*/, "");
} else if (line.startsWith("retry:")) {
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10);
if (!Number.isNaN(parsed)) {
retryDelay = parsed;
}
}
}
let data: unknown;
let parsedJson = false;
if (dataLines.length) {
const rawData = dataLines.join("\n");
try {
data = JSON.parse(rawData);
parsedJson = true;
} catch {
data = rawData;
}
}
if (parsedJson) {
if (responseValidator) {
await responseValidator(data);
}
if (responseTransformer) {
data = await responseTransformer(data);
}
}
onSseEvent?.({
data,
event: eventName,
id: lastEventId,
retry: retryDelay,
});
if (dataLines.length) {
yield data as any;
}
}
}
} finally {
signal.removeEventListener("abort", abortHandler);
reader.releaseLock();
}
break; // exit loop on normal completion
} catch (error) {
// connection failed or aborted; retry after delay
onSseError?.(error);
if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) {
break; // stop after firing error
}
// exponential backoff: double retry each attempt, cap at 30s
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000);
await sleep(backoff);
}
}
};
const stream = createStream();
return { stream };
};

View File

@@ -3,24 +3,19 @@
import type { Auth, AuthToken } from "./auth.gen"; import type { Auth, AuthToken } from "./auth.gen";
import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen"; import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen";
export interface Client<RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never> { export type HttpMethod = "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
export type Client<RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, SseFn = never> = {
/** /**
* Returns the final request URL. * Returns the final request URL.
*/ */
buildUrl: BuildUrlFn; buildUrl: BuildUrlFn;
connect: MethodFn;
delete: MethodFn;
get: MethodFn;
getConfig: () => Config; getConfig: () => Config;
head: MethodFn;
options: MethodFn;
patch: MethodFn;
post: MethodFn;
put: MethodFn;
request: RequestFn; request: RequestFn;
setConfig: (config: Config) => Config; setConfig: (config: Config) => Config;
trace: MethodFn; } & {
} [K in HttpMethod]: MethodFn;
} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } });
export interface Config { export interface Config {
/** /**
@@ -47,7 +42,7 @@ export interface Config {
* *
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
*/ */
method?: "CONNECT" | "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT" | "TRACE"; method?: Uppercase<HttpMethod>;
/** /**
* A function for serializing request query parameters. By default, arrays * A function for serializing request query parameters. By default, arrays
* will be exploded in form style, objects will be exploded in deepObject * will be exploded in form style, objects will be exploded in deepObject

View File

@@ -0,0 +1,137 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { BodySerializer, QuerySerializer } from "./bodySerializer.gen";
import {
type ArraySeparatorStyle,
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from "./pathSerializer.gen";
export interface PathSerializer {
path: Record<string, unknown>;
url: string;
}
export const PATH_PARAM_RE = /\{[^{}]+\}/g;
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
let url = _url;
const matches = _url.match(PATH_PARAM_RE);
if (matches) {
for (const match of matches) {
let explode = false;
let name = match.substring(1, match.length - 1);
let style: ArraySeparatorStyle = "simple";
if (name.endsWith("*")) {
explode = true;
name = name.substring(0, name.length - 1);
}
if (name.startsWith(".")) {
name = name.substring(1);
style = "label";
} else if (name.startsWith(";")) {
name = name.substring(1);
style = "matrix";
}
const value = path[name];
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
url = url.replace(match, serializeArrayParam({ explode, name, style, value }));
continue;
}
if (typeof value === "object") {
url = url.replace(
match,
serializeObjectParam({
explode,
name,
style,
value: value as Record<string, unknown>,
valueOnly: true,
}),
);
continue;
}
if (style === "matrix") {
url = url.replace(
match,
`;${serializePrimitiveParam({
name,
value: value as string,
})}`,
);
continue;
}
const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string));
url = url.replace(match, replaceValue);
}
}
return url;
};
export const getUrl = ({
baseUrl,
path,
query,
querySerializer,
url: _url,
}: {
baseUrl?: string;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
querySerializer: QuerySerializer;
url: string;
}) => {
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
let url = (baseUrl ?? "") + pathUrl;
if (path) {
url = defaultPathSerializer({ path, url });
}
let search = query ? querySerializer(query) : "";
if (search.startsWith("?")) {
search = search.substring(1);
}
if (search) {
url += `?${search}`;
}
return url;
};
export function getValidRequestBody(options: {
body?: unknown;
bodySerializer?: BodySerializer | null;
serializedBody?: unknown;
}) {
const hasBody = options.body !== undefined;
const isSerializedBody = hasBody && options.bodySerializer;
if (isSerializedBody) {
if ("serializedBody" in options) {
const hasSerializedBody = options.serializedBody !== undefined && options.serializedBody !== "";
return hasSerializedBody ? options.serializedBody : null;
}
// not all clients implement a serializedBody property (i.e. client-axios)
return options.body !== "" ? options.body : null;
}
// plain/text body
if (hasBody) {
return options.body;
}
// no body was provided
return undefined;
}

View File

@@ -1,3 +1,4 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
export * from "./types.gen";
export type * from "./types.gen";
export * from "./sdk.gen"; export * from "./sdk.gen";

View File

@@ -1,92 +1,92 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
import type { Options as ClientOptions, TDataShape, Client } from "./client"; import type { Client, Options as Options2, TDataShape } from "./client";
import { client } from "./client.gen";
import type { import type {
RegisterData, BrowseFilesystemData,
RegisterResponses, BrowseFilesystemResponses,
ChangePasswordData,
ChangePasswordResponses,
CreateBackupScheduleData,
CreateBackupScheduleResponses,
CreateRepositoryData,
CreateRepositoryResponses,
CreateVolumeData,
CreateVolumeResponses,
DeleteBackupScheduleData,
DeleteBackupScheduleResponses,
DeleteRepositoryData,
DeleteRepositoryResponses,
DeleteVolumeData,
DeleteVolumeResponses,
DoctorRepositoryData,
DoctorRepositoryResponses,
DownloadResticPasswordData,
DownloadResticPasswordResponses,
GetBackupScheduleData,
GetBackupScheduleForVolumeData,
GetBackupScheduleForVolumeResponses,
GetBackupScheduleResponses,
GetContainersUsingVolumeData,
GetContainersUsingVolumeErrors,
GetContainersUsingVolumeResponses,
GetMeData,
GetMeResponses,
GetRepositoryData,
GetRepositoryResponses,
GetSnapshotDetailsData,
GetSnapshotDetailsResponses,
GetStatusData,
GetStatusResponses,
GetSystemInfoData,
GetSystemInfoResponses,
GetVolumeData,
GetVolumeErrors,
GetVolumeResponses,
HealthCheckVolumeData,
HealthCheckVolumeErrors,
HealthCheckVolumeResponses,
ListBackupSchedulesData,
ListBackupSchedulesResponses,
ListFilesData,
ListFilesResponses,
ListRcloneRemotesData,
ListRcloneRemotesResponses,
ListRepositoriesData,
ListRepositoriesResponses,
ListSnapshotFilesData,
ListSnapshotFilesResponses,
ListSnapshotsData,
ListSnapshotsResponses,
ListVolumesData,
ListVolumesResponses,
LoginData, LoginData,
LoginResponses, LoginResponses,
LogoutData, LogoutData,
LogoutResponses, LogoutResponses,
GetMeData,
GetMeResponses,
GetStatusData,
GetStatusResponses,
ChangePasswordData,
ChangePasswordResponses,
ListVolumesData,
ListVolumesResponses,
CreateVolumeData,
CreateVolumeResponses,
TestConnectionData,
TestConnectionResponses,
DeleteVolumeData,
DeleteVolumeResponses,
GetVolumeData,
GetVolumeResponses,
GetVolumeErrors,
UpdateVolumeData,
UpdateVolumeResponses,
UpdateVolumeErrors,
GetContainersUsingVolumeData,
GetContainersUsingVolumeResponses,
GetContainersUsingVolumeErrors,
MountVolumeData, MountVolumeData,
MountVolumeResponses, MountVolumeResponses,
UnmountVolumeData, RegisterData,
UnmountVolumeResponses, RegisterResponses,
HealthCheckVolumeData,
HealthCheckVolumeResponses,
HealthCheckVolumeErrors,
ListFilesData,
ListFilesResponses,
BrowseFilesystemData,
BrowseFilesystemResponses,
ListRepositoriesData,
ListRepositoriesResponses,
CreateRepositoryData,
CreateRepositoryResponses,
ListRcloneRemotesData,
ListRcloneRemotesResponses,
DeleteRepositoryData,
DeleteRepositoryResponses,
GetRepositoryData,
GetRepositoryResponses,
ListSnapshotsData,
ListSnapshotsResponses,
GetSnapshotDetailsData,
GetSnapshotDetailsResponses,
ListSnapshotFilesData,
ListSnapshotFilesResponses,
RestoreSnapshotData, RestoreSnapshotData,
RestoreSnapshotResponses, RestoreSnapshotResponses,
DoctorRepositoryData,
DoctorRepositoryResponses,
ListBackupSchedulesData,
ListBackupSchedulesResponses,
CreateBackupScheduleData,
CreateBackupScheduleResponses,
DeleteBackupScheduleData,
DeleteBackupScheduleResponses,
GetBackupScheduleData,
GetBackupScheduleResponses,
UpdateBackupScheduleData,
UpdateBackupScheduleResponses,
GetBackupScheduleForVolumeData,
GetBackupScheduleForVolumeResponses,
RunBackupNowData, RunBackupNowData,
RunBackupNowResponses, RunBackupNowResponses,
StopBackupData, StopBackupData,
StopBackupResponses,
StopBackupErrors, StopBackupErrors,
GetSystemInfoData, StopBackupResponses,
GetSystemInfoResponses, TestConnectionData,
DownloadResticPasswordData, TestConnectionResponses,
DownloadResticPasswordResponses, UnmountVolumeData,
UnmountVolumeResponses,
UpdateBackupScheduleData,
UpdateBackupScheduleResponses,
UpdateVolumeData,
UpdateVolumeErrors,
UpdateVolumeResponses,
} from "./types.gen"; } from "./types.gen";
import { client as _heyApiClient } from "./client.gen";
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions< export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<
TData, TData,
ThrowOnError ThrowOnError
> & { > & {
@@ -107,7 +107,7 @@ export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends
* Register a new user * Register a new user
*/ */
export const register = <ThrowOnError extends boolean = false>(options?: Options<RegisterData, ThrowOnError>) => { export const register = <ThrowOnError extends boolean = false>(options?: Options<RegisterData, ThrowOnError>) => {
return (options?.client ?? _heyApiClient).post<RegisterResponses, unknown, ThrowOnError>({ return (options?.client ?? client).post<RegisterResponses, unknown, ThrowOnError>({
url: "/api/v1/auth/register", url: "/api/v1/auth/register",
...options, ...options,
headers: { headers: {
@@ -121,7 +121,7 @@ export const register = <ThrowOnError extends boolean = false>(options?: Options
* Login with username and password * Login with username and password
*/ */
export const login = <ThrowOnError extends boolean = false>(options?: Options<LoginData, ThrowOnError>) => { export const login = <ThrowOnError extends boolean = false>(options?: Options<LoginData, ThrowOnError>) => {
return (options?.client ?? _heyApiClient).post<LoginResponses, unknown, ThrowOnError>({ return (options?.client ?? client).post<LoginResponses, unknown, ThrowOnError>({
url: "/api/v1/auth/login", url: "/api/v1/auth/login",
...options, ...options,
headers: { headers: {
@@ -135,7 +135,7 @@ export const login = <ThrowOnError extends boolean = false>(options?: Options<Lo
* Logout current user * Logout current user
*/ */
export const logout = <ThrowOnError extends boolean = false>(options?: Options<LogoutData, ThrowOnError>) => { export const logout = <ThrowOnError extends boolean = false>(options?: Options<LogoutData, ThrowOnError>) => {
return (options?.client ?? _heyApiClient).post<LogoutResponses, unknown, ThrowOnError>({ return (options?.client ?? client).post<LogoutResponses, unknown, ThrowOnError>({
url: "/api/v1/auth/logout", url: "/api/v1/auth/logout",
...options, ...options,
}); });
@@ -145,7 +145,7 @@ export const logout = <ThrowOnError extends boolean = false>(options?: Options<L
* Get current authenticated user * Get current authenticated user
*/ */
export const getMe = <ThrowOnError extends boolean = false>(options?: Options<GetMeData, ThrowOnError>) => { export const getMe = <ThrowOnError extends boolean = false>(options?: Options<GetMeData, ThrowOnError>) => {
return (options?.client ?? _heyApiClient).get<GetMeResponses, unknown, ThrowOnError>({ return (options?.client ?? client).get<GetMeResponses, unknown, ThrowOnError>({
url: "/api/v1/auth/me", url: "/api/v1/auth/me",
...options, ...options,
}); });
@@ -155,7 +155,7 @@ export const getMe = <ThrowOnError extends boolean = false>(options?: Options<Ge
* Get authentication system status * Get authentication system status
*/ */
export const getStatus = <ThrowOnError extends boolean = false>(options?: Options<GetStatusData, ThrowOnError>) => { export const getStatus = <ThrowOnError extends boolean = false>(options?: Options<GetStatusData, ThrowOnError>) => {
return (options?.client ?? _heyApiClient).get<GetStatusResponses, unknown, ThrowOnError>({ return (options?.client ?? client).get<GetStatusResponses, unknown, ThrowOnError>({
url: "/api/v1/auth/status", url: "/api/v1/auth/status",
...options, ...options,
}); });
@@ -167,7 +167,7 @@ export const getStatus = <ThrowOnError extends boolean = false>(options?: Option
export const changePassword = <ThrowOnError extends boolean = false>( export const changePassword = <ThrowOnError extends boolean = false>(
options?: Options<ChangePasswordData, ThrowOnError>, options?: Options<ChangePasswordData, ThrowOnError>,
) => { ) => {
return (options?.client ?? _heyApiClient).post<ChangePasswordResponses, unknown, ThrowOnError>({ return (options?.client ?? client).post<ChangePasswordResponses, unknown, ThrowOnError>({
url: "/api/v1/auth/change-password", url: "/api/v1/auth/change-password",
...options, ...options,
headers: { headers: {
@@ -181,7 +181,7 @@ export const changePassword = <ThrowOnError extends boolean = false>(
* List all volumes * List all volumes
*/ */
export const listVolumes = <ThrowOnError extends boolean = false>(options?: Options<ListVolumesData, ThrowOnError>) => { export const listVolumes = <ThrowOnError extends boolean = false>(options?: Options<ListVolumesData, ThrowOnError>) => {
return (options?.client ?? _heyApiClient).get<ListVolumesResponses, unknown, ThrowOnError>({ return (options?.client ?? client).get<ListVolumesResponses, unknown, ThrowOnError>({
url: "/api/v1/volumes", url: "/api/v1/volumes",
...options, ...options,
}); });
@@ -193,7 +193,7 @@ export const listVolumes = <ThrowOnError extends boolean = false>(options?: Opti
export const createVolume = <ThrowOnError extends boolean = false>( export const createVolume = <ThrowOnError extends boolean = false>(
options?: Options<CreateVolumeData, ThrowOnError>, options?: Options<CreateVolumeData, ThrowOnError>,
) => { ) => {
return (options?.client ?? _heyApiClient).post<CreateVolumeResponses, unknown, ThrowOnError>({ return (options?.client ?? client).post<CreateVolumeResponses, unknown, ThrowOnError>({
url: "/api/v1/volumes", url: "/api/v1/volumes",
...options, ...options,
headers: { headers: {
@@ -209,7 +209,7 @@ export const createVolume = <ThrowOnError extends boolean = false>(
export const testConnection = <ThrowOnError extends boolean = false>( export const testConnection = <ThrowOnError extends boolean = false>(
options?: Options<TestConnectionData, ThrowOnError>, options?: Options<TestConnectionData, ThrowOnError>,
) => { ) => {
return (options?.client ?? _heyApiClient).post<TestConnectionResponses, unknown, ThrowOnError>({ return (options?.client ?? client).post<TestConnectionResponses, unknown, ThrowOnError>({
url: "/api/v1/volumes/test-connection", url: "/api/v1/volumes/test-connection",
...options, ...options,
headers: { headers: {
@@ -225,7 +225,7 @@ export const testConnection = <ThrowOnError extends boolean = false>(
export const deleteVolume = <ThrowOnError extends boolean = false>( export const deleteVolume = <ThrowOnError extends boolean = false>(
options: Options<DeleteVolumeData, ThrowOnError>, options: Options<DeleteVolumeData, ThrowOnError>,
) => { ) => {
return (options.client ?? _heyApiClient).delete<DeleteVolumeResponses, unknown, ThrowOnError>({ return (options.client ?? client).delete<DeleteVolumeResponses, unknown, ThrowOnError>({
url: "/api/v1/volumes/{name}", url: "/api/v1/volumes/{name}",
...options, ...options,
}); });
@@ -235,7 +235,7 @@ export const deleteVolume = <ThrowOnError extends boolean = false>(
* Get a volume by name * Get a volume by name
*/ */
export const getVolume = <ThrowOnError extends boolean = false>(options: Options<GetVolumeData, ThrowOnError>) => { export const getVolume = <ThrowOnError extends boolean = false>(options: Options<GetVolumeData, ThrowOnError>) => {
return (options.client ?? _heyApiClient).get<GetVolumeResponses, GetVolumeErrors, ThrowOnError>({ return (options.client ?? client).get<GetVolumeResponses, GetVolumeErrors, ThrowOnError>({
url: "/api/v1/volumes/{name}", url: "/api/v1/volumes/{name}",
...options, ...options,
}); });
@@ -247,7 +247,7 @@ export const getVolume = <ThrowOnError extends boolean = false>(options: Options
export const updateVolume = <ThrowOnError extends boolean = false>( export const updateVolume = <ThrowOnError extends boolean = false>(
options: Options<UpdateVolumeData, ThrowOnError>, options: Options<UpdateVolumeData, ThrowOnError>,
) => { ) => {
return (options.client ?? _heyApiClient).put<UpdateVolumeResponses, UpdateVolumeErrors, ThrowOnError>({ return (options.client ?? client).put<UpdateVolumeResponses, UpdateVolumeErrors, ThrowOnError>({
url: "/api/v1/volumes/{name}", url: "/api/v1/volumes/{name}",
...options, ...options,
headers: { headers: {
@@ -263,7 +263,7 @@ export const updateVolume = <ThrowOnError extends boolean = false>(
export const getContainersUsingVolume = <ThrowOnError extends boolean = false>( export const getContainersUsingVolume = <ThrowOnError extends boolean = false>(
options: Options<GetContainersUsingVolumeData, ThrowOnError>, options: Options<GetContainersUsingVolumeData, ThrowOnError>,
) => { ) => {
return (options.client ?? _heyApiClient).get< return (options.client ?? client).get<
GetContainersUsingVolumeResponses, GetContainersUsingVolumeResponses,
GetContainersUsingVolumeErrors, GetContainersUsingVolumeErrors,
ThrowOnError ThrowOnError
@@ -277,7 +277,7 @@ export const getContainersUsingVolume = <ThrowOnError extends boolean = false>(
* Mount a volume * Mount a volume
*/ */
export const mountVolume = <ThrowOnError extends boolean = false>(options: Options<MountVolumeData, ThrowOnError>) => { export const mountVolume = <ThrowOnError extends boolean = false>(options: Options<MountVolumeData, ThrowOnError>) => {
return (options.client ?? _heyApiClient).post<MountVolumeResponses, unknown, ThrowOnError>({ return (options.client ?? client).post<MountVolumeResponses, unknown, ThrowOnError>({
url: "/api/v1/volumes/{name}/mount", url: "/api/v1/volumes/{name}/mount",
...options, ...options,
}); });
@@ -289,7 +289,7 @@ export const mountVolume = <ThrowOnError extends boolean = false>(options: Optio
export const unmountVolume = <ThrowOnError extends boolean = false>( export const unmountVolume = <ThrowOnError extends boolean = false>(
options: Options<UnmountVolumeData, ThrowOnError>, options: Options<UnmountVolumeData, ThrowOnError>,
) => { ) => {
return (options.client ?? _heyApiClient).post<UnmountVolumeResponses, unknown, ThrowOnError>({ return (options.client ?? client).post<UnmountVolumeResponses, unknown, ThrowOnError>({
url: "/api/v1/volumes/{name}/unmount", url: "/api/v1/volumes/{name}/unmount",
...options, ...options,
}); });
@@ -301,7 +301,7 @@ export const unmountVolume = <ThrowOnError extends boolean = false>(
export const healthCheckVolume = <ThrowOnError extends boolean = false>( export const healthCheckVolume = <ThrowOnError extends boolean = false>(
options: Options<HealthCheckVolumeData, ThrowOnError>, options: Options<HealthCheckVolumeData, ThrowOnError>,
) => { ) => {
return (options.client ?? _heyApiClient).post<HealthCheckVolumeResponses, HealthCheckVolumeErrors, ThrowOnError>({ return (options.client ?? client).post<HealthCheckVolumeResponses, HealthCheckVolumeErrors, ThrowOnError>({
url: "/api/v1/volumes/{name}/health-check", url: "/api/v1/volumes/{name}/health-check",
...options, ...options,
}); });
@@ -311,7 +311,7 @@ export const healthCheckVolume = <ThrowOnError extends boolean = false>(
* List files in a volume directory * List files in a volume directory
*/ */
export const listFiles = <ThrowOnError extends boolean = false>(options: Options<ListFilesData, ThrowOnError>) => { export const listFiles = <ThrowOnError extends boolean = false>(options: Options<ListFilesData, ThrowOnError>) => {
return (options.client ?? _heyApiClient).get<ListFilesResponses, unknown, ThrowOnError>({ return (options.client ?? client).get<ListFilesResponses, unknown, ThrowOnError>({
url: "/api/v1/volumes/{name}/files", url: "/api/v1/volumes/{name}/files",
...options, ...options,
}); });
@@ -323,7 +323,7 @@ export const listFiles = <ThrowOnError extends boolean = false>(options: Options
export const browseFilesystem = <ThrowOnError extends boolean = false>( export const browseFilesystem = <ThrowOnError extends boolean = false>(
options?: Options<BrowseFilesystemData, ThrowOnError>, options?: Options<BrowseFilesystemData, ThrowOnError>,
) => { ) => {
return (options?.client ?? _heyApiClient).get<BrowseFilesystemResponses, unknown, ThrowOnError>({ return (options?.client ?? client).get<BrowseFilesystemResponses, unknown, ThrowOnError>({
url: "/api/v1/volumes/filesystem/browse", url: "/api/v1/volumes/filesystem/browse",
...options, ...options,
}); });
@@ -335,7 +335,7 @@ export const browseFilesystem = <ThrowOnError extends boolean = false>(
export const listRepositories = <ThrowOnError extends boolean = false>( export const listRepositories = <ThrowOnError extends boolean = false>(
options?: Options<ListRepositoriesData, ThrowOnError>, options?: Options<ListRepositoriesData, ThrowOnError>,
) => { ) => {
return (options?.client ?? _heyApiClient).get<ListRepositoriesResponses, unknown, ThrowOnError>({ return (options?.client ?? client).get<ListRepositoriesResponses, unknown, ThrowOnError>({
url: "/api/v1/repositories", url: "/api/v1/repositories",
...options, ...options,
}); });
@@ -347,7 +347,7 @@ export const listRepositories = <ThrowOnError extends boolean = false>(
export const createRepository = <ThrowOnError extends boolean = false>( export const createRepository = <ThrowOnError extends boolean = false>(
options?: Options<CreateRepositoryData, ThrowOnError>, options?: Options<CreateRepositoryData, ThrowOnError>,
) => { ) => {
return (options?.client ?? _heyApiClient).post<CreateRepositoryResponses, unknown, ThrowOnError>({ return (options?.client ?? client).post<CreateRepositoryResponses, unknown, ThrowOnError>({
url: "/api/v1/repositories", url: "/api/v1/repositories",
...options, ...options,
headers: { headers: {
@@ -363,7 +363,7 @@ export const createRepository = <ThrowOnError extends boolean = false>(
export const listRcloneRemotes = <ThrowOnError extends boolean = false>( export const listRcloneRemotes = <ThrowOnError extends boolean = false>(
options?: Options<ListRcloneRemotesData, ThrowOnError>, options?: Options<ListRcloneRemotesData, ThrowOnError>,
) => { ) => {
return (options?.client ?? _heyApiClient).get<ListRcloneRemotesResponses, unknown, ThrowOnError>({ return (options?.client ?? client).get<ListRcloneRemotesResponses, unknown, ThrowOnError>({
url: "/api/v1/repositories/rclone-remotes", url: "/api/v1/repositories/rclone-remotes",
...options, ...options,
}); });
@@ -375,7 +375,7 @@ export const listRcloneRemotes = <ThrowOnError extends boolean = false>(
export const deleteRepository = <ThrowOnError extends boolean = false>( export const deleteRepository = <ThrowOnError extends boolean = false>(
options: Options<DeleteRepositoryData, ThrowOnError>, options: Options<DeleteRepositoryData, ThrowOnError>,
) => { ) => {
return (options.client ?? _heyApiClient).delete<DeleteRepositoryResponses, unknown, ThrowOnError>({ return (options.client ?? client).delete<DeleteRepositoryResponses, unknown, ThrowOnError>({
url: "/api/v1/repositories/{name}", url: "/api/v1/repositories/{name}",
...options, ...options,
}); });
@@ -387,7 +387,7 @@ export const deleteRepository = <ThrowOnError extends boolean = false>(
export const getRepository = <ThrowOnError extends boolean = false>( export const getRepository = <ThrowOnError extends boolean = false>(
options: Options<GetRepositoryData, ThrowOnError>, options: Options<GetRepositoryData, ThrowOnError>,
) => { ) => {
return (options.client ?? _heyApiClient).get<GetRepositoryResponses, unknown, ThrowOnError>({ return (options.client ?? client).get<GetRepositoryResponses, unknown, ThrowOnError>({
url: "/api/v1/repositories/{name}", url: "/api/v1/repositories/{name}",
...options, ...options,
}); });
@@ -399,7 +399,7 @@ export const getRepository = <ThrowOnError extends boolean = false>(
export const listSnapshots = <ThrowOnError extends boolean = false>( export const listSnapshots = <ThrowOnError extends boolean = false>(
options: Options<ListSnapshotsData, ThrowOnError>, options: Options<ListSnapshotsData, ThrowOnError>,
) => { ) => {
return (options.client ?? _heyApiClient).get<ListSnapshotsResponses, unknown, ThrowOnError>({ return (options.client ?? client).get<ListSnapshotsResponses, unknown, ThrowOnError>({
url: "/api/v1/repositories/{name}/snapshots", url: "/api/v1/repositories/{name}/snapshots",
...options, ...options,
}); });
@@ -411,7 +411,7 @@ export const listSnapshots = <ThrowOnError extends boolean = false>(
export const getSnapshotDetails = <ThrowOnError extends boolean = false>( export const getSnapshotDetails = <ThrowOnError extends boolean = false>(
options: Options<GetSnapshotDetailsData, ThrowOnError>, options: Options<GetSnapshotDetailsData, ThrowOnError>,
) => { ) => {
return (options.client ?? _heyApiClient).get<GetSnapshotDetailsResponses, unknown, ThrowOnError>({ return (options.client ?? client).get<GetSnapshotDetailsResponses, unknown, ThrowOnError>({
url: "/api/v1/repositories/{name}/snapshots/{snapshotId}", url: "/api/v1/repositories/{name}/snapshots/{snapshotId}",
...options, ...options,
}); });
@@ -423,7 +423,7 @@ export const getSnapshotDetails = <ThrowOnError extends boolean = false>(
export const listSnapshotFiles = <ThrowOnError extends boolean = false>( export const listSnapshotFiles = <ThrowOnError extends boolean = false>(
options: Options<ListSnapshotFilesData, ThrowOnError>, options: Options<ListSnapshotFilesData, ThrowOnError>,
) => { ) => {
return (options.client ?? _heyApiClient).get<ListSnapshotFilesResponses, unknown, ThrowOnError>({ return (options.client ?? client).get<ListSnapshotFilesResponses, unknown, ThrowOnError>({
url: "/api/v1/repositories/{name}/snapshots/{snapshotId}/files", url: "/api/v1/repositories/{name}/snapshots/{snapshotId}/files",
...options, ...options,
}); });
@@ -435,7 +435,7 @@ export const listSnapshotFiles = <ThrowOnError extends boolean = false>(
export const restoreSnapshot = <ThrowOnError extends boolean = false>( export const restoreSnapshot = <ThrowOnError extends boolean = false>(
options: Options<RestoreSnapshotData, ThrowOnError>, options: Options<RestoreSnapshotData, ThrowOnError>,
) => { ) => {
return (options.client ?? _heyApiClient).post<RestoreSnapshotResponses, unknown, ThrowOnError>({ return (options.client ?? client).post<RestoreSnapshotResponses, unknown, ThrowOnError>({
url: "/api/v1/repositories/{name}/restore", url: "/api/v1/repositories/{name}/restore",
...options, ...options,
headers: { headers: {
@@ -451,7 +451,7 @@ export const restoreSnapshot = <ThrowOnError extends boolean = false>(
export const doctorRepository = <ThrowOnError extends boolean = false>( export const doctorRepository = <ThrowOnError extends boolean = false>(
options: Options<DoctorRepositoryData, ThrowOnError>, options: Options<DoctorRepositoryData, ThrowOnError>,
) => { ) => {
return (options.client ?? _heyApiClient).post<DoctorRepositoryResponses, unknown, ThrowOnError>({ return (options.client ?? client).post<DoctorRepositoryResponses, unknown, ThrowOnError>({
url: "/api/v1/repositories/{name}/doctor", url: "/api/v1/repositories/{name}/doctor",
...options, ...options,
}); });
@@ -463,7 +463,7 @@ export const doctorRepository = <ThrowOnError extends boolean = false>(
export const listBackupSchedules = <ThrowOnError extends boolean = false>( export const listBackupSchedules = <ThrowOnError extends boolean = false>(
options?: Options<ListBackupSchedulesData, ThrowOnError>, options?: Options<ListBackupSchedulesData, ThrowOnError>,
) => { ) => {
return (options?.client ?? _heyApiClient).get<ListBackupSchedulesResponses, unknown, ThrowOnError>({ return (options?.client ?? client).get<ListBackupSchedulesResponses, unknown, ThrowOnError>({
url: "/api/v1/backups", url: "/api/v1/backups",
...options, ...options,
}); });
@@ -475,7 +475,7 @@ export const listBackupSchedules = <ThrowOnError extends boolean = false>(
export const createBackupSchedule = <ThrowOnError extends boolean = false>( export const createBackupSchedule = <ThrowOnError extends boolean = false>(
options?: Options<CreateBackupScheduleData, ThrowOnError>, options?: Options<CreateBackupScheduleData, ThrowOnError>,
) => { ) => {
return (options?.client ?? _heyApiClient).post<CreateBackupScheduleResponses, unknown, ThrowOnError>({ return (options?.client ?? client).post<CreateBackupScheduleResponses, unknown, ThrowOnError>({
url: "/api/v1/backups", url: "/api/v1/backups",
...options, ...options,
headers: { headers: {
@@ -491,7 +491,7 @@ export const createBackupSchedule = <ThrowOnError extends boolean = false>(
export const deleteBackupSchedule = <ThrowOnError extends boolean = false>( export const deleteBackupSchedule = <ThrowOnError extends boolean = false>(
options: Options<DeleteBackupScheduleData, ThrowOnError>, options: Options<DeleteBackupScheduleData, ThrowOnError>,
) => { ) => {
return (options.client ?? _heyApiClient).delete<DeleteBackupScheduleResponses, unknown, ThrowOnError>({ return (options.client ?? client).delete<DeleteBackupScheduleResponses, unknown, ThrowOnError>({
url: "/api/v1/backups/{scheduleId}", url: "/api/v1/backups/{scheduleId}",
...options, ...options,
}); });
@@ -503,7 +503,7 @@ export const deleteBackupSchedule = <ThrowOnError extends boolean = false>(
export const getBackupSchedule = <ThrowOnError extends boolean = false>( export const getBackupSchedule = <ThrowOnError extends boolean = false>(
options: Options<GetBackupScheduleData, ThrowOnError>, options: Options<GetBackupScheduleData, ThrowOnError>,
) => { ) => {
return (options.client ?? _heyApiClient).get<GetBackupScheduleResponses, unknown, ThrowOnError>({ return (options.client ?? client).get<GetBackupScheduleResponses, unknown, ThrowOnError>({
url: "/api/v1/backups/{scheduleId}", url: "/api/v1/backups/{scheduleId}",
...options, ...options,
}); });
@@ -515,7 +515,7 @@ export const getBackupSchedule = <ThrowOnError extends boolean = false>(
export const updateBackupSchedule = <ThrowOnError extends boolean = false>( export const updateBackupSchedule = <ThrowOnError extends boolean = false>(
options: Options<UpdateBackupScheduleData, ThrowOnError>, options: Options<UpdateBackupScheduleData, ThrowOnError>,
) => { ) => {
return (options.client ?? _heyApiClient).patch<UpdateBackupScheduleResponses, unknown, ThrowOnError>({ return (options.client ?? client).patch<UpdateBackupScheduleResponses, unknown, ThrowOnError>({
url: "/api/v1/backups/{scheduleId}", url: "/api/v1/backups/{scheduleId}",
...options, ...options,
headers: { headers: {
@@ -531,7 +531,7 @@ export const updateBackupSchedule = <ThrowOnError extends boolean = false>(
export const getBackupScheduleForVolume = <ThrowOnError extends boolean = false>( export const getBackupScheduleForVolume = <ThrowOnError extends boolean = false>(
options: Options<GetBackupScheduleForVolumeData, ThrowOnError>, options: Options<GetBackupScheduleForVolumeData, ThrowOnError>,
) => { ) => {
return (options.client ?? _heyApiClient).get<GetBackupScheduleForVolumeResponses, unknown, ThrowOnError>({ return (options.client ?? client).get<GetBackupScheduleForVolumeResponses, unknown, ThrowOnError>({
url: "/api/v1/backups/volume/{volumeId}", url: "/api/v1/backups/volume/{volumeId}",
...options, ...options,
}); });
@@ -543,7 +543,7 @@ export const getBackupScheduleForVolume = <ThrowOnError extends boolean = false>
export const runBackupNow = <ThrowOnError extends boolean = false>( export const runBackupNow = <ThrowOnError extends boolean = false>(
options: Options<RunBackupNowData, ThrowOnError>, options: Options<RunBackupNowData, ThrowOnError>,
) => { ) => {
return (options.client ?? _heyApiClient).post<RunBackupNowResponses, unknown, ThrowOnError>({ return (options.client ?? client).post<RunBackupNowResponses, unknown, ThrowOnError>({
url: "/api/v1/backups/{scheduleId}/run", url: "/api/v1/backups/{scheduleId}/run",
...options, ...options,
}); });
@@ -553,7 +553,7 @@ export const runBackupNow = <ThrowOnError extends boolean = false>(
* Stop a backup that is currently in progress * Stop a backup that is currently in progress
*/ */
export const stopBackup = <ThrowOnError extends boolean = false>(options: Options<StopBackupData, ThrowOnError>) => { export const stopBackup = <ThrowOnError extends boolean = false>(options: Options<StopBackupData, ThrowOnError>) => {
return (options.client ?? _heyApiClient).post<StopBackupResponses, StopBackupErrors, ThrowOnError>({ return (options.client ?? client).post<StopBackupResponses, StopBackupErrors, ThrowOnError>({
url: "/api/v1/backups/{scheduleId}/stop", url: "/api/v1/backups/{scheduleId}/stop",
...options, ...options,
}); });
@@ -565,7 +565,7 @@ export const stopBackup = <ThrowOnError extends boolean = false>(options: Option
export const getSystemInfo = <ThrowOnError extends boolean = false>( export const getSystemInfo = <ThrowOnError extends boolean = false>(
options?: Options<GetSystemInfoData, ThrowOnError>, options?: Options<GetSystemInfoData, ThrowOnError>,
) => { ) => {
return (options?.client ?? _heyApiClient).get<GetSystemInfoResponses, unknown, ThrowOnError>({ return (options?.client ?? client).get<GetSystemInfoResponses, unknown, ThrowOnError>({
url: "/api/v1/system/info", url: "/api/v1/system/info",
...options, ...options,
}); });
@@ -577,7 +577,7 @@ export const getSystemInfo = <ThrowOnError extends boolean = false>(
export const downloadResticPassword = <ThrowOnError extends boolean = false>( export const downloadResticPassword = <ThrowOnError extends boolean = false>(
options?: Options<DownloadResticPasswordData, ThrowOnError>, options?: Options<DownloadResticPasswordData, ThrowOnError>,
) => { ) => {
return (options?.client ?? _heyApiClient).post<DownloadResticPasswordResponses, unknown, ThrowOnError>({ return (options?.client ?? client).post<DownloadResticPasswordResponses, unknown, ThrowOnError>({
url: "/api/v1/system/restic-password", url: "/api/v1/system/restic-password",
...options, ...options,
headers: { headers: {

View File

@@ -1,5 +1,9 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
export type ClientOptions = {
baseUrl: "http://192.168.2.42:4096" | (string & {});
};
export type RegisterData = { export type RegisterData = {
body?: { body?: {
password: string; password: string;
@@ -1672,7 +1676,3 @@ export type DownloadResticPasswordResponses = {
}; };
export type DownloadResticPasswordResponse = DownloadResticPasswordResponses[keyof DownloadResticPasswordResponses]; export type DownloadResticPasswordResponse = DownloadResticPasswordResponses[keyof DownloadResticPasswordResponses];
export type ClientOptions = {
baseUrl: "http://192.168.2.42:4096" | (string & {});
};

View File

@@ -6,8 +6,8 @@ import {
BreadcrumbList, BreadcrumbList,
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator, BreadcrumbSeparator,
} from "~/components/ui/breadcrumb"; } from "~/client/components/ui/breadcrumb";
import { useBreadcrumbs } from "~/lib/breadcrumbs"; import { useBreadcrumbs } from "~/client/lib/breadcrumbs";
export function AppBreadcrumb() { export function AppBreadcrumb() {
const breadcrumbs = useBreadcrumbs(); const breadcrumbs = useBreadcrumbs();

View File

@@ -10,9 +10,9 @@ import {
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem,
useSidebar, useSidebar,
} from "~/components/ui/sidebar"; } from "~/client/components/ui/sidebar";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/client/components/ui/tooltip";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
const items = [ const items = [
{ {

View File

@@ -2,12 +2,12 @@ import { useMutation } from "@tanstack/react-query";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { useId } from "react"; import { useId } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { createRepositoryMutation } from "~/api-client/@tanstack/react-query.gen"; import { parseError } from "~/client/lib/errors";
import { parseError } from "~/lib/errors";
import { CreateRepositoryForm } from "./create-repository-form"; import { CreateRepositoryForm } from "./create-repository-form";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
import { ScrollArea } from "./ui/scroll-area"; import { ScrollArea } from "./ui/scroll-area";
import { createRepositoryMutation } from "../api-client/@tanstack/react-query.gen";
type Props = { type Props = {
open: boolean; open: boolean;

View File

@@ -1,20 +1,20 @@
import { arktypeResolver } from "@hookform/resolvers/arktype"; import { arktypeResolver } from "@hookform/resolvers/arktype";
import { COMPRESSION_MODES, repositoryConfigSchema } from "@ironmount/schemas/restic";
import { type } from "arktype"; import { type } from "arktype";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { cn, slugify } from "~/lib/utils"; import { cn, slugify } from "~/client/lib/utils";
import { deepClean } from "~/utils/object"; import { deepClean } from "~/utils/object";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form";
import { Input } from "./ui/input"; import { Input } from "./ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
import { listRcloneRemotesOptions } from "~/api-client/@tanstack/react-query.gen";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Alert, AlertDescription } from "./ui/alert"; import { Alert, AlertDescription } from "./ui/alert";
import { ExternalLink } from "lucide-react"; import { ExternalLink } from "lucide-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
import { useSystemInfo } from "~/hooks/use-system-info"; import { useSystemInfo } from "~/client/hooks/use-system-info";
import { COMPRESSION_MODES, repositoryConfigSchema } from "~/schemas/restic";
import { listRcloneRemotesOptions } from "../api-client/@tanstack/react-query.gen";
export const formSchema = type({ export const formSchema = type({
name: "2<=string<=32", name: "2<=string<=32",
@@ -61,17 +61,17 @@ export const CreateRepositoryForm = ({
const { watch } = form; const { watch } = form;
const watchedBackend = watch("backend"); const watchedBackend = watch("backend");
const watchedName = watch("name");
const { data: rcloneRemotes, isLoading: isLoadingRemotes } = useQuery({ const { data: rcloneRemotes, isLoading: isLoadingRemotes } = useQuery({
...listRcloneRemotesOptions(), ...listRcloneRemotesOptions(),
}); });
useEffect(() => { useEffect(() => {
if (watchedBackend && watchedBackend in defaultValuesForType) { form.reset({
form.reset({ name: watchedName, ...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType] }); name: form.getValues().name,
} ...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType],
}, [watchedBackend, watchedName, form]); });
}, [watchedBackend, form]);
const { capabilities } = useSystemInfo(); const { capabilities } = useSystemInfo();

View File

@@ -2,12 +2,12 @@ import { useMutation } from "@tanstack/react-query";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { useId } from "react"; import { useId } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { createVolumeMutation } from "~/api-client/@tanstack/react-query.gen"; import { parseError } from "~/client/lib/errors";
import { parseError } from "~/lib/errors";
import { CreateVolumeForm } from "./create-volume-form"; import { CreateVolumeForm } from "./create-volume-form";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
import { ScrollArea } from "./ui/scroll-area"; import { ScrollArea } from "./ui/scroll-area";
import { createVolumeMutation } from "../api-client/@tanstack/react-query.gen";
type Props = { type Props = {
open: boolean; open: boolean;

View File

@@ -1,18 +1,18 @@
import { arktypeResolver } from "@hookform/resolvers/arktype"; import { arktypeResolver } from "@hookform/resolvers/arktype";
import { volumeConfigSchema } from "@ironmount/schemas";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { type } from "arktype"; import { type } from "arktype";
import { CheckCircle, Loader2, XCircle } from "lucide-react"; import { CheckCircle, Loader2, XCircle } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { testConnectionMutation } from "~/api-client/@tanstack/react-query.gen"; import { cn, slugify } from "~/client/lib/utils";
import { cn, slugify } from "~/lib/utils";
import { deepClean } from "~/utils/object"; import { deepClean } from "~/utils/object";
import { DirectoryBrowser } from "./directory-browser"; import { DirectoryBrowser } from "./directory-browser";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form";
import { Input } from "./ui/input"; import { Input } from "./ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
import { volumeConfigSchema } from "~/schemas/volumes";
import { testConnectionMutation } from "../api-client/@tanstack/react-query.gen";
export const formSchema = type({ export const formSchema = type({
name: "2<=string<=32", name: "2<=string<=32",
@@ -50,13 +50,15 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
const { watch, getValues } = form; const { watch, getValues } = form;
const watchedBackend = watch("backend"); const watchedBackend = watch("backend");
const watchedName = watch("name");
useEffect(() => { useEffect(() => {
if (mode === "create") { if (mode === "create") {
form.reset({ name: watchedName, ...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType] }); form.reset({
name: form.getValues().name,
...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType],
});
} }
}, [watchedBackend, watchedName, form.reset, mode]); }, [watchedBackend, form, mode]);
const [testMessage, setTestMessage] = useState<{ success: boolean; message: string } | null>(null); const [testMessage, setTestMessage] = useState<{ success: boolean; message: string } | null>(null);
@@ -141,19 +143,17 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
control={form.control} control={form.control}
name="path" name="path"
render={({ field }) => { render={({ field }) => {
const [showBrowser, setShowBrowser] = useState(!field.value || field.value === "/");
return ( return (
<FormItem> <FormItem>
<FormLabel>Directory Path</FormLabel> <FormLabel>Directory Path</FormLabel>
<FormControl> <FormControl>
{!showBrowser && field.value ? ( {field.value ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1 border rounded-md p-3 bg-muted/50"> <div className="flex-1 border rounded-md p-3 bg-muted/50">
<div className="text-xs font-medium text-muted-foreground mb-1">Selected path:</div> <div className="text-xs font-medium text-muted-foreground mb-1">Selected path:</div>
<div className="text-sm font-mono break-all">{field.value}</div> <div className="text-sm font-mono break-all">{field.value}</div>
</div> </div>
<Button type="button" variant="outline" size="sm" onClick={() => setShowBrowser(true)}> <Button type="button" variant="outline" size="sm" onClick={() => field.onChange("")}>
Change Change
</Button> </Button>
</div> </div>

View File

@@ -1,8 +1,8 @@
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { browseFilesystemOptions } from "~/api-client/@tanstack/react-query.gen";
import { FileTree, type FileEntry } from "./file-tree"; import { FileTree, type FileEntry } from "./file-tree";
import { ScrollArea } from "./ui/scroll-area"; import { ScrollArea } from "./ui/scroll-area";
import { browseFilesystemOptions } from "../api-client/@tanstack/react-query.gen";
type Props = { type Props = {
onSelectPath: (path: string) => void; onSelectPath: (path: string) => void;

View File

@@ -17,7 +17,7 @@ export function EmptyState(props: EmptyStateProps) {
<div className="absolute inset-0 animate-pulse"> <div className="absolute inset-0 animate-pulse">
<div className="w-32 h-32 rounded-full bg-primary/10 blur-2xl" /> <div className="w-32 h-32 rounded-full bg-primary/10 blur-2xl" />
</div> </div>
<div className="relative flex items-center justify-center w-32 h-32 rounded-full bg-gradient-to-br from-primary/20 to-primary/5 border-2 border-primary/20"> <div className="relative flex items-center justify-center w-32 h-32 rounded-full bg-linear-to-br from-primary/20 to-primary/5 border-2 border-primary/20">
<Cicon className="w-16 h-16 text-primary/70" strokeWidth={1.5} /> <Cicon className="w-16 h-16 text-primary/70" strokeWidth={1.5} />
</div> </div>
</div> </div>

View File

@@ -10,8 +10,8 @@
import { ChevronDown, ChevronRight, File as FileIcon, Folder as FolderIcon, FolderOpen, Loader2 } from "lucide-react"; import { ChevronDown, ChevronRight, File as FileIcon, Folder as FolderIcon, FolderOpen, Loader2 } from "lucide-react";
import { memo, type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { memo, type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
import { Checkbox } from "~/components/ui/checkbox"; import { Checkbox } from "~/client/components/ui/checkbox";
const NODE_PADDING_LEFT = 12; const NODE_PADDING_LEFT = 12;

View File

@@ -1,5 +1,5 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
interface GridBackgroundProps { interface GridBackgroundProps {
children: ReactNode; children: ReactNode;
@@ -12,9 +12,9 @@ export function GridBackground({ children, className, containerClassName }: Grid
<div <div
className={cn( className={cn(
"relative min-h-full w-full overflow-x-hidden", "relative min-h-full w-full overflow-x-hidden",
"[background-size:20px_20px] sm:[background-size:40px_40px]", "bg-size-[20px_20px] sm:bg-size-[40px_40px]",
"[background-image:linear-gradient(to_right,#e4e4e7_1px,transparent_1px),linear-gradient(to_bottom,#e4e4e7_1px,transparent_1px)]", "bg-[linear-gradient(to_right,#e4e4e7_1px,transparent_1px),linear-gradient(to_bottom,#e4e4e7_1px,transparent_1px)]",
"dark:[background-image:linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]", "dark:bg-[linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]",
containerClassName, containerClassName,
)} )}
> >

View File

@@ -2,7 +2,6 @@ import { useMutation } from "@tanstack/react-query";
import { LifeBuoy } from "lucide-react"; import { LifeBuoy } from "lucide-react";
import { Outlet, redirect, useNavigate } from "react-router"; import { Outlet, redirect, useNavigate } from "react-router";
import { toast } from "sonner"; import { toast } from "sonner";
import { logoutMutation } from "~/api-client/@tanstack/react-query.gen";
import { appContext } from "~/context"; import { appContext } from "~/context";
import { authMiddleware } from "~/middleware/auth"; import { authMiddleware } from "~/middleware/auth";
import type { Route } from "./+types/layout"; import type { Route } from "./+types/layout";
@@ -11,6 +10,7 @@ import { GridBackground } from "./grid-background";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { SidebarProvider, SidebarTrigger } from "./ui/sidebar"; import { SidebarProvider, SidebarTrigger } from "./ui/sidebar";
import { AppSidebar } from "./app-sidebar"; import { AppSidebar } from "./app-sidebar";
import { logoutMutation } from "../api-client/@tanstack/react-query.gen";
export const clientMiddleware = [authMiddleware]; export const clientMiddleware = [authMiddleware];
@@ -42,7 +42,7 @@ export default function Layout({ loaderData }: Route.ComponentProps) {
<SidebarProvider defaultOpen={true}> <SidebarProvider defaultOpen={true}>
<AppSidebar /> <AppSidebar />
<div className="w-full relative flex flex-col h-screen overflow-hidden"> <div className="w-full relative flex flex-col h-screen overflow-hidden">
<header className="z-50 bg-card-header border-b border-border/50 flex-shrink-0"> <header className="z-50 bg-card-header border-b border-border/50 shrink-0">
<div className="flex items-center justify-between py-3 sm:py-4 px-2 sm:px-8 mx-auto container"> <div className="flex items-center justify-between py-3 sm:py-4 px-2 sm:px-8 mx-auto container">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<SidebarTrigger /> <SidebarTrigger />

View File

@@ -1,4 +1,4 @@
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
import { Switch } from "./ui/switch"; import { Switch } from "./ui/switch";
type Props = { type Props = {

View File

@@ -1,5 +1,5 @@
import type { RepositoryBackend } from "@ironmount/schemas/restic";
import { Database, HardDrive, Cloud } from "lucide-react"; import { Database, HardDrive, Cloud } from "lucide-react";
import type { RepositoryBackend } from "~/schemas/restic";
type Props = { type Props = {
backend: RepositoryBackend; backend: RepositoryBackend;

View File

@@ -1,10 +1,10 @@
import { Calendar, Clock, Database, FolderTree, HardDrive } from "lucide-react"; import { Calendar, Clock, Database, FolderTree, HardDrive } from "lucide-react";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import type { ListSnapshotsResponse } from "~/api-client/types.gen"; import { ByteSize } from "~/client/components/bytes-size";
import { ByteSize } from "~/components/bytes-size"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"; import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip";
import { formatDuration } from "~/utils/utils"; import { formatDuration } from "~/utils/utils";
import type { ListSnapshotsResponse } from "../api-client";
type Snapshot = ListSnapshotsResponse[number]; type Snapshot = ListSnapshotsResponse[number];

View File

@@ -1,5 +1,5 @@
import type { VolumeStatus } from "~/lib/types"; import type { VolumeStatus } from "~/client/lib/types";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
export const StatusDot = ({ status }: { status: VolumeStatus }) => { export const StatusDot = ({ status }: { status: VolumeStatus }) => {
@@ -38,10 +38,7 @@ export const StatusDot = ({ status }: { status: VolumeStatus }) => {
)} )}
/> />
)} )}
<span <span className={cn("relative inline-flex size-3 rounded-full", `${statusMapping.color}`)} />
aria-label={status}
className={cn("relative inline-flex size-3 rounded-full", `${statusMapping.color}`)}
/>
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>

View File

@@ -1,7 +1,7 @@
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import type * as React from "react"; import type * as React from "react";
import { buttonVariants } from "~/components/ui/button"; import { buttonVariants } from "~/client/components/ui/button";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) { function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />; return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;

View File

@@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
const alertVariants = cva( const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",

View File

@@ -2,7 +2,7 @@ import type * as React from "react";
import { Slot } from "@radix-ui/react-slot"; import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
const badgeVariants = cva( const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",

View File

@@ -0,0 +1,92 @@
import type * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "~/client/lib/utils";
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm wrap-break-words sm:gap-2.5",
className,
)}
{...props}
/>
);
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return <li data-slot="breadcrumb-item" className={cn("inline-flex items-center gap-1.5", className)} {...props} />;
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "a";
return (
<Comp data-slot="breadcrumb-link" className={cn("hover:text-foreground transition-colors", className)} {...props} />
);
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
);
}
function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
}
function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
);
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};

View File

@@ -3,7 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import type * as React from "react"; import type * as React from "react";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex cursor-pointer uppercase rounded-sm items-center justify-center gap-2 whitespace-nowrap text-xs font-semibold tracking-wide transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-ring border-0", "inline-flex cursor-pointer uppercase rounded-sm items-center justify-center gap-2 whitespace-nowrap text-xs font-semibold tracking-wide transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-ring border-0",

View File

@@ -1,6 +1,6 @@
import type * as React from "react"; import type * as React from "react";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
function Card({ className, children, ...props }: React.ComponentProps<"div">) { function Card({ className, children, ...props }: React.ComponentProps<"div">) {
return ( return (

View File

@@ -3,7 +3,7 @@
import * as React from "react"; import * as React from "react";
import * as RechartsPrimitive from "recharts"; import * as RechartsPrimitive from "recharts";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
// Format: { THEME_NAME: CSS_SELECTOR } // Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const; const THEMES = { light: "", dark: ".dark" } as const;

View File

@@ -0,0 +1,27 @@
import type * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { CheckIcon } from "lucide-react";
import { cn } from "~/client/lib/utils";
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };

View File

@@ -0,0 +1,121 @@
import type * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import { cn } from "~/client/lib/utils";
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
{...props}
/>
);
}
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
);
}
function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};

View File

@@ -1,5 +1,5 @@
import * as React from "react"; import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label"; import type * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot"; import { Slot } from "@radix-ui/react-slot";
import { import {
Controller, Controller,
@@ -11,8 +11,8 @@ import {
type FieldValues, type FieldValues,
} from "react-hook-form"; } from "react-hook-form";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
import { Label } from "~/components/ui/label"; import { Label } from "~/client/components/ui/label";
const Form = FormProvider; const Form = FormProvider;

View File

@@ -1,6 +1,6 @@
import type * as React from "react"; import type * as React from "react";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) { function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return ( return (

View File

@@ -0,0 +1,21 @@
"use client";
import type * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cn } from "~/client/lib/utils";
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className,
)}
{...props}
/>
);
}
export { Label };

View File

@@ -1,7 +1,7 @@
import * as React from "react"; import type * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress"; import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
function Progress({ className, value, ...props }: React.ComponentProps<typeof ProgressPrimitive.Root>) { function Progress({ className, value, ...props }: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return ( return (

View File

@@ -0,0 +1,46 @@
import type * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "~/client/lib/utils";
function ScrollArea({ className, children, ...props }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root data-slot="scroll-area" className={cn("relative", className)} {...props}>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);
}
export { ScrollArea, ScrollBar };

View File

@@ -1,24 +1,18 @@
import * as React from "react"; import type * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select"; import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
function Select({ function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />; return <SelectPrimitive.Root data-slot="select" {...props} />;
} }
function SelectGroup({ function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />; return <SelectPrimitive.Group data-slot="select-group" {...props} />;
} }
function SelectValue({ function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />; return <SelectPrimitive.Value data-slot="select-value" {...props} />;
} }
@@ -83,10 +77,7 @@ function SelectContent({
); );
} }
function SelectLabel({ function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return ( return (
<SelectPrimitive.Label <SelectPrimitive.Label
data-slot="select-label" data-slot="select-label"
@@ -96,11 +87,7 @@ function SelectLabel({
); );
} }
function SelectItem({ function SelectItem({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>) {
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return ( return (
<SelectPrimitive.Item <SelectPrimitive.Item
data-slot="select-item" data-slot="select-item"
@@ -120,10 +107,7 @@ function SelectItem({
); );
} }
function SelectSeparator({ function SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return ( return (
<SelectPrimitive.Separator <SelectPrimitive.Separator
data-slot="select-separator" data-slot="select-separator"
@@ -133,17 +117,11 @@ function SelectSeparator({
); );
} }
function SelectScrollUpButton({ function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return ( return (
<SelectPrimitive.ScrollUpButton <SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button" data-slot="select-scroll-up-button"
className={cn( className={cn("flex cursor-default items-center justify-center py-1", className)}
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props} {...props}
> >
<ChevronUpIcon className="size-4" /> <ChevronUpIcon className="size-4" />
@@ -158,10 +136,7 @@ function SelectScrollDownButton({
return ( return (
<SelectPrimitive.ScrollDownButton <SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button" data-slot="select-scroll-down-button"
className={cn( className={cn("flex cursor-default items-center justify-center py-1", className)}
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props} {...props}
> >
<ChevronDownIcon className="size-4" /> <ChevronDownIcon className="size-4" />

View File

@@ -0,0 +1,26 @@
import type * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "~/client/lib/utils";
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className,
)}
{...props}
/>
);
}
export { Separator };

View File

@@ -0,0 +1,103 @@
"use client";
import type * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import { cn } from "~/client/lib/utils";
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}
function SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
function SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
function SheetOverlay({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left";
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className,
)}
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="sheet-header" className={cn("flex flex-col gap-1.5 p-4", className)} {...props} />;
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="sheet-footer" className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />;
}
function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
function SheetDescription({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription };

View File

@@ -5,14 +5,14 @@ import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { PanelLeftIcon } from "lucide-react"; import { PanelLeftIcon } from "lucide-react";
import { useIsMobile } from "~/hooks/use-mobile"; import { useIsMobile } from "~/client/hooks/use-mobile";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
import { Button } from "~/components/ui/button"; import { Button } from "~/client/components/ui/button";
import { Input } from "~/components/ui/input"; import { Input } from "~/client/components/ui/input";
import { Separator } from "~/components/ui/separator"; import { Separator } from "~/client/components/ui/separator";
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "~/components/ui/sheet"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "~/client/components/ui/sheet";
import { Skeleton } from "~/components/ui/skeleton"; import { Skeleton } from "~/client/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/client/components/ui/tooltip";
const SIDEBAR_COOKIE_NAME = "sidebar_state"; const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;

View File

@@ -0,0 +1,7 @@
import { cn } from "~/client/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="skeleton" className={cn("bg-accent animate-pulse rounded-md", className)} {...props} />;
}
export { Skeleton };

View File

@@ -1,7 +1,7 @@
import * as SwitchPrimitive from "@radix-ui/react-switch"; import * as SwitchPrimitive from "@radix-ui/react-switch";
import type * as React from "react"; import type * as React from "react";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) { function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return ( return (

View File

@@ -0,0 +1,73 @@
import type * as React from "react";
import { cn } from "~/client/lib/utils";
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div data-slot="table-container" className="relative w-full overflow-x-auto">
<table data-slot="table" className={cn("w-full caption-bottom text-sm", className)} {...props} />
</div>
);
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return <thead data-slot="table-header" className={cn("[&_tr]:border-b", className)} {...props} />;
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return <tbody data-slot="table-body" className={cn("[&_tr:last-child]:border-0", className)} {...props} />;
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)}
{...props}
/>
);
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn("hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", className)}
{...props}
/>
);
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
/>
);
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
/>
);
}
function TableCaption({ className, ...props }: React.ComponentProps<"caption">) {
return (
<caption data-slot="table-caption" className={cn("text-muted-foreground mt-4 text-sm", className)} {...props} />
);
}
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };

View File

@@ -1,7 +1,7 @@
import * as TabsPrimitive from "@radix-ui/react-tabs"; import * as TabsPrimitive from "@radix-ui/react-tabs";
import type * as React from "react"; import type * as React from "react";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) { function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
return <TabsPrimitive.Root data-slot="tabs" className={cn("flex flex-col gap-2", className)} {...props} />; return <TabsPrimitive.Root data-slot="tabs" className={cn("flex flex-col gap-2", className)} {...props} />;

View File

@@ -1,6 +1,6 @@
import * as React from "react"; import type * as React from "react";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return ( return (

View File

@@ -0,0 +1,46 @@
import type * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "~/client/lib/utils";
function TooltipProvider({ delayDuration = 0, ...props }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} />;
}
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
}
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@@ -1,8 +1,8 @@
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { FolderOpen } from "lucide-react"; import { FolderOpen } from "lucide-react";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { listFilesOptions } from "~/api-client/@tanstack/react-query.gen"; import { FileTree } from "~/client/components/file-tree";
import { FileTree } from "~/components/file-tree"; import { listFilesOptions } from "../api-client/@tanstack/react-query.gen";
interface FileEntry { interface FileEntry {
name: string; name: string;

View File

@@ -1,5 +1,5 @@
import type { BackendType } from "@ironmount/schemas";
import { Cloud, Folder, Server, Share2 } from "lucide-react"; import { Cloud, Folder, Server, Share2 } from "lucide-react";
import type { BackendType } from "~/schemas/volumes";
type VolumeIconProps = { type VolumeIconProps = {
backend: BackendType; backend: BackendType;

View File

@@ -0,0 +1,19 @@
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}

View File

@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { getSystemInfoOptions } from "~/api-client/@tanstack/react-query.gen"; import { getSystemInfoOptions } from "../api-client/@tanstack/react-query.gen";
export function useSystemInfo() { export function useSystemInfo() {
const { data, isLoading, error } = useQuery({ const { data, isLoading, error } = useQuery({

View File

@@ -4,7 +4,7 @@ import type {
GetRepositoryResponse, GetRepositoryResponse,
GetVolumeResponse, GetVolumeResponse,
ListSnapshotsResponse, ListSnapshotsResponse,
} from "~/api-client"; } from "../api-client";
export type Volume = GetVolumeResponse["volume"]; export type Volume = GetVolumeResponse["volume"];
export type StatFs = GetVolumeResponse["statfs"]; export type StatFs = GetVolumeResponse["statfs"];

View File

@@ -3,14 +3,14 @@ import { AlertTriangle, Download } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { toast } from "sonner"; import { toast } from "sonner";
import { downloadResticPasswordMutation } from "~/api-client/@tanstack/react-query.gen"; import { AuthLayout } from "~/client/components/auth-layout";
import { AuthLayout } from "~/components/auth-layout"; import { Alert, AlertDescription, AlertTitle } from "~/client/components/ui/alert";
import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert"; import { Button } from "~/client/components/ui/button";
import { Button } from "~/components/ui/button"; import { Input } from "~/client/components/ui/input";
import { Input } from "~/components/ui/input"; import { Label } from "~/client/components/ui/label";
import { Label } from "~/components/ui/label";
import { authMiddleware } from "~/middleware/auth"; import { authMiddleware } from "~/middleware/auth";
import type { Route } from "./+types/download-recovery-key"; import type { Route } from "./+types/download-recovery-key";
import { downloadResticPasswordMutation } from "~/client/api-client/@tanstack/react-query.gen";
export const clientMiddleware = [authMiddleware]; export const clientMiddleware = [authMiddleware];

View File

@@ -4,13 +4,13 @@ import { type } from "arktype";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { toast } from "sonner"; import { toast } from "sonner";
import { loginMutation } from "~/api-client/@tanstack/react-query.gen"; import { AuthLayout } from "~/client/components/auth-layout";
import { AuthLayout } from "~/components/auth-layout"; import { Button } from "~/client/components/ui/button";
import { Button } from "~/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form"; import { Input } from "~/client/components/ui/input";
import { Input } from "~/components/ui/input";
import { authMiddleware } from "~/middleware/auth"; import { authMiddleware } from "~/middleware/auth";
import type { Route } from "./+types/login"; import type { Route } from "./+types/login";
import { loginMutation } from "~/client/api-client/@tanstack/react-query.gen";
export const clientMiddleware = [authMiddleware]; export const clientMiddleware = [authMiddleware];

View File

@@ -4,13 +4,21 @@ import { type } from "arktype";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { toast } from "sonner"; import { toast } from "sonner";
import { registerMutation } from "~/api-client/@tanstack/react-query.gen"; import {
import { AuthLayout } from "~/components/auth-layout"; Form,
import { Button } from "~/components/ui/button"; FormControl,
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form"; FormDescription,
import { Input } from "~/components/ui/input"; FormField,
FormItem,
FormLabel,
FormMessage,
} from "~/client/components/ui/form";
import { authMiddleware } from "~/middleware/auth"; import { authMiddleware } from "~/middleware/auth";
import type { Route } from "./+types/onboarding"; import type { Route } from "./+types/onboarding";
import { AuthLayout } from "~/client/components/auth-layout";
import { Input } from "~/client/components/ui/input";
import { Button } from "~/client/components/ui/button";
import { registerMutation } from "~/client/api-client/@tanstack/react-query.gen";
export const clientMiddleware = [authMiddleware]; export const clientMiddleware = [authMiddleware];

View File

@@ -1,8 +1,8 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ByteSize, formatBytes } from "~/components/bytes-size"; import { ByteSize, formatBytes } from "~/client/components/bytes-size";
import { Card } from "~/components/ui/card"; import { Card } from "~/client/components/ui/card";
import { Progress } from "~/components/ui/progress"; import { Progress } from "~/client/components/ui/progress";
import { type BackupProgressEvent, useServerEvents } from "~/hooks/use-server-events"; import { type BackupProgressEvent, useServerEvents } from "~/client/hooks/use-server-events";
import { formatDuration } from "~/utils/utils"; import { formatDuration } from "~/utils/utils";
type Props = { type Props = {

View File

@@ -1,5 +1,5 @@
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
type BackupStatus = "active" | "paused" | "error" | "in_progress"; type BackupStatus = "active" | "paused" | "error" | "in_progress";
@@ -7,7 +7,11 @@ export const BackupStatusDot = ({
enabled, enabled,
hasError, hasError,
isInProgress, isInProgress,
}: { enabled: boolean; hasError?: boolean; isInProgress?: boolean }) => { }: {
enabled: boolean;
hasError?: boolean;
isInProgress?: boolean;
}) => {
let status: BackupStatus = "paused"; let status: BackupStatus = "paused";
if (isInProgress) { if (isInProgress) {
status = "in_progress"; status = "in_progress";

View File

@@ -3,15 +3,23 @@ import { useQuery } from "@tanstack/react-query";
import { type } from "arktype"; import { type } from "arktype";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { listRepositoriesOptions } from "~/api-client/@tanstack/react-query.gen"; import { listRepositoriesOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { RepositoryIcon } from "~/components/repository-icon"; import { RepositoryIcon } from "~/client/components/repository-icon";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form"; import {
import { Input } from "~/components/ui/input"; Form,
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"; FormControl,
import { Textarea } from "~/components/ui/textarea"; FormDescription,
import { VolumeFileBrowser } from "~/components/volume-file-browser"; FormField,
import type { BackupSchedule, Volume } from "~/lib/types"; FormItem,
FormLabel,
FormMessage,
} from "~/client/components/ui/form";
import { Input } from "~/client/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
import { Textarea } from "~/client/components/ui/textarea";
import { VolumeFileBrowser } from "~/client/components/volume-file-browser";
import type { BackupSchedule, Volume } from "~/client/lib/types";
import { deepClean } from "~/utils/object"; import { deepClean } from "~/utils/object";
const internalFormSchema = type({ const internalFormSchema = type({
@@ -128,7 +136,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(handleSubmit)} onSubmit={form.handleSubmit(handleSubmit)}
className="grid gap-4 xl:grid-cols-[minmax(0,_2.3fr)_minmax(320px,_1fr)]" className="grid gap-4 xl:grid-cols-[minmax(0,2.3fr)_minmax(320px,1fr)]"
id={formId} id={formId}
> >
<div className="grid gap-4"> <div className="grid gap-4">

View File

@@ -1,8 +1,8 @@
import { Pencil, Play, Square, Trash2 } from "lucide-react"; import { Pencil, Play, Square, Trash2 } from "lucide-react";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { OnOff } from "~/components/onoff"; import { OnOff } from "~/client/components/onoff";
import { Button } from "~/components/ui/button"; import { Button } from "~/client/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -11,8 +11,8 @@ import {
AlertDialogDescription, AlertDialogDescription,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "~/components/ui/alert-dialog"; } from "~/client/components/ui/alert-dialog";
import type { BackupSchedule } from "~/lib/types"; import type { BackupSchedule } from "~/client/lib/types";
import { BackupProgressCard } from "./backup-progress-card"; import { BackupProgressCard } from "./backup-progress-card";
type Props = { type Props = {

View File

@@ -1,12 +1,11 @@
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { FileIcon } from "lucide-react"; import { FileIcon } from "lucide-react";
import { listSnapshotFilesOptions, restoreSnapshotMutation } from "~/api-client/@tanstack/react-query.gen"; import { FileTree, type FileEntry } from "~/client/components/file-tree";
import { FileTree, type FileEntry } from "~/components/file-tree"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; import { Button } from "~/client/components/ui/button";
import { Button } from "~/components/ui/button"; import { Checkbox } from "~/client/components/ui/checkbox";
import { Checkbox } from "~/components/ui/checkbox"; import { Label } from "~/client/components/ui/label";
import { Label } from "~/components/ui/label";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -16,10 +15,11 @@ import {
AlertDialogFooter, AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "~/components/ui/alert-dialog"; } from "~/client/components/ui/alert-dialog";
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
import type { Snapshot, Volume } from "~/lib/types"; import type { Snapshot, Volume } from "~/client/lib/types";
import { toast } from "sonner"; import { toast } from "sonner";
import { listSnapshotFilesOptions, restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
interface Props { interface Props {
snapshot: Snapshot; snapshot: Snapshot;

View File

@@ -1,8 +1,8 @@
import type { ListSnapshotsResponse } from "~/api-client/types.gen"; import { cn } from "~/client/lib/utils";
import { cn } from "~/lib/utils"; import { Card } from "~/client/components/ui/card";
import { Card } from "~/components/ui/card"; import { ByteSize } from "~/client/components/bytes-size";
import { ByteSize } from "~/components/bytes-size";
import { useEffect } from "react"; import { useEffect } from "react";
import type { ListSnapshotsResponse } from "~/client/api-client";
interface Props { interface Props {
snapshots: ListSnapshotsResponse; snapshots: ListSnapshotsResponse;
@@ -56,7 +56,7 @@ export const SnapshotTimeline = (props: Props) => {
<div className="w-full bg-card"> <div className="w-full bg-card">
<div className="relative flex items-center"> <div className="relative flex items-center">
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
<div className="flex gap-4 overflow-x-auto pb-2 [&>:first-child]:ml-2 [&>:last-child]:mr-2"> <div className="flex gap-4 overflow-x-auto pb-2 *:first:ml-2 *:last:mr-2">
{snapshots.map((snapshot, index) => { {snapshots.map((snapshot, index) => {
const date = new Date(snapshot.time); const date = new Date(snapshot.time);
const isSelected = snapshotId === snapshot.short_id; const isSelected = snapshotId === snapshot.short_id;

View File

@@ -2,7 +2,7 @@ import { useId, useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query"; import { useQuery, useMutation } from "@tanstack/react-query";
import { redirect, useNavigate } from "react-router"; import { redirect, useNavigate } from "react-router";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "~/components/ui/button"; import { Button } from "~/client/components/ui/button";
import { import {
getBackupScheduleOptions, getBackupScheduleOptions,
runBackupNowMutation, runBackupNowMutation,
@@ -10,15 +10,15 @@ import {
listSnapshotsOptions, listSnapshotsOptions,
updateBackupScheduleMutation, updateBackupScheduleMutation,
stopBackupMutation, stopBackupMutation,
} from "~/api-client/@tanstack/react-query.gen"; } from "~/client/api-client/@tanstack/react-query.gen";
import { parseError } from "~/lib/errors"; import { parseError } from "~/client/lib/errors";
import { getCronExpression } from "~/utils/utils"; import { getCronExpression } from "~/utils/utils";
import { CreateScheduleForm, type BackupScheduleFormValues } from "../components/create-schedule-form"; import { CreateScheduleForm, type BackupScheduleFormValues } from "../components/create-schedule-form";
import { ScheduleSummary } from "../components/schedule-summary"; import { ScheduleSummary } from "../components/schedule-summary";
import { getBackupSchedule } from "~/api-client";
import type { Route } from "./+types/backup-details"; import type { Route } from "./+types/backup-details";
import { SnapshotFileBrowser } from "../components/snapshot-file-browser"; import { SnapshotFileBrowser } from "../components/snapshot-file-browser";
import { SnapshotTimeline } from "../components/snapshot-timeline"; import { SnapshotTimeline } from "../components/snapshot-timeline";
import { getBackupSchedule } from "~/client/api-client";
export function meta(_: Route.MetaArgs) { export function meta(_: Route.MetaArgs) {
return [ return [

View File

@@ -1,13 +1,13 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { CalendarClock, Database, HardDrive, Plus } from "lucide-react"; import { CalendarClock, Database, HardDrive, Plus } from "lucide-react";
import { Link } from "react-router"; import { Link } from "react-router";
import { listBackupSchedules } from "~/api-client";
import { listBackupSchedulesOptions } from "~/api-client/@tanstack/react-query.gen";
import { BackupStatusDot } from "../components/backup-status-dot"; import { BackupStatusDot } from "../components/backup-status-dot";
import { EmptyState } from "~/components/empty-state"; import { EmptyState } from "~/client/components/empty-state";
import { Button } from "~/components/ui/button"; import { Button } from "~/client/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
import type { Route } from "./+types/backups"; import type { Route } from "./+types/backups";
import { listBackupSchedules } from "~/client/api-client";
import { listBackupSchedulesOptions } from "~/client/api-client/@tanstack/react-query.gen";
export function meta(_: Route.MetaArgs) { export function meta(_: Route.MetaArgs) {
return [ return [
@@ -68,7 +68,7 @@ export default function Backups({ loaderData }: Route.ComponentProps) {
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2 flex-1 min-w-0"> <div className="flex items-center gap-2 flex-1 min-w-0">
<HardDrive className="h-5 w-5 text-muted-foreground flex-shrink-0" /> <HardDrive className="h-5 w-5 text-muted-foreground shrink-0" />
<CardTitle className="text-lg truncate"> <CardTitle className="text-lg truncate">
Volume <span className="text-strong-accent">{schedule.volume.name}</span> Volume <span className="text-strong-accent">{schedule.volume.name}</span>
</CardTitle> </CardTitle>

View File

@@ -1,5 +1,5 @@
import { useId, useState } from "react"; import { useId, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import { Database, HardDrive } from "lucide-react"; import { Database, HardDrive } from "lucide-react";
import { Link, useNavigate } from "react-router"; import { Link, useNavigate } from "react-router";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -7,16 +7,16 @@ import {
createBackupScheduleMutation, createBackupScheduleMutation,
listRepositoriesOptions, listRepositoriesOptions,
listVolumesOptions, listVolumesOptions,
} from "~/api-client/@tanstack/react-query.gen"; } from "~/client/api-client/@tanstack/react-query.gen";
import { Button } from "~/components/ui/button"; import { Button } from "~/client/components/ui/button";
import { Card, CardContent } from "~/components/ui/card"; import { Card, CardContent } from "~/client/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
import { parseError } from "~/lib/errors"; import { parseError } from "~/client/lib/errors";
import { EmptyState } from "~/components/empty-state"; import { EmptyState } from "~/client/components/empty-state";
import { getCronExpression } from "~/utils/utils"; import { getCronExpression } from "~/utils/utils";
import { CreateScheduleForm, type BackupScheduleFormValues } from "../components/create-schedule-form"; import { CreateScheduleForm, type BackupScheduleFormValues } from "../components/create-schedule-form";
import type { Route } from "./+types/create-backup"; import type { Route } from "./+types/create-backup";
import { listRepositories, listVolumes } from "~/api-client"; import { listRepositories, listVolumes } from "~/client/api-client";
export function meta(_: Route.MetaArgs) { export function meta(_: Route.MetaArgs) {
return [ return [
@@ -168,7 +168,7 @@ export default function CreateBackup({ loaderData }: Route.ComponentProps) {
<div className="absolute inset-0 animate-pulse"> <div className="absolute inset-0 animate-pulse">
<div className="w-24 h-24 rounded-full bg-primary/10 blur-2xl" /> <div className="w-24 h-24 rounded-full bg-primary/10 blur-2xl" />
</div> </div>
<div className="relative flex items-center justify-center w-24 h-24 rounded-full bg-gradient-to-br from-primary/20 to-primary/5 border-2 border-primary/20"> <div className="relative flex items-center justify-center w-24 h-24 rounded-full bg-linear-to-br from-primary/20 to-primary/5 border-2 border-primary/20">
<Database className="w-12 h-12 text-primary/70" strokeWidth={1.5} /> <Database className="w-12 h-12 text-primary/70" strokeWidth={1.5} />
</div> </div>
</div> </div>

View File

@@ -2,9 +2,9 @@ import { useMutation } from "@tanstack/react-query";
import { RotateCcw } from "lucide-react"; import { RotateCcw } from "lucide-react";
import { useId, useState } from "react"; import { useId, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { restoreSnapshotMutation } from "~/api-client/@tanstack/react-query.gen"; import { restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
import { parseError } from "~/lib/errors"; import { parseError } from "~/client/lib/errors";
import { Button } from "~/components/ui/button"; import { Button } from "~/client/components/ui/button";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -13,8 +13,8 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "~/components/ui/dialog"; } from "~/client/components/ui/dialog";
import { ScrollArea } from "~/components/ui/scroll-area"; import { ScrollArea } from "~/client/components/ui/scroll-area";
import { RestoreSnapshotForm, type RestoreSnapshotFormValues } from "./restore-snapshot-form"; import { RestoreSnapshotForm, type RestoreSnapshotFormValues } from "./restore-snapshot-form";
type Props = { type Props = {

View File

@@ -1,8 +1,16 @@
import { arktypeResolver } from "@hookform/resolvers/arktype"; import { arktypeResolver } from "@hookform/resolvers/arktype";
import { type } from "arktype"; import { type } from "arktype";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form"; import {
import { Input } from "~/components/ui/input"; Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "~/client/components/ui/form";
import { Input } from "~/client/components/ui/input";
const restoreSnapshotFormSchema = type({ const restoreSnapshotFormSchema = type({
path: "string?", path: "string?",

View File

@@ -2,18 +2,18 @@ import { useQuery } from "@tanstack/react-query";
import { Database, RotateCcw } from "lucide-react"; import { Database, RotateCcw } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { listRepositories } from "~/api-client/sdk.gen"; import { listRepositories } from "~/client/api-client/sdk.gen";
import { listRepositoriesOptions } from "~/api-client/@tanstack/react-query.gen"; import { listRepositoriesOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { CreateRepositoryDialog } from "~/components/create-repository-dialog"; import { CreateRepositoryDialog } from "~/client/components/create-repository-dialog";
import { RepositoryIcon } from "~/components/repository-icon"; import { RepositoryIcon } from "~/client/components/repository-icon";
import { Button } from "~/components/ui/button"; import { Button } from "~/client/components/ui/button";
import { Card } from "~/components/ui/card"; import { Card } from "~/client/components/ui/card";
import { Input } from "~/components/ui/input"; import { Input } from "~/client/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
import type { Route } from "./+types/repositories"; import type { Route } from "./+types/repositories";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
import { EmptyState } from "~/components/empty-state"; import { EmptyState } from "~/client/components/empty-state";
export function meta(_: Route.MetaArgs) { export function meta(_: Route.MetaArgs) {
return [ return [
@@ -79,13 +79,13 @@ export default function Repositories({ loaderData }: Route.ComponentProps) {
<div className="flex flex-col lg:flex-row items-stretch lg:items-center gap-2 md:justify-between p-4 bg-card-header py-4"> <div className="flex flex-col lg:flex-row items-stretch lg:items-center gap-2 md:justify-between p-4 bg-card-header py-4">
<span className="flex flex-col sm:flex-row items-stretch md:items-center gap-0 flex-wrap "> <span className="flex flex-col sm:flex-row items-stretch md:items-center gap-0 flex-wrap ">
<Input <Input
className="w-full lg:w-[180px] min-w-[180px] mr-[-1px] mt-[-1px]" className="w-full lg:w-[180px] min-w-[180px] -mr-px -mt-px"
placeholder="Search repositories…" placeholder="Search repositories…"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
/> />
<Select value={statusFilter} onValueChange={setStatusFilter}> <Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-full lg:w-[180px] min-w-[180px] mr-[-1px] mt-[-1px]"> <SelectTrigger className="w-full lg:w-[180px] min-w-[180px] -mr-px -mt-px">
<SelectValue placeholder="All status" /> <SelectValue placeholder="All status" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -95,7 +95,7 @@ export default function Repositories({ loaderData }: Route.ComponentProps) {
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={backendFilter} onValueChange={setBackendFilter}> <Select value={backendFilter} onValueChange={setBackendFilter}>
<SelectTrigger className="w-full lg:w-[180px] min-w-[180px] mt-[-1px]"> <SelectTrigger className="w-full lg:w-[180px] min-w-[180px] -mt-px">
<SelectValue placeholder="All backends" /> <SelectValue placeholder="All backends" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@@ -7,8 +7,8 @@ import {
doctorRepositoryMutation, doctorRepositoryMutation,
getRepositoryOptions, getRepositoryOptions,
listSnapshotsOptions, listSnapshotsOptions,
} from "~/api-client/@tanstack/react-query.gen"; } from "~/client/api-client/@tanstack/react-query.gen";
import { Button } from "~/components/ui/button"; import { Button } from "~/client/components/ui/button";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -17,12 +17,12 @@ import {
AlertDialogDescription, AlertDialogDescription,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "~/components/ui/alert-dialog"; } from "~/client/components/ui/alert-dialog";
import { parseError } from "~/lib/errors"; import { parseError } from "~/client/lib/errors";
import { getRepository } from "~/api-client/sdk.gen"; import { getRepository } from "~/client/api-client/sdk.gen";
import type { Route } from "./+types/repository-details"; import type { Route } from "./+types/repository-details";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
import { RepositoryInfoTabContent } from "../tabs/info"; import { RepositoryInfoTabContent } from "../tabs/info";
import { RepositorySnapshotsTabContent } from "../tabs/snapshots"; import { RepositorySnapshotsTabContent } from "../tabs/snapshots";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";

View File

@@ -1,10 +1,10 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { redirect, useParams } from "react-router"; import { redirect, useParams } from "react-router";
import { listSnapshotFilesOptions } from "~/api-client/@tanstack/react-query.gen"; import { listSnapshotFilesOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
import { RestoreSnapshotDialog } from "../components/restore-snapshot-dialog"; import { RestoreSnapshotDialog } from "../components/restore-snapshot-dialog";
import { SnapshotFileBrowser } from "~/modules/backups/components/snapshot-file-browser"; import { SnapshotFileBrowser } from "~/client/modules/backups/components/snapshot-file-browser";
import { getSnapshotDetails } from "~/api-client"; import { getSnapshotDetails } from "~/client/api-client";
import type { Route } from "./+types/snapshot-details"; import type { Route } from "./+types/snapshot-details";
export function meta({ params }: Route.MetaArgs) { export function meta({ params }: Route.MetaArgs) {

View File

@@ -1,5 +1,5 @@
import { Card } from "~/components/ui/card"; import { Card } from "~/client/components/ui/card";
import type { Repository } from "~/lib/types"; import type { Repository } from "~/client/lib/types";
type Props = { type Props = {
repository: Repository; repository: Repository;

View File

@@ -1,13 +1,13 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Database } from "lucide-react"; import { Database } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { listSnapshotsOptions } from "~/api-client/@tanstack/react-query.gen"; import { listSnapshotsOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { SnapshotsTable } from "~/components/snapshots-table"; import { SnapshotsTable } from "~/client/components/snapshots-table";
import { Button } from "~/components/ui/button"; import { Button } from "~/client/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
import { Input } from "~/components/ui/input"; import { Input } from "~/client/components/ui/input";
import { Table, TableBody, TableCell, TableRow } from "~/components/ui/table"; import { Table, TableBody, TableCell, TableRow } from "~/client/components/ui/table";
import type { Repository, Snapshot } from "~/lib/types"; import type { Repository, Snapshot } from "~/client/lib/types";
type Props = { type Props = {
repository: Repository; repository: Repository;
@@ -83,7 +83,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
<div className="absolute inset-0 animate-pulse"> <div className="absolute inset-0 animate-pulse">
<div className="w-32 h-32 rounded-full bg-primary/10 blur-2xl" /> <div className="w-32 h-32 rounded-full bg-primary/10 blur-2xl" />
</div> </div>
<div className="relative flex items-center justify-center w-32 h-32 rounded-full bg-gradient-to-br from-primary/20 to-primary/5 border-2 border-primary/20"> <div className="relative flex items-center justify-center w-32 h-32 rounded-full bg-linear-to-br from-primary/20 to-primary/5 border-2 border-primary/20">
<Database className="w-16 h-16 text-primary/70" strokeWidth={1.5} /> <Database className="w-16 h-16 text-primary/70" strokeWidth={1.5} />
</div> </div>
</div> </div>
@@ -110,7 +110,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Input <Input
className="w-full lg:w-[240px]" className="w-full lg:w-60"
placeholder="Search snapshots..." placeholder="Search snapshots..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}

View File

@@ -3,13 +3,8 @@ import { Download, KeyRound, User } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { toast } from "sonner"; import { toast } from "sonner";
import { import { Button } from "~/client/components/ui/button";
changePasswordMutation, import { Card, CardContent, CardDescription, CardTitle } from "~/client/components/ui/card";
downloadResticPasswordMutation,
logoutMutation,
} from "~/api-client/@tanstack/react-query.gen";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardDescription, CardTitle } from "~/components/ui/card";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -18,11 +13,16 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "~/components/ui/dialog"; } from "~/client/components/ui/dialog";
import { Input } from "~/components/ui/input"; import { Input } from "~/client/components/ui/input";
import { Label } from "~/components/ui/label"; import { Label } from "~/client/components/ui/label";
import { appContext } from "~/context"; import { appContext } from "~/context";
import type { Route } from "./+types/settings"; import type { Route } from "./+types/settings";
import {
changePasswordMutation,
downloadResticPasswordMutation,
logoutMutation,
} from "~/client/api-client/@tanstack/react-query.gen";
export function meta(_: Route.MetaArgs) { export function meta(_: Route.MetaArgs) {
return [ return [

View File

@@ -2,11 +2,11 @@ import { useMutation } from "@tanstack/react-query";
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
import { HeartIcon } from "lucide-react"; import { HeartIcon } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { healthCheckVolumeMutation, updateVolumeMutation } from "~/api-client/@tanstack/react-query.gen"; import { healthCheckVolumeMutation, updateVolumeMutation } from "~/client/api-client/@tanstack/react-query.gen";
import { OnOff } from "~/components/onoff"; import { OnOff } from "~/client/components/onoff";
import { Button } from "~/components/ui/button"; import { Button } from "~/client/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
import type { Volume } from "~/lib/types"; import type { Volume } from "~/client/lib/types";
type Props = { type Props = {
volume: Volume; volume: Volume;
@@ -54,7 +54,7 @@ export const HealthchecksCard = ({ volume }: Props) => {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex flex-col flex-1 justify-start"> <div className="flex flex-col flex-1 justify-start">
{volume.lastError && <span className="text-sm text-red-500 break-words">{volume.lastError}</span>} {volume.lastError && <span className="text-sm text-red-500 wrap-break-word">{volume.lastError}</span>}
{volume.status === "mounted" && <span className="text-md text-emerald-500">Healthy</span>} {volume.status === "mounted" && <span className="text-md text-emerald-500">Healthy</span>}
{volume.status !== "unmounted" && ( {volume.status !== "unmounted" && (
<span className="text-xs text-muted-foreground mb-4">Checked {timeAgo || "never"}</span> <span className="text-xs text-muted-foreground mb-4">Checked {timeAgo || "never"}</span>

View File

@@ -3,10 +3,10 @@
import { HardDrive, Unplug } from "lucide-react"; import { HardDrive, Unplug } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { Label, Pie, PieChart } from "recharts"; import { Label, Pie, PieChart } from "recharts";
import { ByteSize } from "~/components/bytes-size"; import { ByteSize } from "~/client/components/bytes-size";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "~/components/ui/chart"; import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "~/client/components/ui/chart";
import type { StatFs } from "~/lib/types"; import type { StatFs } from "~/client/lib/types";
type Props = { type Props = {
statfs: StatFs; statfs: StatFs;

View File

@@ -2,15 +2,9 @@ import { useMutation, useQuery } from "@tanstack/react-query";
import { useNavigate, useParams, useSearchParams } from "react-router"; import { useNavigate, useParams, useSearchParams } from "react-router";
import { toast } from "sonner"; import { toast } from "sonner";
import { useState } from "react"; import { useState } from "react";
import { import { StatusDot } from "~/client/components/status-dot";
deleteVolumeMutation, import { Button } from "~/client/components/ui/button";
getVolumeOptions, import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
mountVolumeMutation,
unmountVolumeMutation,
} from "~/api-client/@tanstack/react-query.gen";
import { StatusDot } from "~/components/status-dot";
import { Button } from "~/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -19,17 +13,23 @@ import {
AlertDialogDescription, AlertDialogDescription,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "~/components/ui/alert-dialog"; } from "~/client/components/ui/alert-dialog";
import { VolumeIcon } from "~/components/volume-icon"; import { VolumeIcon } from "~/client/components/volume-icon";
import { parseError } from "~/lib/errors"; import { parseError } from "~/client/lib/errors";
import { cn } from "~/lib/utils"; import { cn } from "~/client/lib/utils";
import type { Route } from "./+types/volume-details"; import type { Route } from "./+types/volume-details";
import { getVolume } from "~/api-client";
import { VolumeInfoTabContent } from "../tabs/info"; import { VolumeInfoTabContent } from "../tabs/info";
import { FilesTabContent } from "../tabs/files"; import { FilesTabContent } from "../tabs/files";
import { DockerTabContent } from "../tabs/docker"; import { DockerTabContent } from "../tabs/docker";
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
import { useSystemInfo } from "~/hooks/use-system-info"; import { useSystemInfo } from "~/client/hooks/use-system-info";
import { getVolume } from "~/client/api-client";
import {
deleteVolumeMutation,
getVolumeOptions,
mountVolumeMutation,
unmountVolumeMutation,
} from "~/client/api-client/@tanstack/react-query.gen";
export function meta({ params }: Route.MetaArgs) { export function meta({ params }: Route.MetaArgs) {
return [ return [
@@ -42,7 +42,7 @@ export function meta({ params }: Route.MetaArgs) {
} }
export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => { export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
const volume = await getVolume({ path: { name: params.name ?? "" } }); const volume = await getVolume({ path: { name: params.name } });
if (volume.data) return volume.data; if (volume.data) return volume.data;
}; };

View File

@@ -2,18 +2,18 @@ import { useQuery } from "@tanstack/react-query";
import { HardDrive, RotateCcw } from "lucide-react"; import { HardDrive, RotateCcw } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { listVolumes } from "~/api-client"; import { CreateVolumeDialog } from "~/client/components/create-volume-dialog";
import { listVolumesOptions } from "~/api-client/@tanstack/react-query.gen"; import { EmptyState } from "~/client/components/empty-state";
import { CreateVolumeDialog } from "~/components/create-volume-dialog"; import { StatusDot } from "~/client/components/status-dot";
import { EmptyState } from "~/components/empty-state"; import { Button } from "~/client/components/ui/button";
import { StatusDot } from "~/components/status-dot"; import { Card } from "~/client/components/ui/card";
import { Button } from "~/components/ui/button"; import { Input } from "~/client/components/ui/input";
import { Card } from "~/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
import { Input } from "~/components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"; import { VolumeIcon } from "~/client/components/volume-icon";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
import { VolumeIcon } from "~/components/volume-icon";
import type { Route } from "./+types/volumes"; import type { Route } from "./+types/volumes";
import { listVolumes } from "~/client/api-client";
import { listVolumesOptions } from "~/client/api-client/@tanstack/react-query.gen";
export function meta(_: Route.MetaArgs) { export function meta(_: Route.MetaArgs) {
return [ return [
@@ -79,13 +79,13 @@ export default function Volumes({ loaderData }: Route.ComponentProps) {
<div className="flex flex-col lg:flex-row items-stretch lg:items-center gap-2 md:justify-between p-4 bg-card-header py-4"> <div className="flex flex-col lg:flex-row items-stretch lg:items-center gap-2 md:justify-between p-4 bg-card-header py-4">
<span className="flex flex-col sm:flex-row items-stretch md:items-center gap-0 flex-wrap "> <span className="flex flex-col sm:flex-row items-stretch md:items-center gap-0 flex-wrap ">
<Input <Input
className="w-full lg:w-[180px] min-w-[180px] mr-[-1px] mt-[-1px]" className="w-full lg:w-[180px] min-w-[180px] -mr-px -mt-px"
placeholder="Search volumes…" placeholder="Search volumes…"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
/> />
<Select value={statusFilter} onValueChange={setStatusFilter}> <Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-full lg:w-[180px] min-w-[180px] mr-[-1px] mt-[-1px]"> <SelectTrigger className="w-full lg:w-[180px] min-w-[180px] -mr-px -mt-px">
<SelectValue placeholder="All status" /> <SelectValue placeholder="All status" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -95,7 +95,7 @@ export default function Volumes({ loaderData }: Route.ComponentProps) {
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={backendFilter} onValueChange={setBackendFilter}> <Select value={backendFilter} onValueChange={setBackendFilter}>
<SelectTrigger className="w-full lg:w-[180px] min-w-[180px] mt-[-1px]"> <SelectTrigger className="w-full lg:w-[180px] min-w-[180px] -mt-px">
<SelectValue placeholder="All backends" /> <SelectValue placeholder="All backends" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@@ -1,11 +1,11 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Unplug } from "lucide-react"; import { Unplug } from "lucide-react";
import * as YML from "yaml"; import * as YML from "yaml";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; import { getContainersUsingVolumeOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { CodeBlock } from "~/components/ui/code-block"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"; import { CodeBlock } from "~/client/components/ui/code-block";
import type { Volume } from "~/lib/types"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
import { getContainersUsingVolumeOptions } from "../../../api-client/@tanstack/react-query.gen"; import type { Volume } from "~/client/lib/types";
type Props = { type Props = {
volume: Volume; volume: Volume;
@@ -52,7 +52,7 @@ export const DockerTabContent = ({ volume }: Props) => {
}; };
return ( return (
<div className="grid gap-4 xl:grid-cols-[minmax(0,_1fr)_minmax(0,_1fr)]"> <div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Plug-and-play Docker integration</CardTitle> <CardTitle>Plug-and-play Docker integration</CardTitle>

View File

@@ -1,7 +1,7 @@
import { FolderOpen } from "lucide-react"; import { FolderOpen } from "lucide-react";
import { VolumeFileBrowser } from "~/components/volume-file-browser"; import { VolumeFileBrowser } from "~/client/components/volume-file-browser";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
import type { Volume } from "~/lib/types"; import type { Volume } from "~/client/lib/types";
type Props = { type Props = {
volume: Volume; volume: Volume;

View File

@@ -1,8 +1,7 @@
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { updateVolumeMutation } from "~/api-client/@tanstack/react-query.gen"; import { CreateVolumeForm, type FormValues } from "~/client/components/create-volume-form";
import { CreateVolumeForm, type FormValues } from "~/components/create-volume-form";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -12,11 +11,12 @@ import {
AlertDialogFooter, AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "~/components/ui/alert-dialog"; } from "~/client/components/ui/alert-dialog";
import { Card } from "~/components/ui/card"; import { Card } from "~/client/components/ui/card";
import type { StatFs, Volume } from "~/lib/types"; import type { StatFs, Volume } from "~/client/lib/types";
import { HealthchecksCard } from "../components/healthchecks-card"; import { HealthchecksCard } from "../components/healthchecks-card";
import { StorageChart } from "../components/storage-chart"; import { StorageChart } from "../components/storage-chart";
import { updateVolumeMutation } from "~/client/api-client/@tanstack/react-query.gen";
type Props = { type Props = {
volume: Volume; volume: Volume;
@@ -57,7 +57,7 @@ export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
return ( return (
<> <>
<div className="grid gap-4 xl:grid-cols-[minmax(0,_2.3fr)_minmax(320px,_1fr)]"> <div className="grid gap-4 xl:grid-cols-[minmax(0,2.3fr)_minmax(320px,1fr)]">
<Card className="p-6"> <Card className="p-6">
<CreateVolumeForm <CreateVolumeForm
initialValues={{ ...volume, ...volume.config }} initialValues={{ ...volume, ...volume.config }}

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