mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat: docker usage examples & statfs
This commit is contained in:
@@ -13,7 +13,7 @@ export type ListVolumesResponses = {
|
|||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
volumes: Array<{
|
volumes: Array<{
|
||||||
autoRemount: boolean;
|
autoRemount: 0 | 1;
|
||||||
config:
|
config:
|
||||||
| {
|
| {
|
||||||
backend: "directory";
|
backend: "directory";
|
||||||
@@ -72,7 +72,6 @@ export type CreateVolumeResponses = {
|
|||||||
201: {
|
201: {
|
||||||
message: string;
|
message: string;
|
||||||
volume: {
|
volume: {
|
||||||
createdAt: number;
|
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
@@ -156,29 +155,36 @@ export type GetVolumeResponses = {
|
|||||||
* Volume details
|
* Volume details
|
||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
autoRemount: boolean;
|
statfs: {
|
||||||
config:
|
free: number;
|
||||||
| {
|
total: number;
|
||||||
backend: "directory";
|
used: number;
|
||||||
}
|
};
|
||||||
| {
|
volume: {
|
||||||
backend: "nfs";
|
autoRemount: 0 | 1;
|
||||||
exportPath: string;
|
config:
|
||||||
server: string;
|
| {
|
||||||
version: "3" | "4" | "4.1";
|
backend: "directory";
|
||||||
port?: number | string;
|
}
|
||||||
}
|
| {
|
||||||
| {
|
backend: "nfs";
|
||||||
backend: "smb";
|
exportPath: string;
|
||||||
};
|
server: string;
|
||||||
createdAt: number;
|
version: "3" | "4" | "4.1";
|
||||||
lastError: string;
|
port?: number | string;
|
||||||
lastHealthCheck: number;
|
}
|
||||||
name: string;
|
| {
|
||||||
path: string;
|
backend: "smb";
|
||||||
status: "error" | "mounted" | "unknown" | "unmounted";
|
};
|
||||||
type: "directory" | "nfs" | "smb";
|
createdAt: number;
|
||||||
updatedAt: number;
|
lastError: string;
|
||||||
|
lastHealthCheck: number;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
status: "error" | "mounted" | "unknown" | "unmounted";
|
||||||
|
type: "directory" | "nfs" | "smb";
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -268,7 +274,8 @@ export type MountVolumeResponses = {
|
|||||||
* Volume mounted successfully
|
* Volume mounted successfully
|
||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
message: string;
|
status: "error" | "mounted" | "unmounted";
|
||||||
|
error?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -295,7 +302,8 @@ export type UnmountVolumeResponses = {
|
|||||||
* Volume unmounted successfully
|
* Volume unmounted successfully
|
||||||
*/
|
*/
|
||||||
200: {
|
200: {
|
||||||
message: string;
|
status: "error" | "mounted" | "unmounted";
|
||||||
|
error?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
115
apps/client/app/components/bytes-size.tsx
Normal file
115
apps/client/app/components/bytes-size.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type ByteSizeProps = {
|
||||||
|
bytes: number;
|
||||||
|
base?: 1000 | 1024; // 1000 = SI (KB, MB, ...), 1024 = IEC (KiB, MiB, ...)
|
||||||
|
maximumFractionDigits?: number; // default: 2
|
||||||
|
smartRounding?: boolean; // dynamically reduces decimals for big numbers (default: true)
|
||||||
|
locale?: string | string[]; // e.g., 'en', 'de', or navigator.languages
|
||||||
|
space?: boolean; // space between number and unit (default: true)
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
fallback?: string; // shown if bytes is not a finite number (default: '—')
|
||||||
|
};
|
||||||
|
|
||||||
|
const SI_UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] as const;
|
||||||
|
const IEC_UNITS = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"] as const;
|
||||||
|
|
||||||
|
type FormatBytesResult = {
|
||||||
|
text: string;
|
||||||
|
unit: string;
|
||||||
|
unitIndex: number;
|
||||||
|
numeric: number; // numeric value before formatting (with sign)
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatBytes(
|
||||||
|
bytes: number,
|
||||||
|
options?: {
|
||||||
|
base?: 1000 | 1024;
|
||||||
|
maximumFractionDigits?: number;
|
||||||
|
smartRounding?: boolean;
|
||||||
|
locale?: string | string[];
|
||||||
|
},
|
||||||
|
): FormatBytesResult {
|
||||||
|
const { base = 1000, maximumFractionDigits = 2, smartRounding = true, locale } = options ?? {};
|
||||||
|
|
||||||
|
if (!Number.isFinite(bytes)) {
|
||||||
|
return {
|
||||||
|
text: "—",
|
||||||
|
unit: "",
|
||||||
|
unitIndex: 0,
|
||||||
|
numeric: NaN,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const units = base === 1024 ? IEC_UNITS : SI_UNITS;
|
||||||
|
|
||||||
|
const sign = Math.sign(bytes) || 1;
|
||||||
|
const abs = Math.abs(bytes);
|
||||||
|
|
||||||
|
let idx = 0;
|
||||||
|
if (abs > 0) {
|
||||||
|
idx = Math.floor(Math.log(abs) / Math.log(base));
|
||||||
|
if (!Number.isFinite(idx)) idx = 0;
|
||||||
|
idx = Math.max(0, Math.min(idx, units.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
const numeric = (abs / Math.pow(base, idx)) * sign;
|
||||||
|
|
||||||
|
const maxFrac = (() => {
|
||||||
|
if (!smartRounding) return maximumFractionDigits;
|
||||||
|
const v = Math.abs(numeric);
|
||||||
|
if (v >= 100) return 0;
|
||||||
|
if (v >= 10) return Math.min(1, maximumFractionDigits);
|
||||||
|
return maximumFractionDigits;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const text = new Intl.NumberFormat(locale, {
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: maxFrac,
|
||||||
|
}).format(numeric);
|
||||||
|
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
unit: units[idx],
|
||||||
|
unitIndex: idx,
|
||||||
|
numeric,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ByteSize(props: ByteSizeProps) {
|
||||||
|
const {
|
||||||
|
bytes,
|
||||||
|
base = 1000,
|
||||||
|
maximumFractionDigits = 2,
|
||||||
|
smartRounding = true,
|
||||||
|
locale,
|
||||||
|
space = true,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
fallback = "—",
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const { text, unit } = formatBytes(bytes, {
|
||||||
|
base,
|
||||||
|
maximumFractionDigits,
|
||||||
|
smartRounding,
|
||||||
|
locale,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (text === "—") {
|
||||||
|
return (
|
||||||
|
<span className={className} style={style}>
|
||||||
|
{fallback}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={className} style={style}>
|
||||||
|
{text}
|
||||||
|
{space ? " " : ""}
|
||||||
|
{unit}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
apps/client/app/components/ui/code-block.tsx
Normal file
46
apps/client/app/components/ui/code-block.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import Prism from "prismjs";
|
||||||
|
import "prismjs/themes/prism-twilight.css";
|
||||||
|
import "prismjs/components/prism-yaml";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { copyToClipboard } from "~/utils/clipboard";
|
||||||
|
|
||||||
|
interface CodeBlockProps {
|
||||||
|
code: string;
|
||||||
|
language?: string;
|
||||||
|
filename?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CodeBlock: React.FC<CodeBlockProps> = ({ code, language = "jsx", filename }) => {
|
||||||
|
useEffect(() => {
|
||||||
|
Prism.highlightAll();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
await copyToClipboard(code);
|
||||||
|
toast.success("Code copied to clipboard");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden rounded-sm bg-slate-900 ring-1 ring-white/10">
|
||||||
|
<div className="flex items-center justify-between border-b border-white/10 px-4 py-2 text-xs text-slate-400">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="h-2.5 w-2.5 rounded-full bg-rose-500" />
|
||||||
|
<span className="h-2.5 w-2.5 rounded-full bg-amber-500" />
|
||||||
|
<span className="h-2.5 w-2.5 rounded-full bg-emerald-500" />
|
||||||
|
{filename && <span className="ml-3 font-medium text-slate-300">{filename}</span>}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleCopy()}
|
||||||
|
className="cursor-pointer rounded-md bg-white/5 px-2 py-1 text-[11px] font-medium text-slate-300 ring-1 ring-inset ring-white/10 transition hover:bg-white/10 active:translate-y-px"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre className="overflow-x-auto leading-6 text-xs m-0" style={{ marginTop: 0, marginBottom: 0 }}>
|
||||||
|
<code className={`language-${language}`}>{code}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { GetVolumeResponse } from "~/api-client";
|
import type { GetVolumeResponse } from "~/api-client";
|
||||||
|
|
||||||
export type Volume = GetVolumeResponse;
|
export type Volume = GetVolumeResponse["volume"];
|
||||||
|
export type StatFs = GetVolumeResponse["statfs"];
|
||||||
export type VolumeStatus = Volume["status"];
|
export type VolumeStatus = Volume["status"];
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import { ScanHeartIcon } from "lucide-react";
|
import { ScanHeartIcon } from "lucide-react";
|
||||||
import type { GetVolumeResponse } from "~/api-client";
|
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Card } from "~/components/ui/card";
|
import { Card } from "~/components/ui/card";
|
||||||
import { Switch } from "~/components/ui/switch";
|
import { Switch } from "~/components/ui/switch";
|
||||||
|
import type { Volume } from "~/lib/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
volume: GetVolumeResponse;
|
volume: Volume;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HealthchecksCard = ({ volume }: Props) => {
|
export const HealthchecksCard = ({ volume }: Props) => {
|
||||||
@@ -28,7 +28,7 @@ export const HealthchecksCard = ({ volume }: Props) => {
|
|||||||
)}
|
)}
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
Remount on error
|
Remount on error
|
||||||
<Switch className="ml-auto cursor-pointer" checked={volume.autoRemount} />
|
<Switch className="ml-auto cursor-pointer" checked={Boolean(volume.autoRemount)} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline">Run Health Check</Button>
|
<Button variant="outline">Run Health Check</Button>
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { Card } from "~/components/ui/card";
|
import { Card } from "~/components/ui/card";
|
||||||
import type { Volume } from "~/lib/types";
|
import type { Volume } from "~/lib/types";
|
||||||
import CodeMirror from "@uiw/react-codemirror";
|
|
||||||
import { yaml } from "@codemirror/lang-yaml";
|
|
||||||
import { copilot } from "@uiw/codemirror-theme-copilot";
|
|
||||||
import * as YML from "yaml";
|
import * as YML from "yaml";
|
||||||
|
import { CodeBlock } from "~/components/ui/code-block";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
volume: Volume;
|
volume: Volume;
|
||||||
@@ -27,23 +25,16 @@ export const DockerTabContent = ({ volume }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="grid gap-4 grid-cols-1 lg:grid-cols-3 lg:grid-rows-[auto_1fr]">
|
<div className="grid gap-4 grid-cols-1 lg:grid-cols-3 lg:grid-rows-[auto_1fr]">
|
||||||
<Card className="p-6 lg:col-span-2 lg:row-span-2">
|
<Card className="p-6 lg:col-span-2 lg:row-span-2">
|
||||||
<CodeMirror readOnly={true} value={yamlString} height="200px" extensions={[yaml()]} theme={copilot} />
|
<div className="text-sm text-muted-foreground">
|
||||||
Alternatively, you can use the following command to run a Docker container with the volume mounted:
|
|
||||||
<CodeMirror
|
|
||||||
readOnly={true}
|
|
||||||
value={`docker run -v ${volume.name}:/path/in/container nginx:latest`}
|
|
||||||
height="25px"
|
|
||||||
extensions={[yaml()]}
|
|
||||||
theme={copilot}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
<Card className="p-6 h-full lg:row-span-2">
|
|
||||||
<h3 className="text-lg font-semibold mb-2 text-center">Using the volume with Docker</h3>
|
|
||||||
<div className="text-sm text-muted-foreground mb-4 text-center flex h-full">
|
|
||||||
This volume can be used in your Docker Compose files by referencing it as an external volume. The example
|
This volume can be used in your Docker Compose files by referencing it as an external volume. The example
|
||||||
demonstrates how to mount the volume to a service (nginx in this case). Make sure to adjust the path inside
|
demonstrates how to mount the volume to a service (nginx in this case). Make sure to adjust the path inside
|
||||||
the container to fit your application's needs.
|
the container to fit your application's needs.
|
||||||
</div>
|
</div>
|
||||||
|
<CodeBlock code={yamlString} language="yaml" filename="docker-compose.yml" />
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Alternatively, you can use the following command to run a Docker container with the volume mounted:
|
||||||
|
</div>
|
||||||
|
<CodeBlock code={`docker run -v ${volume.name}:/path/in/container nginx:latest`} />
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { CreateVolumeForm } from "~/components/create-volume-form";
|
import { CreateVolumeForm } from "~/components/create-volume-form";
|
||||||
import { Card } from "~/components/ui/card";
|
import { Card } from "~/components/ui/card";
|
||||||
import { HealthchecksCard } from "../components/healthchecks-card";
|
import { HealthchecksCard } from "../components/healthchecks-card";
|
||||||
import type { Volume } from "~/lib/types";
|
import type { StatFs, Volume } from "~/lib/types";
|
||||||
|
import { ByteSize } from "~/components/bytes-size";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
volume: Volume;
|
volume: Volume;
|
||||||
|
statfs: StatFs;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VolumeInfoTabContent = ({ volume }: Props) => {
|
export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4 grid-cols-1 lg:grid-cols-3 lg:grid-rows-[auto_1fr]">
|
<div className="grid gap-4 grid-cols-1 lg:grid-cols-3 lg:grid-rows-[auto_1fr]">
|
||||||
<Card className="p-6 lg:col-span-2 lg:row-span-2">
|
<Card className="p-6 lg:col-span-2 lg:row-span-2">
|
||||||
@@ -16,6 +18,11 @@ export const VolumeInfoTabContent = ({ volume }: Props) => {
|
|||||||
<HealthchecksCard volume={volume} />
|
<HealthchecksCard volume={volume} />
|
||||||
<Card className="p-6 h-full">
|
<Card className="p-6 h-full">
|
||||||
<h2 className="text-lg font-medium">Volume Information</h2>
|
<h2 className="text-lg font-medium">Volume Information</h2>
|
||||||
|
Total: <ByteSize bytes={statfs.total} />
|
||||||
|
<br />
|
||||||
|
Free: <ByteSize bytes={statfs.free} />
|
||||||
|
<br />
|
||||||
|
Used: <ByteSize bytes={statfs.used} />
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ import { VolumeInfoTabContent } from "~/modules/details/tabs/info";
|
|||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
||||||
import { DockerTabContent } from "~/modules/details/tabs/docker";
|
import { DockerTabContent } from "~/modules/details/tabs/docker";
|
||||||
|
|
||||||
|
export function meta({ params }: Route.MetaArgs) {
|
||||||
|
return [
|
||||||
|
{ title: "Ironmount - " + params.name },
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
content: "Create, manage, monitor, and automate your Docker volumes with ease.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -83,15 +93,18 @@ export default function DetailsPage({ loaderData }: Route.ComponentProps) {
|
|||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
const { volume, statfs } = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-semibold mb-2 text-muted-foreground flex items-center gap-2">
|
<div className="text-sm font-semibold mb-2 text-muted-foreground flex items-center gap-2">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<StatusDot status={data.status} /> {data.status[0].toUpperCase() + data.status.slice(1)}
|
<StatusDot status={volume.status} /> {volume.status[0].toUpperCase() + volume.status.slice(1)}
|
||||||
</span>
|
</span>
|
||||||
<VolumeIcon size={14} backend={data?.config.backend} />
|
<VolumeIcon size={14} backend={volume?.config.backend} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
@@ -99,7 +112,7 @@ export default function DetailsPage({ loaderData }: Route.ComponentProps) {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => mountVol.mutate({ path: { name } })}
|
onClick={() => mountVol.mutate({ path: { name } })}
|
||||||
loading={mountVol.isPending}
|
loading={mountVol.isPending}
|
||||||
className={cn({ hidden: data.status === "mounted" })}
|
className={cn({ hidden: volume.status === "mounted" })}
|
||||||
>
|
>
|
||||||
Mount
|
Mount
|
||||||
</Button>
|
</Button>
|
||||||
@@ -107,7 +120,7 @@ export default function DetailsPage({ loaderData }: Route.ComponentProps) {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => unmountVol.mutate({ path: { name } })}
|
onClick={() => unmountVol.mutate({ path: { name } })}
|
||||||
loading={unmountVol.isPending}
|
loading={unmountVol.isPending}
|
||||||
className={cn({ hidden: data.status !== "mounted" })}
|
className={cn({ hidden: volume.status !== "mounted" })}
|
||||||
>
|
>
|
||||||
Unmount
|
Unmount
|
||||||
</Button>
|
</Button>
|
||||||
@@ -122,10 +135,10 @@ export default function DetailsPage({ loaderData }: Route.ComponentProps) {
|
|||||||
<TabsTrigger value="docker">Docker usage</TabsTrigger>
|
<TabsTrigger value="docker">Docker usage</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="info">
|
<TabsContent value="info">
|
||||||
<VolumeInfoTabContent volume={data} />
|
<VolumeInfoTabContent volume={volume} statfs={statfs} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="docker">
|
<TabsContent value="docker">
|
||||||
<DockerTabContent volume={data} />
|
<DockerTabContent volume={volume} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Copy } from "lucide-react";
|
import { Copy, RotateCcw } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { type ListVolumesResponse, listVolumes } from "~/api-client";
|
import { type ListVolumesResponse, listVolumes } from "~/api-client";
|
||||||
import { listVolumesOptions } from "~/api-client/@tanstack/react-query.gen";
|
import { listVolumesOptions } from "~/api-client/@tanstack/react-query.gen";
|
||||||
import { CreateVolumeDialog } from "~/components/create-volume-dialog";
|
import { CreateVolumeDialog } from "~/components/create-volume-dialog";
|
||||||
import { EditVolumeDialog } from "~/components/edit-volume-dialog";
|
import { EditVolumeDialog } from "~/components/edit-volume-dialog";
|
||||||
|
import { StatusDot } from "~/components/status-dot";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
|
||||||
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
|
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
|
||||||
import { VolumeIcon } from "~/components/volume-icon";
|
import { VolumeIcon } from "~/components/volume-icon";
|
||||||
import type { Route } from "./+types/home";
|
import type { Route } from "./+types/home";
|
||||||
import { StatusDot } from "~/components/status-dot";
|
|
||||||
|
|
||||||
export function meta(_: Route.MetaArgs) {
|
export function meta(_: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
@@ -32,6 +33,15 @@ export const clientLoader = async () => {
|
|||||||
export default function Home({ loaderData }: Route.ComponentProps) {
|
export default function Home({ loaderData }: Route.ComponentProps) {
|
||||||
const [volumeToEdit, setVolumeToEdit] = useState<ListVolumesResponse["volumes"][number]>();
|
const [volumeToEdit, setVolumeToEdit] = useState<ListVolumesResponse["volumes"][number]>();
|
||||||
const [createVolumeOpen, setCreateVolumeOpen] = useState(false);
|
const [createVolumeOpen, setCreateVolumeOpen] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [statusFilter, setStatusFilter] = useState("");
|
||||||
|
const [backendFilter, setBackendFilter] = useState("");
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
setSearchQuery("");
|
||||||
|
setStatusFilter("");
|
||||||
|
setBackendFilter("");
|
||||||
|
};
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -40,6 +50,14 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
|||||||
initialData: loaderData,
|
initialData: loaderData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const filteredVolumes =
|
||||||
|
data?.volumes.filter((volume) => {
|
||||||
|
const matchesSearch = volume.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
const matchesStatus = !statusFilter || volume.status === statusFilter;
|
||||||
|
const matchesBackend = !backendFilter || volume.type === backendFilter;
|
||||||
|
return matchesSearch && matchesStatus && matchesBackend;
|
||||||
|
}) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className="text-3xl font-bold mb-0 uppercase">Ironmount</h1>
|
<h1 className="text-3xl font-bold mb-0 uppercase">Ironmount</h1>
|
||||||
@@ -48,8 +66,13 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
|||||||
</h2>
|
</h2>
|
||||||
<div className="flex items-center gap-2 mt-4 justify-between">
|
<div className="flex items-center gap-2 mt-4 justify-between">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Input className="w-[180px]" placeholder="Search volumes…" />
|
<Input
|
||||||
<Select>
|
className="w-[180px]"
|
||||||
|
placeholder="Search volumes…"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||||
<SelectTrigger className="w-[180px]">
|
<SelectTrigger className="w-[180px]">
|
||||||
<SelectValue placeholder="All status" />
|
<SelectValue placeholder="All status" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -59,7 +82,7 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
|||||||
<SelectItem value="error">Error</SelectItem>
|
<SelectItem value="error">Error</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Select>
|
<Select value={backendFilter} onValueChange={setBackendFilter}>
|
||||||
<SelectTrigger className="w-[180px]">
|
<SelectTrigger className="w-[180px]">
|
||||||
<SelectValue placeholder="All backends" />
|
<SelectValue placeholder="All backends" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -69,6 +92,12 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
|||||||
<SelectItem value="smb">SMB</SelectItem>
|
<SelectItem value="smb">SMB</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
{(searchQuery || statusFilter || backendFilter) && (
|
||||||
|
<Button variant="outline" size="sm" onClick={clearFilters}>
|
||||||
|
<RotateCcw className="h-4 w-4 mr-2" />
|
||||||
|
Clear filters
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<CreateVolumeDialog open={createVolumeOpen} setOpen={setCreateVolumeOpen} />
|
<CreateVolumeDialog open={createVolumeOpen} setOpen={setCreateVolumeOpen} />
|
||||||
</div>
|
</div>
|
||||||
@@ -83,7 +112,7 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data?.volumes.map((volume) => (
|
{filteredVolumes.map((volume) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={volume.name}
|
key={volume.name}
|
||||||
className="hover:bg-accent/50 hover:cursor-pointer"
|
className="hover:bg-accent/50 hover:cursor-pointer"
|
||||||
|
|||||||
25
apps/client/app/utils/clipboard.ts
Normal file
25
apps/client/app/utils/clipboard.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export async function copyToClipboard(textToCopy: string) {
|
||||||
|
// Navigator clipboard api needs a secure context (https)
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
await navigator.clipboard.writeText(textToCopy);
|
||||||
|
} else {
|
||||||
|
// Use the 'out of viewport hidden text area' trick
|
||||||
|
const textArea = document.createElement("textarea");
|
||||||
|
textArea.value = textToCopy;
|
||||||
|
|
||||||
|
// Move textarea out of the viewport so it's not visible
|
||||||
|
textArea.style.position = "absolute";
|
||||||
|
textArea.style.left = "-999999px";
|
||||||
|
|
||||||
|
document.body.prepend(textArea);
|
||||||
|
textArea.select();
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.execCommand("copy");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
textArea.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
"typecheck": "react-router typegen && tsc"
|
"typecheck": "react-router typegen && tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-yaml": "^6.1.2",
|
|
||||||
"@hookform/resolvers": "^5.2.1",
|
"@hookform/resolvers": "^5.2.1",
|
||||||
"@ironmount/schemas": "workspace:*",
|
"@ironmount/schemas": "workspace:*",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
@@ -25,8 +24,6 @@
|
|||||||
"@tanstack/react-query": "^5.84.2",
|
"@tanstack/react-query": "^5.84.2",
|
||||||
"@tanstack/react-query-devtools": "^5.85.9",
|
"@tanstack/react-query-devtools": "^5.85.9",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@uiw/codemirror-theme-copilot": "^4.25.2",
|
|
||||||
"@uiw/react-codemirror": "^4.25.2",
|
|
||||||
"arktype": "^2.1.20",
|
"arktype": "^2.1.20",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -34,6 +31,7 @@
|
|||||||
"isbot": "^5.1.27",
|
"isbot": "^5.1.27",
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"prismjs": "^1.30.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-hook-form": "^7.62.0",
|
"react-hook-form": "^7.62.0",
|
||||||
@@ -46,6 +44,7 @@
|
|||||||
"@react-router/dev": "^7.7.1",
|
"@react-router/dev": "^7.7.1",
|
||||||
"@tailwindcss/vite": "^4.1.4",
|
"@tailwindcss/vite": "^4.1.4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
"@types/prismjs": "^1.26.5",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.2",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import {
|
|||||||
testConnectionDto,
|
testConnectionDto,
|
||||||
updateVolumeBody,
|
updateVolumeBody,
|
||||||
updateVolumeDto,
|
updateVolumeDto,
|
||||||
type VolumeDto,
|
|
||||||
mountVolumeDto,
|
mountVolumeDto,
|
||||||
unmountVolumeDto,
|
unmountVolumeDto,
|
||||||
|
type GetVolumeResponseDto,
|
||||||
} from "./volume.dto";
|
} from "./volume.dto";
|
||||||
import { volumeService } from "./volume.service";
|
import { volumeService } from "./volume.service";
|
||||||
|
|
||||||
@@ -55,11 +55,14 @@ export const volumeController = new Hono()
|
|||||||
const res = await volumeService.getVolume(name);
|
const res = await volumeService.getVolume(name);
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
...res.volume,
|
...res,
|
||||||
createdAt: res.volume.createdAt.getTime(),
|
volume: {
|
||||||
updatedAt: res.volume.updatedAt.getTime(),
|
...res.volume,
|
||||||
lastHealthCheck: res.volume.lastHealthCheck.getTime(),
|
createdAt: res.volume.createdAt.getTime(),
|
||||||
} satisfies VolumeDto;
|
updatedAt: res.volume.updatedAt.getTime(),
|
||||||
|
lastHealthCheck: res.volume.lastHealthCheck.getTime(),
|
||||||
|
},
|
||||||
|
} satisfies GetVolumeResponseDto;
|
||||||
|
|
||||||
return c.json(response, 200);
|
return c.json(response, 200);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -100,6 +100,16 @@ export const deleteVolumeDto = describeRoute({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getVolumeResponse = type({
|
||||||
|
volume: volumeSchema,
|
||||||
|
statfs: type({
|
||||||
|
total: "number",
|
||||||
|
used: "number",
|
||||||
|
free: "number",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type GetVolumeResponseDto = typeof getVolumeResponse.infer;
|
||||||
/**
|
/**
|
||||||
* Get a volume
|
* Get a volume
|
||||||
*/
|
*/
|
||||||
@@ -113,7 +123,7 @@ export const getVolumeDto = describeRoute({
|
|||||||
description: "Volume details",
|
description: "Volume details",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(volumeSchema),
|
schema: resolver(getVolumeResponse),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { db } from "../../db/db";
|
|||||||
import { volumesTable } from "../../db/schema";
|
import { volumesTable } from "../../db/schema";
|
||||||
import { createVolumeBackend } from "../backends/backend";
|
import { createVolumeBackend } from "../backends/backend";
|
||||||
import { toMessage } from "../../utils/errors";
|
import { toMessage } from "../../utils/errors";
|
||||||
|
import { getStatFs } from "../../utils/mountinfo";
|
||||||
|
import { VOLUME_MOUNT_BASE } from "../../core/constants";
|
||||||
|
|
||||||
const listVolumes = async () => {
|
const listVolumes = async () => {
|
||||||
const volumes = await db.query.volumesTable.findMany({});
|
const volumes = await db.query.volumesTable.findMany({});
|
||||||
@@ -99,11 +101,13 @@ const getVolume = async (name: string) => {
|
|||||||
where: eq(volumesTable.name, name),
|
where: eq(volumesTable.name, name),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const statfs = await getStatFs(`${VOLUME_MOUNT_BASE}/${name}/_data`);
|
||||||
|
|
||||||
if (!volume) {
|
if (!volume) {
|
||||||
throw new NotFoundError("Volume not found");
|
throw new NotFoundError("Volume not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return { volume };
|
return { volume, statfs };
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateVolume = async (name: string, backendConfig: BackendConfig) => {
|
const updateVolume = async (name: string, backendConfig: BackendConfig) => {
|
||||||
|
|||||||
@@ -51,3 +51,13 @@ export async function getMountForPath(p: string): Promise<MountInfo | undefined>
|
|||||||
}
|
}
|
||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getStatFs(mountPoint: string) {
|
||||||
|
const stats = await fs.statfs(mountPoint);
|
||||||
|
|
||||||
|
const total = Number(stats.blocks) * Number(stats.bsize);
|
||||||
|
const free = Number(stats.bfree) * Number(stats.bsize);
|
||||||
|
const used = total - free;
|
||||||
|
|
||||||
|
return { total, used, free };
|
||||||
|
}
|
||||||
|
|||||||
55
bun.lock
55
bun.lock
@@ -11,7 +11,6 @@
|
|||||||
"apps/client": {
|
"apps/client": {
|
||||||
"name": "@ironmount/client",
|
"name": "@ironmount/client",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-yaml": "^6.1.2",
|
|
||||||
"@hookform/resolvers": "^5.2.1",
|
"@hookform/resolvers": "^5.2.1",
|
||||||
"@ironmount/schemas": "workspace:*",
|
"@ironmount/schemas": "workspace:*",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
@@ -27,8 +26,6 @@
|
|||||||
"@tanstack/react-query": "^5.84.2",
|
"@tanstack/react-query": "^5.84.2",
|
||||||
"@tanstack/react-query-devtools": "^5.85.9",
|
"@tanstack/react-query-devtools": "^5.85.9",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@uiw/codemirror-theme-copilot": "^4.25.2",
|
|
||||||
"@uiw/react-codemirror": "^4.25.2",
|
|
||||||
"arktype": "^2.1.20",
|
"arktype": "^2.1.20",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -36,6 +33,7 @@
|
|||||||
"isbot": "^5.1.27",
|
"isbot": "^5.1.27",
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"prismjs": "^1.30.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-hook-form": "^7.62.0",
|
"react-hook-form": "^7.62.0",
|
||||||
@@ -48,6 +46,7 @@
|
|||||||
"@react-router/dev": "^7.7.1",
|
"@react-router/dev": "^7.7.1",
|
||||||
"@tailwindcss/vite": "^4.1.4",
|
"@tailwindcss/vite": "^4.1.4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
"@types/prismjs": "^1.26.5",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.2",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
@@ -145,32 +144,12 @@
|
|||||||
|
|
||||||
"@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="],
|
"@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="],
|
||||||
|
|
||||||
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
|
||||||
|
|
||||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
"@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
|
"@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
|
||||||
|
|
||||||
"@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
|
"@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
|
||||||
|
|
||||||
"@codemirror/autocomplete": ["@codemirror/autocomplete@6.18.7", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-8EzdeIoWPJDsMBwz3zdzwXnUpCzMiCyz5/A3FIPpriaclFCGDkAzK13sMcnsu5rowqiyeQN2Vs2TsOcoDPZirQ=="],
|
|
||||||
|
|
||||||
"@codemirror/commands": ["@codemirror/commands@6.8.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw=="],
|
|
||||||
|
|
||||||
"@codemirror/lang-yaml": ["@codemirror/lang-yaml@6.1.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.0.0", "@lezer/yaml": "^1.0.0" } }, "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw=="],
|
|
||||||
|
|
||||||
"@codemirror/language": ["@codemirror/language@6.11.3", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.1.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA=="],
|
|
||||||
|
|
||||||
"@codemirror/lint": ["@codemirror/lint@6.8.5", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA=="],
|
|
||||||
|
|
||||||
"@codemirror/search": ["@codemirror/search@6.5.11", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "crelt": "^1.0.5" } }, "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA=="],
|
|
||||||
|
|
||||||
"@codemirror/state": ["@codemirror/state@6.5.2", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA=="],
|
|
||||||
|
|
||||||
"@codemirror/theme-one-dark": ["@codemirror/theme-one-dark@6.1.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/highlight": "^1.0.0" } }, "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA=="],
|
|
||||||
|
|
||||||
"@codemirror/view": ["@codemirror/view@6.38.3", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-x2t87+oqwB1mduiQZ6huIghjMt4uZKFEdj66IcXw7+a5iBEvv9lh7EWDRHI7crnD4BMGpnyq/RzmCGbiEZLcvQ=="],
|
|
||||||
|
|
||||||
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
|
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
|
||||||
|
|
||||||
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="],
|
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="],
|
||||||
@@ -271,16 +250,6 @@
|
|||||||
|
|
||||||
"@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="],
|
"@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="],
|
||||||
|
|
||||||
"@lezer/common": ["@lezer/common@1.2.3", "", {}, "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA=="],
|
|
||||||
|
|
||||||
"@lezer/highlight": ["@lezer/highlight@1.2.1", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA=="],
|
|
||||||
|
|
||||||
"@lezer/lr": ["@lezer/lr@1.4.2", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA=="],
|
|
||||||
|
|
||||||
"@lezer/yaml": ["@lezer/yaml@1.0.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.4.0" } }, "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA=="],
|
|
||||||
|
|
||||||
"@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="],
|
|
||||||
|
|
||||||
"@mjackson/node-fetch-server": ["@mjackson/node-fetch-server@0.2.0", "", {}, "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng=="],
|
"@mjackson/node-fetch-server": ["@mjackson/node-fetch-server@0.2.0", "", {}, "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng=="],
|
||||||
|
|
||||||
"@npmcli/git": ["@npmcli/git@4.1.0", "", { "dependencies": { "@npmcli/promise-spawn": "^6.0.0", "lru-cache": "^7.4.4", "npm-pick-manifest": "^8.0.0", "proc-log": "^3.0.0", "promise-inflight": "^1.0.1", "promise-retry": "^2.0.1", "semver": "^7.3.5", "which": "^3.0.0" } }, "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ=="],
|
"@npmcli/git": ["@npmcli/git@4.1.0", "", { "dependencies": { "@npmcli/promise-spawn": "^6.0.0", "lru-cache": "^7.4.4", "npm-pick-manifest": "^8.0.0", "proc-log": "^3.0.0", "promise-inflight": "^1.0.1", "promise-retry": "^2.0.1", "semver": "^7.3.5", "which": "^3.0.0" } }, "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ=="],
|
||||||
@@ -467,20 +436,14 @@
|
|||||||
|
|
||||||
"@types/node": ["@types/node@20.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow=="],
|
"@types/node": ["@types/node@20.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow=="],
|
||||||
|
|
||||||
|
"@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.1.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg=="],
|
"@types/react": ["@types/react@19.1.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.1.7", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw=="],
|
"@types/react-dom": ["@types/react-dom@19.1.7", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw=="],
|
||||||
|
|
||||||
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
|
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
|
||||||
|
|
||||||
"@uiw/codemirror-extensions-basic-setup": ["@uiw/codemirror-extensions-basic-setup@4.25.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-s2fbpdXrSMWEc86moll/d007ZFhu6jzwNu5cWv/2o7egymvLeZO52LWkewgbr+BUCGWGPsoJVWeaejbsb/hLcw=="],
|
|
||||||
|
|
||||||
"@uiw/codemirror-theme-copilot": ["@uiw/codemirror-theme-copilot@4.25.2", "", { "dependencies": { "@uiw/codemirror-themes": "4.25.2" } }, "sha512-fsJbXVyeqZm1olA6arpZUI6oRSWvK4hKqzNnsefaVDXs4klSEbq5LG7oa/velqeV9W+/+Zenf9l8/h+sOYn94w=="],
|
|
||||||
|
|
||||||
"@uiw/codemirror-themes": ["@uiw/codemirror-themes@4.25.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-WFYxW3OlCkMomXQBlQdGj1JZ011UNCa7xYdmgYqywVc4E8f5VgIzRwCZSBNVjpWGGDBOjc+Z6F65l7gttP16pg=="],
|
|
||||||
|
|
||||||
"@uiw/react-codemirror": ["@uiw/react-codemirror@4.25.2", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@codemirror/commands": "^6.1.0", "@codemirror/state": "^6.1.1", "@codemirror/theme-one-dark": "^6.0.0", "@uiw/codemirror-extensions-basic-setup": "4.25.2", "codemirror": "^6.0.0" }, "peerDependencies": { "@codemirror/view": ">=6.0.0", "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-XP3R1xyE0CP6Q0iR0xf3ed+cJzJnfmbLelgJR6osVVtMStGGZP3pGQjjwDRYptmjGHfEELUyyBLdY25h0BQg7w=="],
|
|
||||||
|
|
||||||
"@vitejs/plugin-rsc": ["@vitejs/plugin-rsc@0.4.11", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.7.0", "es-module-lexer": "^1.7.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.17", "periscopic": "^4.0.2", "turbo-stream": "^3.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*", "vite": "*" } }, "sha512-+4H4wLi+Y9yF58znBfKgGfX8zcqUGt8ngnmNgzrdGdF1SVz7EO0sg7WnhK5fFVHt6fUxsVEjmEabsCWHKPL1Tw=="],
|
"@vitejs/plugin-rsc": ["@vitejs/plugin-rsc@0.4.11", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.7.0", "es-module-lexer": "^1.7.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.17", "periscopic": "^4.0.2", "turbo-stream": "^3.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*", "vite": "*" } }, "sha512-+4H4wLi+Y9yF58znBfKgGfX8zcqUGt8ngnmNgzrdGdF1SVz7EO0sg7WnhK5fFVHt6fUxsVEjmEabsCWHKPL1Tw=="],
|
||||||
|
|
||||||
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||||
@@ -547,8 +510,6 @@
|
|||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
"codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="],
|
|
||||||
|
|
||||||
"color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="],
|
"color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="],
|
||||||
|
|
||||||
"color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
"color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||||
@@ -581,8 +542,6 @@
|
|||||||
|
|
||||||
"cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="],
|
"cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="],
|
||||||
|
|
||||||
"crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
|
|
||||||
|
|
||||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
@@ -889,6 +848,8 @@
|
|||||||
|
|
||||||
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||||
|
|
||||||
|
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
|
||||||
|
|
||||||
"proc-log": ["proc-log@3.0.0", "", {}, "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A=="],
|
"proc-log": ["proc-log@3.0.0", "", {}, "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A=="],
|
||||||
|
|
||||||
"promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="],
|
"promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="],
|
||||||
@@ -999,8 +960,6 @@
|
|||||||
|
|
||||||
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"style-mod": ["style-mod@4.1.2", "", {}, "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="],
|
|
||||||
|
|
||||||
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
|
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.1.12", "", {}, "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA=="],
|
"tailwindcss": ["tailwindcss@4.1.12", "", {}, "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA=="],
|
||||||
@@ -1079,8 +1038,6 @@
|
|||||||
|
|
||||||
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
|
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
|
||||||
|
|
||||||
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
|
|
||||||
|
|
||||||
"which": ["which@3.0.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg=="],
|
"which": ["which@3.0.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg=="],
|
||||||
|
|
||||||
"winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="],
|
"winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="],
|
||||||
|
|||||||
Reference in New Issue
Block a user