mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: add react tour
This commit is contained in:
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": ["biomejs.biome"]
|
|
||||||
}
|
|
||||||
26
.vscode/settings.json
vendored
26
.vscode/settings.json
vendored
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[json]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[jsonc]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[typescript]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[typescriptreact]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"quickfix.biome": "explicit",
|
|
||||||
"source.organizeImports.biome": "explicit"
|
|
||||||
},
|
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
|
||||||
"editor.formatOnPaste": true,
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"emmet.showExpandedAbbreviation": "never",
|
|
||||||
"prettier.enable": false
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { FileTerminal } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serverId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
command: z.string().min(1, {
|
||||||
|
message: "Command is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
export const EditScript = ({ serverId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { data: server } = api.server.one.useQuery(
|
||||||
|
{
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!serverId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = api.server.update.useMutation();
|
||||||
|
|
||||||
|
const { data: defaultCommand } = api.server.getDefaultCommand.useQuery(
|
||||||
|
{
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!serverId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = useForm<Schema>({
|
||||||
|
defaultValues: {
|
||||||
|
command: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (server) {
|
||||||
|
form.reset({
|
||||||
|
command: server.command || defaultCommand,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [server, defaultCommand]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: Schema) => {
|
||||||
|
if (server) {
|
||||||
|
await mutateAsync({
|
||||||
|
...server,
|
||||||
|
command: formData.command || "",
|
||||||
|
serverId,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
toast.success("Script modified successfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error modifying the script");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">
|
||||||
|
Modify Script
|
||||||
|
<FileTerminal className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl overflow-x-hidden">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Modify Script</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Modify the script which install everything necessary to deploy
|
||||||
|
applications on your server,
|
||||||
|
</DialogDescription>
|
||||||
|
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
We suggest to don't modify the script if you don't know what you are
|
||||||
|
doing
|
||||||
|
</AlertBlock>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
id="hook-form-delete-application"
|
||||||
|
className="grid w-full gap-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="command"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Command</FormLabel>
|
||||||
|
<FormControl className="max-h-[75vh] max-w-[60rem] overflow-y-scroll overflow-x-hidden">
|
||||||
|
<CodeEditor
|
||||||
|
language="shell"
|
||||||
|
{...field}
|
||||||
|
placeholder={`
|
||||||
|
set -e
|
||||||
|
echo "Hello world"
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="flex justify-between w-full">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
form.reset({
|
||||||
|
command: defaultCommand || "",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
form="hook-form-delete-application"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -32,6 +32,7 @@ import Link from "next/link";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ShowDeployment } from "../../application/deployments/show-deployment";
|
import { ShowDeployment } from "../../application/deployments/show-deployment";
|
||||||
|
import { EditScript } from "./edit-script";
|
||||||
import { GPUSupport } from "./gpu-support";
|
import { GPUSupport } from "./gpu-support";
|
||||||
import { ValidateServer } from "./validate-server";
|
import { ValidateServer } from "./validate-server";
|
||||||
|
|
||||||
@@ -89,7 +90,12 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div id="hook-form-add-gitlab" className="grid w-full gap-1">
|
<div id="hook-form-add-gitlab" className="grid w-full gap-4">
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
Using a root user is required to ensure everything works as
|
||||||
|
expected.
|
||||||
|
</AlertBlock>
|
||||||
|
|
||||||
<Tabs defaultValue="ssh-keys">
|
<Tabs defaultValue="ssh-keys">
|
||||||
<TabsList className="grid grid-cols-4 w-[600px]">
|
<TabsList className="grid grid-cols-4 w-[600px]">
|
||||||
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
|
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
|
||||||
@@ -198,6 +204,28 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col gap-2 w-full border rounded-lg p-4">
|
||||||
|
<span className="text-base font-semibold text-primary">
|
||||||
|
Supported Distros:
|
||||||
|
</span>
|
||||||
|
<p>
|
||||||
|
We strongly recommend to use the following distros to
|
||||||
|
ensure the best experience:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>1. Ubuntu 24.04 LTS</li>
|
||||||
|
<li>2. Ubuntu 23.10 LTS </li>
|
||||||
|
<li>3. Ubuntu 22.04 LTS</li>
|
||||||
|
<li>4. Ubuntu 20.04 LTS</li>
|
||||||
|
<li>5. Ubuntu 18.04 LTS</li>
|
||||||
|
<li>6. Debian 12</li>
|
||||||
|
<li>7. Debian 11</li>
|
||||||
|
<li>8. Debian 10</li>
|
||||||
|
<li>9. Fedora 40</li>
|
||||||
|
<li>10. Centos 9</li>
|
||||||
|
<li>11. Centos 8</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="deployments">
|
<TabsContent value="deployments">
|
||||||
@@ -214,24 +242,29 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
See all the 5 Server Setup
|
See all the 5 Server Setup
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<DialogAction
|
<div className="flex flex-row gap-2">
|
||||||
title={"Setup Server?"}
|
<EditScript serverId={server?.serverId || ""} />
|
||||||
description="This will setup the server and all associated data"
|
<DialogAction
|
||||||
onClick={async () => {
|
title={"Setup Server?"}
|
||||||
await mutateAsync({
|
description="This will setup the server and all associated data"
|
||||||
serverId: server?.serverId || "",
|
onClick={async () => {
|
||||||
})
|
await mutateAsync({
|
||||||
.then(async () => {
|
serverId: server?.serverId || "",
|
||||||
refetch();
|
|
||||||
toast.success("Server setup successfully");
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(async () => {
|
||||||
toast.error("Error configuring server");
|
refetch();
|
||||||
});
|
toast.success("Server setup successfully");
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button isLoading={isLoading}>Setup Server</Button>
|
toast.error("Error configuring server");
|
||||||
</DialogAction>
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button isLoading={isLoading}>
|
||||||
|
Setup Server
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { cn } from "@/lib/utils";
|
|||||||
import { json } from "@codemirror/lang-json";
|
import { json } from "@codemirror/lang-json";
|
||||||
import { yaml } from "@codemirror/lang-yaml";
|
import { yaml } from "@codemirror/lang-yaml";
|
||||||
import { StreamLanguage } from "@codemirror/language";
|
import { StreamLanguage } from "@codemirror/language";
|
||||||
|
|
||||||
import { properties } from "@codemirror/legacy-modes/mode/properties";
|
import { properties } from "@codemirror/legacy-modes/mode/properties";
|
||||||
|
import { shell } from "@codemirror/legacy-modes/mode/shell";
|
||||||
import { EditorView } from "@codemirror/view";
|
import { EditorView } from "@codemirror/view";
|
||||||
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
|
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
|
||||||
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
||||||
@@ -10,7 +12,7 @@ import { useTheme } from "next-themes";
|
|||||||
interface Props extends ReactCodeMirrorProps {
|
interface Props extends ReactCodeMirrorProps {
|
||||||
wrapperClassName?: string;
|
wrapperClassName?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
language?: "yaml" | "json" | "properties";
|
language?: "yaml" | "json" | "properties" | "shell";
|
||||||
lineWrapping?: boolean;
|
lineWrapping?: boolean;
|
||||||
lineNumbers?: boolean;
|
lineNumbers?: boolean;
|
||||||
}
|
}
|
||||||
@@ -39,7 +41,9 @@ export const CodeEditor = ({
|
|||||||
? yaml()
|
? yaml()
|
||||||
: language === "json"
|
: language === "json"
|
||||||
? json()
|
? json()
|
||||||
: StreamLanguage.define(properties),
|
: language === "shell"
|
||||||
|
? StreamLanguage.define(shell)
|
||||||
|
: StreamLanguage.define(properties),
|
||||||
props.lineWrapping ? EditorView.lineWrapping : [],
|
props.lineWrapping ? EditorView.lineWrapping : [],
|
||||||
]}
|
]}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0051_hard_gorgon.sql
Normal file
1
apps/dokploy/drizzle/0051_hard_gorgon.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "server" ADD COLUMN "command" text DEFAULT '' NOT NULL;
|
||||||
4240
apps/dokploy/drizzle/meta/0051_snapshot.json
Normal file
4240
apps/dokploy/drizzle/meta/0051_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -358,6 +358,13 @@
|
|||||||
"when": 1733889104203,
|
"when": 1733889104203,
|
||||||
"tag": "0050_nappy_wrecker",
|
"tag": "0050_nappy_wrecker",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 51,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1734241482851,
|
||||||
|
"tag": "0051_hard_gorgon",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
"test": "vitest --config __test__/vitest.config.ts"
|
"test": "vitest --config __test__/vitest.config.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@reactour/tour": "3.7.0",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-yaml": "^6.1.1",
|
"@codemirror/lang-yaml": "^6.1.1",
|
||||||
"@codemirror/language": "^6.10.1",
|
"@codemirror/language": "^6.10.1",
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import "@/styles/globals.css";
|
import "@/styles/globals.css";
|
||||||
|
|
||||||
|
import { SearchCommand } from "@/components/dashboard/search-command";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { Languages } from "@/lib/languages";
|
import { Languages } from "@/lib/languages";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { TourProvider } from "@reactour/tour";
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
import { appWithTranslation } from "next-i18next";
|
import { appWithTranslation } from "next-i18next";
|
||||||
import { ThemeProvider } from "next-themes";
|
import { ThemeProvider } from "next-themes";
|
||||||
@@ -11,74 +13,83 @@ import { Inter } from "next/font/google";
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import type { ReactElement, ReactNode } from "react";
|
import type { ReactElement, ReactNode } from "react";
|
||||||
import { SearchCommand } from "@/components/dashboard/search-command";
|
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
|
export const steps = [
|
||||||
|
{
|
||||||
|
selector: ".first-step",
|
||||||
|
content: "This is the first Step",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: ".second-step",
|
||||||
|
content: "This is the second Step",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: ".third-step",
|
||||||
|
content: "This is the third Step",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
||||||
getLayout?: (page: ReactElement) => ReactNode;
|
getLayout?: (page: ReactElement) => ReactNode;
|
||||||
// session: Session | null;
|
// session: Session | null;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AppPropsWithLayout = AppProps & {
|
type AppPropsWithLayout = AppProps & {
|
||||||
Component: NextPageWithLayout;
|
Component: NextPageWithLayout;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MyApp = ({
|
const MyApp = ({
|
||||||
Component,
|
Component,
|
||||||
pageProps: { ...pageProps },
|
pageProps: { ...pageProps },
|
||||||
}: AppPropsWithLayout) => {
|
}: AppPropsWithLayout) => {
|
||||||
const getLayout = Component.getLayout ?? ((page) => page);
|
const getLayout = Component.getLayout ?? ((page) => page);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<style jsx global>{`
|
<style jsx global>{`
|
||||||
:root {
|
:root {
|
||||||
--font-inter: ${inter.style.fontFamily};
|
--font-inter: ${inter.style.fontFamily};
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Dokploy</title>
|
<title>Dokploy</title>
|
||||||
</Head>
|
</Head>
|
||||||
{process.env.NEXT_PUBLIC_UMAMI_HOST &&
|
{process.env.NEXT_PUBLIC_UMAMI_HOST &&
|
||||||
process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID && (
|
process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID && (
|
||||||
<Script
|
<Script
|
||||||
src={process.env.NEXT_PUBLIC_UMAMI_HOST}
|
src={process.env.NEXT_PUBLIC_UMAMI_HOST}
|
||||||
data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}
|
data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ThemeProvider
|
<TourProvider steps={steps}>
|
||||||
attribute="class"
|
<ThemeProvider
|
||||||
defaultTheme="system"
|
attribute="class"
|
||||||
enableSystem
|
defaultTheme="system"
|
||||||
disableTransitionOnChange
|
enableSystem
|
||||||
forcedTheme={Component.theme}
|
disableTransitionOnChange
|
||||||
>
|
forcedTheme={Component.theme}
|
||||||
<Toaster richColors />
|
>
|
||||||
<SearchCommand />
|
<Toaster richColors />
|
||||||
{getLayout(<Component {...pageProps} />)}
|
<SearchCommand />
|
||||||
</ThemeProvider>
|
{getLayout(<Component {...pageProps} />)}
|
||||||
</>
|
</ThemeProvider>
|
||||||
);
|
</TourProvider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default api.withTRPC(
|
export default api.withTRPC(
|
||||||
appWithTranslation(
|
appWithTranslation(MyApp, {
|
||||||
MyApp,
|
i18n: {
|
||||||
// keep this in sync with next-i18next.config.js
|
defaultLocale: "en",
|
||||||
// if you want to know why don't just import the config file, this because next-i18next.config.js must be a CJS, but the rest of the code is ESM.
|
locales: Object.values(Languages),
|
||||||
// Add the config here is due to the issue: https://github.com/i18next/next-i18next/issues/2259
|
localeDetection: false,
|
||||||
// if one day every page is translated, we can safely remove this config.
|
},
|
||||||
{
|
fallbackLng: "en",
|
||||||
i18n: {
|
keySeparator: false,
|
||||||
defaultLocale: "en",
|
}),
|
||||||
locales: Object.values(Languages),
|
|
||||||
localeDetection: false,
|
|
||||||
},
|
|
||||||
fallbackLng: "en",
|
|
||||||
keySeparator: false,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
createServer,
|
createServer,
|
||||||
|
defaultCommand,
|
||||||
deleteServer,
|
deleteServer,
|
||||||
findAdminById,
|
findAdminById,
|
||||||
findServerById,
|
findServerById,
|
||||||
@@ -69,6 +70,11 @@ export const serverRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return server;
|
return server;
|
||||||
}),
|
}),
|
||||||
|
getDefaultCommand: protectedProcedure
|
||||||
|
.input(apiFindOneServer)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
return defaultCommand();
|
||||||
|
}),
|
||||||
all: protectedProcedure.query(async ({ ctx }) => {
|
all: protectedProcedure.query(async ({ ctx }) => {
|
||||||
const result = await db
|
const result = await db
|
||||||
.select({
|
.select({
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
|||||||
port: server.port,
|
port: server.port,
|
||||||
username: server.username,
|
username: server.username,
|
||||||
privateKey: server.sshKey?.privateKey,
|
privateKey: server.sshKey?.privateKey,
|
||||||
timeout: 99999,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const shell = getShell();
|
const shell = getShell();
|
||||||
|
|||||||
@@ -124,7 +124,6 @@ export const setupTerminalWebSocketServer = (
|
|||||||
port: server.port,
|
port: server.port,
|
||||||
username: server.username,
|
username: server.username,
|
||||||
privateKey: server.sshKey?.privateKey,
|
privateKey: server.sshKey?.privateKey,
|
||||||
timeout: 99999,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const server = pgTable("server", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => admins.adminId, { onDelete: "cascade" }),
|
.references(() => admins.adminId, { onDelete: "cascade" }),
|
||||||
serverStatus: serverStatus("serverStatus").notNull().default("active"),
|
serverStatus: serverStatus("serverStatus").notNull().default("active"),
|
||||||
|
command: text("command").notNull().default(""),
|
||||||
sshKeyId: text("sshKeyId").references(() => sshKeys.sshKeyId, {
|
sshKeyId: text("sshKeyId").references(() => sshKeys.sshKeyId, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
@@ -105,4 +105,7 @@ export const apiUpdateServer = createSchema
|
|||||||
username: true,
|
username: true,
|
||||||
sshKeyId: true,
|
sshKeyId: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required()
|
||||||
|
.extend({
|
||||||
|
command: z.string().optional(),
|
||||||
|
});
|
||||||
|
|||||||
@@ -60,6 +60,116 @@ export const serverSetup = async (serverId: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const defaultCommand = () => {
|
||||||
|
const bashCommand = `
|
||||||
|
set -e;
|
||||||
|
DOCKER_VERSION=27.0.3
|
||||||
|
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||||
|
SYS_ARCH=$(uname -m)
|
||||||
|
CURRENT_USER=$USER
|
||||||
|
|
||||||
|
echo "Installing requirements for: OS: $OS_TYPE"
|
||||||
|
if [ $EUID != 0 ]; then
|
||||||
|
echo "Please run this script as root or with sudo ❌"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the OS is manjaro, if so, change it to arch
|
||||||
|
if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then
|
||||||
|
OS_TYPE="arch"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the OS is Asahi Linux, if so, change it to fedora
|
||||||
|
if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then
|
||||||
|
OS_TYPE="fedora"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the OS is popOS, if so, change it to ubuntu
|
||||||
|
if [ "$OS_TYPE" = "pop" ]; then
|
||||||
|
OS_TYPE="ubuntu"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the OS is linuxmint, if so, change it to ubuntu
|
||||||
|
if [ "$OS_TYPE" = "linuxmint" ]; then
|
||||||
|
OS_TYPE="ubuntu"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#Check if the OS is zorin, if so, change it to ubuntu
|
||||||
|
if [ "$OS_TYPE" = "zorin" ]; then
|
||||||
|
OS_TYPE="ubuntu"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$OS_TYPE" = "arch" ] || [ "$OS_TYPE" = "archarm" ]; then
|
||||||
|
OS_VERSION="rolling"
|
||||||
|
else
|
||||||
|
OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$OS_TYPE" = 'amzn' ]; then
|
||||||
|
dnf install -y findutils >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$OS_TYPE" in
|
||||||
|
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;;
|
||||||
|
*)
|
||||||
|
echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now."
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo -e "---------------------------------------------"
|
||||||
|
echo "| CPU Architecture | $SYS_ARCH"
|
||||||
|
echo "| Operating System | $OS_TYPE $OS_VERSION"
|
||||||
|
echo "| Docker | $DOCKER_VERSION"
|
||||||
|
echo -e "---------------------------------------------\n"
|
||||||
|
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). "
|
||||||
|
|
||||||
|
command_exists() {
|
||||||
|
command -v "$@" > /dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
${installUtilities()}
|
||||||
|
|
||||||
|
echo -e "2. Validating ports. "
|
||||||
|
${validatePorts()}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
echo -e "3. Installing RClone. "
|
||||||
|
${installRClone()}
|
||||||
|
|
||||||
|
echo -e "4. Installing Docker. "
|
||||||
|
${installDocker()}
|
||||||
|
|
||||||
|
echo -e "5. Setting up Docker Swarm"
|
||||||
|
${setupSwarm()}
|
||||||
|
|
||||||
|
echo -e "6. Setting up Network"
|
||||||
|
${setupNetwork()}
|
||||||
|
|
||||||
|
echo -e "7. Setting up Directories"
|
||||||
|
${setupMainDirectory()}
|
||||||
|
${setupDirectories()}
|
||||||
|
|
||||||
|
echo -e "8. Setting up Traefik"
|
||||||
|
${createTraefikConfig()}
|
||||||
|
|
||||||
|
echo -e "9. Setting up Middlewares"
|
||||||
|
${createDefaultMiddlewares()}
|
||||||
|
|
||||||
|
echo -e "10. Setting up Traefik Instance"
|
||||||
|
${createTraefikInstance()}
|
||||||
|
|
||||||
|
echo -e "11. Installing Nixpacks"
|
||||||
|
${installNixpacks()}
|
||||||
|
|
||||||
|
echo -e "12. Installing Buildpacks"
|
||||||
|
${installBuildpacks()}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return bashCommand;
|
||||||
|
};
|
||||||
|
|
||||||
const installRequirements = async (serverId: string, logPath: string) => {
|
const installRequirements = async (serverId: string, logPath: string) => {
|
||||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||||
const client = new Client();
|
const client = new Client();
|
||||||
@@ -73,110 +183,8 @@ const installRequirements = async (serverId: string, logPath: string) => {
|
|||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
client
|
client
|
||||||
.once("ready", () => {
|
.once("ready", () => {
|
||||||
const bashCommand = `
|
const command = server.command || defaultCommand();
|
||||||
set -e;
|
client.exec(command, (err, stream) => {
|
||||||
# Thanks to coolify <3
|
|
||||||
|
|
||||||
DOCKER_VERSION=27.0.3
|
|
||||||
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
|
||||||
SYS_ARCH=$(uname -m)
|
|
||||||
CURRENT_USER=$USER
|
|
||||||
|
|
||||||
echo "Installing requirements for: OS: $OS_TYPE"
|
|
||||||
if [ $EUID != 0 ]; then
|
|
||||||
echo "Please run this script as root or with sudo ❌"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if the OS is manjaro, if so, change it to arch
|
|
||||||
if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then
|
|
||||||
OS_TYPE="arch"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if the OS is Asahi Linux, if so, change it to fedora
|
|
||||||
if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then
|
|
||||||
OS_TYPE="fedora"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if the OS is popOS, if so, change it to ubuntu
|
|
||||||
if [ "$OS_TYPE" = "pop" ]; then
|
|
||||||
OS_TYPE="ubuntu"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if the OS is linuxmint, if so, change it to ubuntu
|
|
||||||
if [ "$OS_TYPE" = "linuxmint" ]; then
|
|
||||||
OS_TYPE="ubuntu"
|
|
||||||
fi
|
|
||||||
|
|
||||||
#Check if the OS is zorin, if so, change it to ubuntu
|
|
||||||
if [ "$OS_TYPE" = "zorin" ]; then
|
|
||||||
OS_TYPE="ubuntu"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$OS_TYPE" = "arch" ] || [ "$OS_TYPE" = "archarm" ]; then
|
|
||||||
OS_VERSION="rolling"
|
|
||||||
else
|
|
||||||
OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$OS_TYPE" in
|
|
||||||
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;;
|
|
||||||
*)
|
|
||||||
echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now."
|
|
||||||
exit
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo -e "---------------------------------------------"
|
|
||||||
echo "| CPU Architecture | $SYS_ARCH"
|
|
||||||
echo "| Operating System | $OS_TYPE $OS_VERSION"
|
|
||||||
echo "| Docker | $DOCKER_VERSION"
|
|
||||||
echo -e "---------------------------------------------\n"
|
|
||||||
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). "
|
|
||||||
|
|
||||||
command_exists() {
|
|
||||||
command -v "$@" > /dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
${installUtilities()}
|
|
||||||
|
|
||||||
echo -e "2. Validating ports. "
|
|
||||||
${validatePorts()}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
echo -e "3. Installing RClone. "
|
|
||||||
${installRClone()}
|
|
||||||
|
|
||||||
echo -e "4. Installing Docker. "
|
|
||||||
${installDocker()}
|
|
||||||
|
|
||||||
echo -e "5. Setting up Docker Swarm"
|
|
||||||
${setupSwarm()}
|
|
||||||
|
|
||||||
echo -e "6. Setting up Network"
|
|
||||||
${setupNetwork()}
|
|
||||||
|
|
||||||
echo -e "7. Setting up Directories"
|
|
||||||
${setupMainDirectory()}
|
|
||||||
${setupDirectories()}
|
|
||||||
|
|
||||||
echo -e "8. Setting up Traefik"
|
|
||||||
${createTraefikConfig()}
|
|
||||||
|
|
||||||
echo -e "9. Setting up Middlewares"
|
|
||||||
${createDefaultMiddlewares()}
|
|
||||||
|
|
||||||
echo -e "10. Setting up Traefik Instance"
|
|
||||||
${createTraefikInstance()}
|
|
||||||
|
|
||||||
echo -e "11. Installing Nixpacks"
|
|
||||||
${installNixpacks()}
|
|
||||||
|
|
||||||
echo -e "12. Installing Buildpacks"
|
|
||||||
${installBuildpacks()}
|
|
||||||
`;
|
|
||||||
client.exec(bashCommand, (err, stream) => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
writeStream.write(err);
|
writeStream.write(err);
|
||||||
reject(err);
|
reject(err);
|
||||||
@@ -218,7 +226,6 @@ const installRequirements = async (serverId: string, logPath: string) => {
|
|||||||
port: server.port,
|
port: server.port,
|
||||||
username: server.username,
|
username: server.username,
|
||||||
privateKey: server.sshKey?.privateKey,
|
privateKey: server.sshKey?.privateKey,
|
||||||
timeout: 99999,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -148,7 +148,6 @@ export const serverValidate = async (serverId: string) => {
|
|||||||
port: server.port,
|
port: server.port,
|
||||||
username: server.username,
|
username: server.username,
|
||||||
privateKey: server.sshKey?.privateKey,
|
privateKey: server.sshKey?.privateKey,
|
||||||
timeout: 99999,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
60
pnpm-lock.yaml
generated
60
pnpm-lock.yaml
generated
@@ -178,6 +178,9 @@ importers:
|
|||||||
'@radix-ui/react-tooltip':
|
'@radix-ui/react-tooltip':
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
|
'@reactour/tour':
|
||||||
|
specifier: 3.7.0
|
||||||
|
version: 3.7.0(react@18.2.0)
|
||||||
'@stripe/stripe-js':
|
'@stripe/stripe-js':
|
||||||
specifier: 4.8.0
|
specifier: 4.8.0
|
||||||
version: 4.8.0
|
version: 4.8.0
|
||||||
@@ -2871,6 +2874,26 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
|
|
||||||
|
'@reactour/mask@1.1.0':
|
||||||
|
resolution: {integrity: sha512-GkJMLuTs3vTsm4Ryq2uXcE4sMzRP1p4xSd6juSOMqbHa7IVD/UiLCLqJWHR9xGSQPbYhpZAZAORUG5cS0U5tBA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: 16.x || 17.x || 18.x
|
||||||
|
|
||||||
|
'@reactour/popover@1.2.0':
|
||||||
|
resolution: {integrity: sha512-1JMykZ+MmDmRlEVC5+DwlvK1exwV5bFHtiSFwVXnoPZmsSfwId6SLyjo9H6bybeuNtNEzviKdsF/ZBC1UQbDqg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: 16.x || 17.x || 18.x
|
||||||
|
|
||||||
|
'@reactour/tour@3.7.0':
|
||||||
|
resolution: {integrity: sha512-p0USaOBc5fcNBS5ZiQ2lsmztAhIGCUfx913Zw14FbEM8bhSXpR1F2JD0alVj9Ya1N+pnTNYatf14rSNGJsEnCg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: 16.x || 17.x || 18.x
|
||||||
|
|
||||||
|
'@reactour/utils@0.5.0':
|
||||||
|
resolution: {integrity: sha512-yQs5Nm/Dg1xRM7d/S/UILBV5OInrTgrjGzgc81/RP5khqdO5KnpOaC46yF83kDtCalte8X3RCwp+F2YA509k1w==}
|
||||||
|
peerDependencies:
|
||||||
|
react: 16.x || 17.x || 18.x
|
||||||
|
|
||||||
'@redis/bloom@1.2.0':
|
'@redis/bloom@1.2.0':
|
||||||
resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==}
|
resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2980,6 +3003,11 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@rooks/use-mutation-observer@4.11.2':
|
||||||
|
resolution: {integrity: sha512-vpsdrZdr6TkB1zZJcHx+fR1YC/pHs2BaqcuYiEGjBVbwY5xcC49+h0hAUtQKHth3oJqXfIX/Ng8S7s5HFHdM/A==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0'
|
||||||
|
|
||||||
'@selderee/plugin-htmlparser2@0.11.0':
|
'@selderee/plugin-htmlparser2@0.11.0':
|
||||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||||
|
|
||||||
@@ -6012,6 +6040,9 @@ packages:
|
|||||||
reselect@5.1.1:
|
reselect@5.1.1:
|
||||||
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
|
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
|
||||||
|
|
||||||
|
resize-observer-polyfill@1.5.1:
|
||||||
|
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
||||||
|
|
||||||
resolve-alpn@1.2.1:
|
resolve-alpn@1.2.1:
|
||||||
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
|
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
|
||||||
|
|
||||||
@@ -8828,6 +8859,29 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
|
|
||||||
|
'@reactour/mask@1.1.0(react@18.2.0)':
|
||||||
|
dependencies:
|
||||||
|
'@reactour/utils': 0.5.0(react@18.2.0)
|
||||||
|
react: 18.2.0
|
||||||
|
|
||||||
|
'@reactour/popover@1.2.0(react@18.2.0)':
|
||||||
|
dependencies:
|
||||||
|
'@reactour/utils': 0.5.0(react@18.2.0)
|
||||||
|
react: 18.2.0
|
||||||
|
|
||||||
|
'@reactour/tour@3.7.0(react@18.2.0)':
|
||||||
|
dependencies:
|
||||||
|
'@reactour/mask': 1.1.0(react@18.2.0)
|
||||||
|
'@reactour/popover': 1.2.0(react@18.2.0)
|
||||||
|
'@reactour/utils': 0.5.0(react@18.2.0)
|
||||||
|
react: 18.2.0
|
||||||
|
|
||||||
|
'@reactour/utils@0.5.0(react@18.2.0)':
|
||||||
|
dependencies:
|
||||||
|
'@rooks/use-mutation-observer': 4.11.2(react@18.2.0)
|
||||||
|
react: 18.2.0
|
||||||
|
resize-observer-polyfill: 1.5.1
|
||||||
|
|
||||||
'@redis/bloom@1.2.0(@redis/client@1.6.0)':
|
'@redis/bloom@1.2.0(@redis/client@1.6.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@redis/client': 1.6.0
|
'@redis/client': 1.6.0
|
||||||
@@ -8902,6 +8956,10 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc@4.19.1':
|
'@rollup/rollup-win32-x64-msvc@4.19.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@rooks/use-mutation-observer@4.11.2(react@18.2.0)':
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
|
||||||
'@selderee/plugin-htmlparser2@0.11.0':
|
'@selderee/plugin-htmlparser2@0.11.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
domhandler: 5.0.3
|
domhandler: 5.0.3
|
||||||
@@ -12238,6 +12296,8 @@ snapshots:
|
|||||||
|
|
||||||
reselect@5.1.1: {}
|
reselect@5.1.1: {}
|
||||||
|
|
||||||
|
resize-observer-polyfill@1.5.1: {}
|
||||||
|
|
||||||
resolve-alpn@1.2.1: {}
|
resolve-alpn@1.2.1: {}
|
||||||
|
|
||||||
resolve-from@4.0.0: {}
|
resolve-from@4.0.0: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user