refactor: simplify dtos and improve type saftey in json returns

This commit is contained in:
Nicolas Meienberger
2025-10-29 18:28:00 +01:00
parent d1c1adaba7
commit b188a84af3
26 changed files with 667 additions and 751 deletions

View File

@@ -12,7 +12,6 @@ import type {
LogoutResponses,
GetMeData,
GetMeResponses,
GetMeErrors,
GetStatusData,
GetStatusResponses,
ListVolumesData,
@@ -34,16 +33,13 @@ import type {
GetContainersUsingVolumeErrors,
MountVolumeData,
MountVolumeResponses,
MountVolumeErrors,
UnmountVolumeData,
UnmountVolumeResponses,
UnmountVolumeErrors,
HealthCheckVolumeData,
HealthCheckVolumeResponses,
HealthCheckVolumeErrors,
ListFilesData,
ListFilesResponses,
ListFilesErrors,
ListRepositoriesData,
ListRepositoriesResponses,
CreateRepositoryData,
@@ -130,7 +126,7 @@ export const logout = <ThrowOnError extends boolean = false>(options?: Options<L
* Get current authenticated user
*/
export const getMe = <ThrowOnError extends boolean = false>(options?: Options<GetMeData, ThrowOnError>) => {
return (options?.client ?? _heyApiClient).get<GetMeResponses, GetMeErrors, ThrowOnError>({
return (options?.client ?? _heyApiClient).get<GetMeResponses, unknown, ThrowOnError>({
url: "/api/v1/auth/me",
...options,
});
@@ -246,7 +242,7 @@ export const getContainersUsingVolume = <ThrowOnError extends boolean = false>(
* Mount a volume
*/
export const mountVolume = <ThrowOnError extends boolean = false>(options: Options<MountVolumeData, ThrowOnError>) => {
return (options.client ?? _heyApiClient).post<MountVolumeResponses, MountVolumeErrors, ThrowOnError>({
return (options.client ?? _heyApiClient).post<MountVolumeResponses, unknown, ThrowOnError>({
url: "/api/v1/volumes/{name}/mount",
...options,
});
@@ -258,7 +254,7 @@ export const mountVolume = <ThrowOnError extends boolean = false>(options: Optio
export const unmountVolume = <ThrowOnError extends boolean = false>(
options: Options<UnmountVolumeData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).post<UnmountVolumeResponses, UnmountVolumeErrors, ThrowOnError>({
return (options.client ?? _heyApiClient).post<UnmountVolumeResponses, unknown, ThrowOnError>({
url: "/api/v1/volumes/{name}/unmount",
...options,
});
@@ -280,7 +276,7 @@ export const healthCheckVolume = <ThrowOnError extends boolean = false>(
* List files in a volume directory
*/
export const listFiles = <ThrowOnError extends boolean = false>(options: Options<ListFilesData, ThrowOnError>) => {
return (options.client ?? _heyApiClient).get<ListFilesResponses, ListFilesErrors, ThrowOnError>({
return (options.client ?? _heyApiClient).get<ListFilesResponses, unknown, ThrowOnError>({
url: "/api/v1/volumes/{name}/files",
...options,
});

View File

@@ -23,8 +23,9 @@ export type RegisterResponses = {
*/
201: {
message: string;
user: {
id: string;
success: boolean;
user?: {
id: number;
username: string;
};
};
@@ -55,8 +56,9 @@ export type LoginResponses = {
*/
200: {
message: string;
user: {
id: string;
success: boolean;
user?: {
id: number;
username: string;
};
};
@@ -76,7 +78,7 @@ export type LogoutResponses = {
* Logout successful
*/
200: {
message: string;
success: boolean;
};
};
@@ -89,21 +91,15 @@ export type GetMeData = {
url: "/api/v1/auth/me";
};
export type GetMeErrors = {
/**
* Not authenticated
*/
401: unknown;
};
export type GetMeResponses = {
/**
* Current user information
*/
200: {
message: string;
user: {
id: string;
success: boolean;
user?: {
id: number;
username: string;
};
};
@@ -232,11 +228,46 @@ export type CreateVolumeResponses = {
* Volume created successfully
*/
201: {
message: string;
volume: {
name: string;
path: string;
};
autoRemount: boolean;
config:
| {
backend: "directory";
}
| {
backend: "nfs";
exportPath: string;
server: string;
version: "3" | "4" | "4.1";
port?: number;
}
| {
backend: "smb";
password: string;
server: string;
share: string;
username: string;
vers?: "1.0" | "2.0" | "2.1" | "3.0";
port?: number;
domain?: string;
}
| {
backend: "webdav";
path: string;
server: string;
port?: number;
password?: string;
ssl?: boolean;
username?: string;
};
createdAt: number;
id: number;
lastError: string | null;
lastHealthCheck: number;
name: string;
path: string;
status: "error" | "mounted" | "unmounted";
type: "directory" | "nfs" | "smb" | "webdav";
updatedAt: number;
};
};
@@ -438,49 +469,46 @@ export type UpdateVolumeResponses = {
* Volume updated successfully
*/
200: {
message: string;
volume: {
autoRemount: boolean;
config:
| {
backend: "directory";
}
| {
backend: "nfs";
exportPath: string;
server: string;
version: "3" | "4" | "4.1";
port?: number;
}
| {
backend: "smb";
password: string;
server: string;
share: string;
username: string;
vers?: "1.0" | "2.0" | "2.1" | "3.0";
port?: number;
domain?: string;
}
| {
backend: "webdav";
path: string;
server: string;
port?: number;
password?: string;
ssl?: boolean;
username?: string;
};
createdAt: number;
id: number;
lastError: string | null;
lastHealthCheck: number;
name: string;
path: string;
status: "error" | "mounted" | "unmounted";
type: "directory" | "nfs" | "smb" | "webdav";
updatedAt: number;
};
autoRemount: boolean;
config:
| {
backend: "directory";
}
| {
backend: "nfs";
exportPath: string;
server: string;
version: "3" | "4" | "4.1";
port?: number;
}
| {
backend: "smb";
password: string;
server: string;
share: string;
username: string;
vers?: "1.0" | "2.0" | "2.1" | "3.0";
port?: number;
domain?: string;
}
| {
backend: "webdav";
path: string;
server: string;
port?: number;
password?: string;
ssl?: boolean;
username?: string;
};
createdAt: number;
id: number;
lastError: string | null;
lastHealthCheck: number;
name: string;
path: string;
status: "error" | "mounted" | "unmounted";
type: "directory" | "nfs" | "smb" | "webdav";
updatedAt: number;
};
};
@@ -506,14 +534,12 @@ export type GetContainersUsingVolumeResponses = {
/**
* List of containers using the volume
*/
200: {
containers: Array<{
id: string;
image: string;
name: string;
state: string;
}>;
};
200: Array<{
id: string;
image: string;
name: string;
state: string;
}>;
};
export type GetContainersUsingVolumeResponse =
@@ -528,13 +554,6 @@ export type MountVolumeData = {
url: "/api/v1/volumes/{name}/mount";
};
export type MountVolumeErrors = {
/**
* Volume not found
*/
404: unknown;
};
export type MountVolumeResponses = {
/**
* Volume mounted successfully
@@ -556,13 +575,6 @@ export type UnmountVolumeData = {
url: "/api/v1/volumes/{name}/unmount";
};
export type UnmountVolumeErrors = {
/**
* Volume not found
*/
404: unknown;
};
export type UnmountVolumeResponses = {
/**
* Volume unmounted successfully
@@ -617,13 +629,6 @@ export type ListFilesData = {
url: "/api/v1/volumes/{name}/files";
};
export type ListFilesErrors = {
/**
* Volume not found
*/
404: unknown;
};
export type ListFilesResponses = {
/**
* List of files in the volume
@@ -653,31 +658,29 @@ export type ListRepositoriesResponses = {
/**
* List of repositories
*/
200: {
repositories: Array<{
compressionMode: "auto" | "better" | "fastest" | "max" | "off" | null;
config:
| {
accessKeyId: string;
backend: "s3";
bucket: string;
endpoint: string;
secretAccessKey: string;
}
| {
backend: "local";
path: string;
};
createdAt: number;
id: string;
lastChecked: number | null;
lastError: string | null;
name: string;
status: "error" | "healthy" | "unknown" | null;
type: "local" | "s3";
updatedAt: number;
}>;
};
200: Array<{
compressionMode: "auto" | "better" | "fastest" | "max" | "off" | null;
config:
| {
accessKeyId: string;
backend: "s3";
bucket: string;
endpoint: string;
secretAccessKey: string;
}
| {
backend: "local";
path: string;
};
createdAt: number;
id: string;
lastChecked: number | null;
lastError: string | null;
name: string;
status: "error" | "healthy" | "unknown" | null;
type: "local" | "s3";
updatedAt: number;
}>;
};
export type ListRepositoriesResponse = ListRepositoriesResponses[keyof ListRepositoriesResponses];
@@ -753,29 +756,27 @@ export type GetRepositoryResponses = {
* Repository details
*/
200: {
repository: {
compressionMode: "auto" | "better" | "fastest" | "max" | "off" | null;
config:
| {
accessKeyId: string;
backend: "s3";
bucket: string;
endpoint: string;
secretAccessKey: string;
}
| {
backend: "local";
path: string;
};
createdAt: number;
id: string;
lastChecked: number | null;
lastError: string | null;
name: string;
status: "error" | "healthy" | "unknown" | null;
type: "local" | "s3";
updatedAt: number;
};
compressionMode: "auto" | "better" | "fastest" | "max" | "off" | null;
config:
| {
accessKeyId: string;
backend: "s3";
bucket: string;
endpoint: string;
secretAccessKey: string;
}
| {
backend: "local";
path: string;
};
createdAt: number;
id: string;
lastChecked: number | null;
lastError: string | null;
name: string;
status: "error" | "healthy" | "unknown" | null;
type: "local" | "s3";
updatedAt: number;
};
};
@@ -818,32 +819,30 @@ export type ListBackupSchedulesResponses = {
/**
* List of backup schedules
*/
200: {
schedules: Array<{
createdAt: number;
cronExpression: string;
enabled: boolean;
excludePatterns: Array<string> | null;
id: number;
includePatterns: Array<string> | null;
lastBackupAt: number | null;
lastBackupError: string | null;
lastBackupStatus: "error" | "success" | null;
nextBackupAt: number | null;
repositoryId: string;
retentionPolicy: {
keepDaily?: number;
keepHourly?: number;
keepLast?: number;
keepMonthly?: number;
keepWeekly?: number;
keepWithinDuration?: string;
keepYearly?: number;
} | null;
updatedAt: number;
volumeId: number;
}>;
};
200: Array<{
createdAt: number;
cronExpression: string;
enabled: boolean;
excludePatterns: Array<string> | null;
id: number;
includePatterns: Array<string> | null;
lastBackupAt: number | null;
lastBackupError: string | null;
lastBackupStatus: "error" | "success" | null;
nextBackupAt: number | null;
repositoryId: string;
retentionPolicy: {
keepDaily?: number;
keepHourly?: number;
keepLast?: number;
keepMonthly?: number;
keepWeekly?: number;
keepWithinDuration?: string;
keepYearly?: number;
} | null;
updatedAt: number;
volumeId: number;
}>;
};
export type ListBackupSchedulesResponse = ListBackupSchedulesResponses[keyof ListBackupSchedulesResponses];
@@ -877,31 +876,28 @@ export type CreateBackupScheduleResponses = {
* Backup schedule created successfully
*/
201: {
message: string;
schedule: {
createdAt: number;
cronExpression: string;
enabled: boolean;
excludePatterns: Array<string> | null;
id: number;
includePatterns: Array<string> | null;
lastBackupAt: number | null;
lastBackupError: string | null;
lastBackupStatus: "error" | "success" | null;
nextBackupAt: number | null;
repositoryId: string;
retentionPolicy: {
keepDaily?: number;
keepHourly?: number;
keepLast?: number;
keepMonthly?: number;
keepWeekly?: number;
keepWithinDuration?: string;
keepYearly?: number;
} | null;
updatedAt: number;
volumeId: number;
};
createdAt: number;
cronExpression: string;
enabled: boolean;
excludePatterns: Array<string> | null;
id: number;
includePatterns: Array<string> | null;
lastBackupAt: number | null;
lastBackupError: string | null;
lastBackupStatus: "error" | "success" | null;
nextBackupAt: number | null;
repositoryId: string;
retentionPolicy: {
keepDaily?: number;
keepHourly?: number;
keepLast?: number;
keepMonthly?: number;
keepWeekly?: number;
keepWithinDuration?: string;
keepYearly?: number;
} | null;
updatedAt: number;
volumeId: number;
};
};
@@ -921,7 +917,7 @@ export type DeleteBackupScheduleResponses = {
* Backup schedule deleted successfully
*/
200: {
message: string;
success: boolean;
};
};
@@ -941,30 +937,28 @@ export type GetBackupScheduleResponses = {
* Backup schedule details
*/
200: {
schedule: {
createdAt: number;
cronExpression: string;
enabled: boolean;
excludePatterns: Array<string> | null;
id: number;
includePatterns: Array<string> | null;
lastBackupAt: number | null;
lastBackupError: string | null;
lastBackupStatus: "error" | "success" | null;
nextBackupAt: number | null;
repositoryId: string;
retentionPolicy: {
keepDaily?: number;
keepHourly?: number;
keepLast?: number;
keepMonthly?: number;
keepWeekly?: number;
keepWithinDuration?: string;
keepYearly?: number;
} | null;
updatedAt: number;
volumeId: number;
};
createdAt: number;
cronExpression: string;
enabled: boolean;
excludePatterns: Array<string> | null;
id: number;
includePatterns: Array<string> | null;
lastBackupAt: number | null;
lastBackupError: string | null;
lastBackupStatus: "error" | "success" | null;
nextBackupAt: number | null;
repositoryId: string;
retentionPolicy: {
keepDaily?: number;
keepHourly?: number;
keepLast?: number;
keepMonthly?: number;
keepWeekly?: number;
keepWithinDuration?: string;
keepYearly?: number;
} | null;
updatedAt: number;
volumeId: number;
};
};
@@ -1000,31 +994,28 @@ export type UpdateBackupScheduleResponses = {
* Backup schedule updated successfully
*/
200: {
message: string;
schedule: {
createdAt: number;
cronExpression: string;
enabled: boolean;
excludePatterns: Array<string> | null;
id: number;
includePatterns: Array<string> | null;
lastBackupAt: number | null;
lastBackupError: string | null;
lastBackupStatus: "error" | "success" | null;
nextBackupAt: number | null;
repositoryId: string;
retentionPolicy: {
keepDaily?: number;
keepHourly?: number;
keepLast?: number;
keepMonthly?: number;
keepWeekly?: number;
keepWithinDuration?: string;
keepYearly?: number;
} | null;
updatedAt: number;
volumeId: number;
};
createdAt: number;
cronExpression: string;
enabled: boolean;
excludePatterns: Array<string> | null;
id: number;
includePatterns: Array<string> | null;
lastBackupAt: number | null;
lastBackupError: string | null;
lastBackupStatus: "error" | "success" | null;
nextBackupAt: number | null;
repositoryId: string;
retentionPolicy: {
keepDaily?: number;
keepHourly?: number;
keepLast?: number;
keepMonthly?: number;
keepWeekly?: number;
keepWithinDuration?: string;
keepYearly?: number;
} | null;
updatedAt: number;
volumeId: number;
};
};
@@ -1086,8 +1077,7 @@ export type RunBackupNowResponses = {
* Backup started successfully
*/
200: {
backupStarted: boolean;
message: string;
success: boolean;
};
};

View File

@@ -1,355 +1,298 @@
import * as React from "react"
import * as RechartsPrimitive from "recharts"
// @ts-nocheck
// biome-ignore-all lint: reason
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "~/lib/utils"
import { cn } from "~/lib/utils";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> });
};
type ChartContextProps = {
config: ChartConfig
}
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null)
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext)
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context
return context;
}
function ChartContainer({
id,
className,
children,
config,
...props
id,
className,
children,
config,
...props
}: React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
config: ChartConfig;
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
}) {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
<div
data-slot="chart"
data-chart={chartId}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
return (
<ChartContext.Provider value={{ config }}>
<div
data-slot="chart"
data-chart={chartId}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className,
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
}
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color
)
const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color);
if (!colorConfig.length) {
return null
}
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
.map(([key, itemConfig]) => {
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`,
)
.join("\n"),
}}
/>
);
};
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltip = RechartsPrimitive.Tooltip;
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}) {
const { config } = useChart()
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}) {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
const [item] = payload;
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string" ? config[label as keyof typeof config]?.label || label : itemConfig?.label;
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (labelFormatter) {
return <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>;
}
if (!value) {
return null
}
if (!value) {
return null;
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
if (!active || !payload?.length) {
return null
}
if (!active || !payload?.length) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== "dot"
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload
.filter((item) => item.type !== "none")
.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className,
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload
.filter((item) => item.type !== "none")
.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center",
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn("shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)", {
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
})}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center",
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">{itemConfig?.label || item.name}</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
}
const ChartLegend = RechartsPrimitive.Legend
const ChartLegend = RechartsPrimitive.Legend;
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
}: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}) {
const { config } = useChart()
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean;
nameKey?: string;
}) {
const { config } = useChart();
if (!payload?.length) {
return null
}
if (!payload?.length) {
return null;
}
return (
<div
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload
.filter((item) => item.type !== "none")
.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div className={cn("flex items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)}>
{payload
.filter((item) => item.type !== "none")
.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
return (
<div
key={item.value}
className={cn("[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3")}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
}
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
const payloadPayload =
"payload" in payload && typeof payload.payload === "object" && payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key
let configLabelKey: string = key;
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}
export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle };

View File

@@ -6,6 +6,6 @@ export type VolumeStatus = Volume["status"];
export type User = GetMeResponse["user"];
export type Repository = GetRepositoryResponse["repository"];
export type Repository = GetRepositoryResponse;
export type BackupSchedule = GetBackupScheduleResponse["schedule"];
export type BackupSchedule = GetBackupScheduleResponse;

View File

@@ -7,7 +7,7 @@ export const authMiddleware: MiddlewareFunction = async ({ context, request }) =
const isAuthRoute = ["/login", "/onboarding"].includes(new URL(request.url).pathname);
if (!session.data?.user.id && !isAuthRoute) {
if (!session.data?.user?.id && !isAuthRoute) {
const status = await getStatus();
if (!status.data?.hasUsers) {
throw redirect("/onboarding");
@@ -16,7 +16,7 @@ export const authMiddleware: MiddlewareFunction = async ({ context, request }) =
throw redirect("/login");
}
if (session.data?.user.id) {
if (session.data?.user?.id) {
context.set(appContext, { user: session.data.user, hasUsers: true });
if (isAuthRoute) {

View File

@@ -114,7 +114,7 @@ export const CreateScheduleForm = ({ initialValues, onSubmit, volume, loading, s
<SelectValue placeholder="Select a repository" />
</SelectTrigger>
<SelectContent>
{repositoriesData?.repositories.map((repo) => (
{repositoriesData?.map((repo) => (
<SelectItem key={repo.id} value={repo.id}>
<span className="flex items-center gap-2">
<RepositoryIcon backend={repo.type} />

View File

@@ -35,7 +35,7 @@ export const HealthchecksCard = ({ volume }: Props) => {
...updateVolumeMutation(),
onSuccess: (d) => {
toast.success("Volume updated", {
description: `Auto remount is now ${d.volume.autoRemount ? "enabled" : "paused"}.`,
description: `Auto remount is now ${d.autoRemount ? "enabled" : "paused"}.`,
});
},
onError: (error) => {

View File

@@ -51,7 +51,7 @@ export const VolumeBackupsTabContent = ({ volume }: Props) => {
const [isEnabled, setIsEnabled] = useState(existingSchedule?.enabled ?? true);
const repositories = repositoriesData?.repositories || [];
const repositories = repositoriesData || [];
const selectedRepository = repositories.find((r) => r.id === (existingSchedule?.repositoryId ?? ""));
const summary = useMemo(() => {

View File

@@ -38,7 +38,7 @@ export const DockerTabContent = ({ volume }: Props) => {
refetchOnWindowFocus: true,
});
const containers = containersData?.containers || [];
const containers = containersData || [];
const getStateClass = (state: string) => {
switch (state) {

View File

@@ -26,8 +26,8 @@ export function meta(_: Route.MetaArgs) {
export const clientLoader = async () => {
const repositories = await listRepositories();
if (repositories.data) return { repositories: repositories.data.repositories };
return { repositories: [] };
if (repositories.data) return repositories.data;
return [];
};
export default function Repositories({ loaderData }: Route.ComponentProps) {
@@ -52,14 +52,14 @@ export default function Repositories({ loaderData }: Route.ComponentProps) {
});
const filteredRepositories =
data?.repositories.filter((repository) => {
data?.filter((repository) => {
const matchesSearch = repository.name.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus = !statusFilter || repository.status === statusFilter;
const matchesBackend = !backendFilter || repository.type === backendFilter;
return matchesSearch && matchesStatus && matchesBackend;
}) || [];
const hasNoRepositories = data?.repositories.length === 0;
const hasNoRepositories = data?.length === 0;
const hasNoFilteredRepositories = filteredRepositories.length === 0 && !hasNoRepositories;
if (hasNoRepositories) {

View File

@@ -83,8 +83,6 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
return <div>Loading...</div>;
}
const { repository } = data;
return (
<>
<div className="flex items-center justify-between mb-4">
@@ -94,14 +92,14 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
className={cn(
"inline-flex items-center gap-2 px-2 py-1 rounded-md text-xs bg-gray-500/10 text-gray-500",
{
"bg-green-500/10 text-green-500": repository.status === "healthy",
"bg-red-500/10 text-red-500": repository.status === "error",
"bg-green-500/10 text-green-500": data.status === "healthy",
"bg-red-500/10 text-red-500": data.status === "error",
},
)}
>
{repository.status || "unknown"}
{data.status || "unknown"}
</span>
<span className="text-xs bg-primary/10 rounded-md px-2 py-1">{repository.type}</span>
<span className="text-xs bg-primary/10 rounded-md px-2 py-1">{data.type}</span>
</div>
</div>
<div className="flex gap-4">
@@ -117,10 +115,10 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
<TabsTrigger value="snapshots">Snapshots</TabsTrigger>
</TabsList>
<TabsContent value="info">
<RepositoryInfoTabContent repository={data.repository} />
<RepositoryInfoTabContent repository={data} />
</TabsContent>
<TabsContent value="snapshots">
<RepositorySnapshotsTabContent repository={data.repository} />
<RepositorySnapshotsTabContent repository={data} />
</TabsContent>
</Tabs>
</>