forked from github-starred/komodo
updates
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { useRead } from "@hooks";
|
||||
import { BuilderConfig, Resource } from "@monitor/client/dist/types";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { Resource } from "@monitor/client/dist/types";
|
||||
import { Fragment, ReactNode } from "react";
|
||||
|
||||
const keys = <T extends Record<string, unknown>>(obj: T) =>
|
||||
Object.keys(obj) as Array<keyof T>;
|
||||
@@ -12,15 +11,17 @@ export const ConfigAgain = <T extends Resource<unknown, unknown>["config"]>({
|
||||
}: {
|
||||
config: T;
|
||||
update: Partial<T>;
|
||||
components: {
|
||||
components: Partial<{
|
||||
[K in keyof T]: (value: T[K]) => ReactNode;
|
||||
};
|
||||
}>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{keys(components).map((key) => {
|
||||
const value = update[key] ?? config[key];
|
||||
return <>{components[key]?.(value)}</>;
|
||||
return (
|
||||
<Fragment key={key.toString()}>{components[key]?.(value)}</Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
@@ -39,27 +40,3 @@ export const VariantConfig = <P, T extends { type: string; params: P }>({
|
||||
}) => {
|
||||
return <>{config}</>;
|
||||
};
|
||||
|
||||
export const Builder = ({ id }: { id: string }) => {
|
||||
const builder = useRead("GetBuilder", { id }).data;
|
||||
if (!builder?.config) return null;
|
||||
const [update, set] = useState<{
|
||||
type: BuilderConfig["type"];
|
||||
params: Partial<BuilderConfig["params"]>;
|
||||
}>({ type: builder.config.type, params: {} });
|
||||
|
||||
return (
|
||||
<VariantConfig
|
||||
config={builder.config}
|
||||
update={update}
|
||||
components={{
|
||||
Server: {
|
||||
id: (id) => <div>{id}</div>,
|
||||
},
|
||||
Aws: {
|
||||
ami_id:
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
38
frontend/src/components/config/util.tsx
Normal file
38
frontend/src/components/config/util.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Input } from "@ui/input";
|
||||
import { Switch } from "@ui/switch";
|
||||
|
||||
export const ConfigInput = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
label: string;
|
||||
value: string | number | undefined;
|
||||
onChange: (value: string) => void;
|
||||
}) => (
|
||||
<div className="flex justify-between items-center border-b pb-4">
|
||||
<div className="capitalize"> {label} </div>
|
||||
<Input
|
||||
className="max-w-[400px]"
|
||||
type={typeof value === "number" ? "number" : undefined}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
// disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const ConfigSwitch = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
label: boolean;
|
||||
value: boolean | undefined;
|
||||
onChange: (value: boolean) => void;
|
||||
}) => (
|
||||
<div className="flex justify-between items-center border-b pb-4">
|
||||
<div className="capitalize"> {label} </div>
|
||||
<Switch checked={value} onCheckedChange={onChange} />
|
||||
</div>
|
||||
);
|
||||
@@ -1,3 +1,6 @@
|
||||
import { ConfirmUpdate } from "@components/config/confirm-update";
|
||||
import { Button } from "@ui/button";
|
||||
import { Settings, Save, History } from "lucide-react";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface PageProps {
|
||||
@@ -39,3 +42,33 @@ export const Section = ({ title, icon, actions, children }: SectionProps) => (
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const ConfigLayout = ({
|
||||
content,
|
||||
children,
|
||||
onConfirm,
|
||||
onReset,
|
||||
}: {
|
||||
content: any;
|
||||
children: ReactNode;
|
||||
onConfirm: () => void;
|
||||
onReset: () => void;
|
||||
}) => (
|
||||
<Section
|
||||
title="Config"
|
||||
icon={<Settings className="w-4 h-4" />}
|
||||
actions={
|
||||
<div className="flex gap-4">
|
||||
<Button variant="outline" intent="warning" onClick={onReset}>
|
||||
<History className="w-4 h-4" />
|
||||
</Button>
|
||||
<ConfirmUpdate
|
||||
content={JSON.stringify(content, null, 2)}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Section>
|
||||
);
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { ResourceCard } from "@layouts/card";
|
||||
import { Bot, Cloud, Factory, History, Settings } from "lucide-react";
|
||||
import { Bot, Cloud, Factory } from "lucide-react";
|
||||
import { ResourceUpdates } from "@components/updates/resource";
|
||||
import { useAddRecentlyViewed, useRead, useWrite } from "@hooks";
|
||||
import { Resource } from "@layouts/resource";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { Types } from "@monitor/client";
|
||||
import { useState } from "react";
|
||||
import { Section } from "@layouts/page";
|
||||
import { Button } from "@ui/button";
|
||||
import { ConfirmUpdate } from "@components/config/confirm-update";
|
||||
import { ConfigAgain, VariantConfig } from "@components/config/again";
|
||||
import { ConfigLayout } from "@layouts/page";
|
||||
import { ConfigAgain } from "@components/config/again";
|
||||
import { Input } from "@ui/input";
|
||||
import {
|
||||
Select,
|
||||
@@ -18,6 +16,10 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@ui/select";
|
||||
import { ServersSelector } from "@resources/deployment/config";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@ui/card";
|
||||
import { Button } from "@ui/button";
|
||||
import { ConfigInput } from "@components/config/util";
|
||||
|
||||
export const BuilderName = ({ id }: { id: string }) => {
|
||||
const builders = useRead("ListBuilders", {}).data;
|
||||
@@ -64,142 +66,160 @@ const BuilderTypeSelector = ({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={"Aws"}>Aws</SelectItem>
|
||||
{/* <SelectItem value={"Server"}>Server</SelectItem> */}
|
||||
<SelectItem value={"Server"}>Server</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
|
||||
const ConfigInput = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
const ServerConfig = ({
|
||||
config,
|
||||
update,
|
||||
set,
|
||||
}: {
|
||||
label: string;
|
||||
value: string | number;
|
||||
onChange: (value: string) => void;
|
||||
config: Types.ServerBuilderConfig;
|
||||
update: Partial<Types.ServerBuilderConfig>;
|
||||
set: (update: Partial<Types.ServerBuilderConfig>) => void;
|
||||
}) => (
|
||||
<div className="flex justify-between items-center border-b pb-4">
|
||||
<div className="capitalize"> {label} </div>
|
||||
<Input
|
||||
className="max-w-[400px]"
|
||||
type={typeof value === "number" ? "number" : undefined}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
// disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<ConfigAgain
|
||||
config={config}
|
||||
update={update}
|
||||
components={{
|
||||
id: (id) => (
|
||||
<div className="flex justify-between items-center border-b pb-4">
|
||||
Select Server
|
||||
<ServersSelector selected={id} onSelect={(id) => set({ id })} />
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const BuilderConfig = ({ id }: { id: string }) => {
|
||||
const builder = useRead("GetBuilder", { id }).data;
|
||||
if (!builder?.config) return null;
|
||||
const AwsBuilderConfig = ({
|
||||
config,
|
||||
update,
|
||||
set,
|
||||
}: {
|
||||
config: Types.AwsBuilderConfig;
|
||||
update: Partial<Types.AwsBuilderConfig>;
|
||||
set: (update: Partial<Types.AwsBuilderConfig>) => void;
|
||||
}) => (
|
||||
<ConfigAgain
|
||||
config={config}
|
||||
update={update}
|
||||
components={{
|
||||
region: (region) => (
|
||||
<ConfigInput
|
||||
label="Region"
|
||||
value={region}
|
||||
onChange={(region) => set({ region })}
|
||||
/>
|
||||
),
|
||||
instance_type: (instance_type) => (
|
||||
<ConfigInput
|
||||
label="Instance Type"
|
||||
value={instance_type}
|
||||
onChange={(instance_type) => set({ instance_type })}
|
||||
/>
|
||||
),
|
||||
|
||||
const [type, setT] = useState(builder.config.type);
|
||||
|
||||
const [update, set] = useState<{
|
||||
type: Types.BuilderConfig["type"];
|
||||
params: Partial<Types.BuilderConfig["params"]>;
|
||||
}>({ type: builder.config.type, params: {} });
|
||||
volume_gb: (volume_gb) => (
|
||||
<ConfigInput
|
||||
label="Region"
|
||||
value={volume_gb}
|
||||
onChange={(ami_id) => set({ ami_id })}
|
||||
/>
|
||||
),
|
||||
ami_id: (ami_id) => (
|
||||
<ConfigInput
|
||||
label="AMI Id"
|
||||
value={ami_id}
|
||||
onChange={(ami_id) => set({ ami_id })}
|
||||
/>
|
||||
),
|
||||
subnet_id: (subnet_id) => (
|
||||
<ConfigInput
|
||||
label="Subnet Id"
|
||||
value={subnet_id}
|
||||
onChange={(subnet_id) => set({ subnet_id })}
|
||||
/>
|
||||
),
|
||||
key_pair_name: (key_pair_name) => (
|
||||
<ConfigInput
|
||||
label="Subnet Id"
|
||||
value={key_pair_name}
|
||||
onChange={(n) => set({ key_pair_name })}
|
||||
/>
|
||||
),
|
||||
assign_public_ip: () => <div>assign_public_ip</div>,
|
||||
// security_group_ids: (ids) => <div>sec group ids</div>,
|
||||
// github_accounts: () => <div>github_accounts</div>,
|
||||
// docker_accounts: () => <div>docker_accounts</div>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const BuilderConfig = ({
|
||||
id,
|
||||
config,
|
||||
}: {
|
||||
id: string;
|
||||
config: Types.BuilderConfig;
|
||||
}) => {
|
||||
const [update, set] = useState({ type: config.type, params: {} });
|
||||
const { mutate } = useWrite("UpdateBuilder");
|
||||
|
||||
return (
|
||||
<Section
|
||||
title="Config"
|
||||
icon={<Settings className="w-4 h-4" />}
|
||||
actions={
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
intent="warning"
|
||||
onClick={() => set({ type: builder.config.type, params: {} })}
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
</Button>
|
||||
<ConfirmUpdate
|
||||
content={JSON.stringify(update, null, 2)}
|
||||
onConfirm={() => {
|
||||
mutate({
|
||||
id,
|
||||
config: update,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<ConfigLayout
|
||||
content={update}
|
||||
onConfirm={() => mutate({ id, config: update })}
|
||||
onReset={() => set({ type: config.type, params: {} })}
|
||||
>
|
||||
<BuilderTypeSelector
|
||||
selected={type ?? builder.config.type}
|
||||
onSelect={(type) => setT(type)}
|
||||
/>
|
||||
{type === "Server" && (
|
||||
<ConfigAgain
|
||||
config={builder.config.params}
|
||||
update={update ?? {}}
|
||||
components={{
|
||||
id: (selected) => <div></div>,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{type === "Aws" && (
|
||||
<VariantConfig
|
||||
config={builder.config.params}
|
||||
update={update ?? {}}
|
||||
components={{
|
||||
region: (region) => (
|
||||
<ConfigInput
|
||||
label="Region"
|
||||
value={region}
|
||||
onChange={(region) => set((u) => ({ ...u, region }))}
|
||||
<div className="flex gap-4">
|
||||
<div className="flex flex-col gap-4 w-[300px]">
|
||||
<Button>General</Button>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>General</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4 mt-6">
|
||||
<div className="flex justify-between items-center border-b pb-4">
|
||||
Builder Type
|
||||
<BuilderTypeSelector
|
||||
selected={update.type}
|
||||
onSelect={(type) => set({ type, params: {} })}
|
||||
/>
|
||||
),
|
||||
instance_type: (instance_type) => (
|
||||
<ConfigInput
|
||||
label="Instance Type"
|
||||
value={instance_type}
|
||||
onChange={(t) => set((u) => ({ ...u, instance_type: t }))}
|
||||
</div>
|
||||
{update.type === "Server" && (
|
||||
<ServerConfig
|
||||
config={config.params as Types.ServerBuilderConfig}
|
||||
update={update.params}
|
||||
set={(u) =>
|
||||
set((p) => ({ ...p, params: { ...p.params, ...u } }))
|
||||
}
|
||||
/>
|
||||
),
|
||||
|
||||
volume_gb: (volume_gb) => (
|
||||
<ConfigInput
|
||||
label="Region"
|
||||
value={volume_gb}
|
||||
onChange={(ami_id) => set((u) => ({ ...u, ami_id }))}
|
||||
)}
|
||||
{update.type === "Aws" && (
|
||||
<AwsBuilderConfig
|
||||
config={config.params as Types.AwsBuilderConfig}
|
||||
update={update.params}
|
||||
set={(u) =>
|
||||
set((p) => ({ ...p, params: { ...p.params, ...u } }))
|
||||
}
|
||||
/>
|
||||
),
|
||||
ami_id: (ami_id) => (
|
||||
<ConfigInput
|
||||
label="AMI Id"
|
||||
value={ami_id}
|
||||
onChange={(ami_id) => set((u) => ({ ...u, ami_id }))}
|
||||
/>
|
||||
),
|
||||
subnet_id: (subnet_id) => (
|
||||
<ConfigInput
|
||||
label="Subnet Id"
|
||||
value={subnet_id}
|
||||
onChange={(subnet_id) => set((u) => ({ ...u, subnet_id }))}
|
||||
/>
|
||||
),
|
||||
security_group_ids: (ids) => <div>sec group ids</div>,
|
||||
key_pair_name: (key_pair_name) => (
|
||||
<ConfigInput
|
||||
label="Subnet Id"
|
||||
value={key_pair_name}
|
||||
onChange={(n) => set((u) => ({ ...u, key_pair_name: n }))}
|
||||
/>
|
||||
),
|
||||
assign_public_ip: () => <div>assign_public_ip</div>,
|
||||
// github_accounts: () => <div>github_accounts</div>,
|
||||
// docker_accounts: () => <div>docker_accounts</div>,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</ConfigLayout>
|
||||
);
|
||||
};
|
||||
|
||||
const BCWrapper = ({ id }: { id: string }) => {
|
||||
const config = useRead("GetBuilder", { id }).data?.config;
|
||||
if (!config) return null;
|
||||
return <BuilderConfig id={id} config={config} />;
|
||||
};
|
||||
|
||||
export const BuilderPage = () => {
|
||||
const id = useParams().builderId;
|
||||
|
||||
@@ -209,7 +229,7 @@ export const BuilderPage = () => {
|
||||
return (
|
||||
<Resource title={<BuilderName id={id} />} info={<></>} actions={<></>}>
|
||||
<ResourceUpdates type="Builder" id={id} />
|
||||
<BuilderConfig id={id} />
|
||||
<BCWrapper id={id} />
|
||||
</Resource>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ResourceUpdates } from "@components/updates/resource";
|
||||
import { useRead, useAddRecentlyViewed } from "@hooks";
|
||||
import { useRead, useAddRecentlyViewed, useWrite } from "@hooks";
|
||||
import { ResourceCard } from "@layouts/card";
|
||||
import { Resource } from "@layouts/resource";
|
||||
import { CardDescription } from "@ui/card";
|
||||
import { useParams, Link } from "react-router-dom";
|
||||
import { SerCon, ServerConfig } from "./config";
|
||||
import { SerCon } from "./config";
|
||||
import { ServerStats } from "./stats";
|
||||
import {
|
||||
ServerName,
|
||||
@@ -12,6 +12,101 @@ import {
|
||||
ServerSpecs,
|
||||
ServerRegion,
|
||||
} from "./util";
|
||||
import { ServerConfig } from "@monitor/client/dist/types";
|
||||
import { useState } from "react";
|
||||
import { Types } from "@monitor/client";
|
||||
import { ConfigLayout } from "@layouts/page";
|
||||
import { Button } from "@ui/button";
|
||||
import { ConfigAgain } from "@components/config/again";
|
||||
import { ConfigInput } from "@components/config/util";
|
||||
|
||||
export const ServerCard = ({ id }: { id: string }) => {
|
||||
const servers = useRead("ListServers", {}).data;
|
||||
const server = servers?.find((server) => server.id === id);
|
||||
if (!server) return null;
|
||||
|
||||
return (
|
||||
<Link to={`/servers/${server.id}`} key={server.id}>
|
||||
<ResourceCard
|
||||
title={server.name}
|
||||
description={server.info.status}
|
||||
statusIcon={<ServerStatusIcon serverId={server.id} />}
|
||||
// icon={<Server className="w-4 h-4" />}
|
||||
>
|
||||
<div className="flex flex-col text-sm">
|
||||
<ServerSpecs server_id={server.id} />
|
||||
<ServerRegion serverId={server.id} />
|
||||
</div>
|
||||
</ResourceCard>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const ServerConfigInner = ({
|
||||
id,
|
||||
config,
|
||||
}: {
|
||||
id: string;
|
||||
config: Types.ServerConfig;
|
||||
}) => {
|
||||
const [update, set] = useState<Partial<Types.ServerConfig>>({});
|
||||
const [show, setShow] = useState("general");
|
||||
const { mutate } = useWrite("UpdateServer");
|
||||
|
||||
<ConfigLayout
|
||||
content={update}
|
||||
onConfirm={() => mutate({ id, config: update })}
|
||||
onReset={() => set({})}
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex flex-col gap-4 w-[300px]">
|
||||
<Button>General</Button>
|
||||
</div>
|
||||
{show === "general" && (
|
||||
<ConfigAgain
|
||||
config={config}
|
||||
update={update}
|
||||
components={{
|
||||
address: (value) => (
|
||||
<ConfigInput
|
||||
label="Address"
|
||||
value={value}
|
||||
onChange={(address) => set((p) => ({ ...p, address }))}
|
||||
/>
|
||||
),
|
||||
region: (value) => (
|
||||
<ConfigInput
|
||||
label="region"
|
||||
value={value}
|
||||
onChange={(region) => set((p) => ({ ...p, region }))}
|
||||
/>
|
||||
),
|
||||
address: (value) => (
|
||||
<ConfigInput
|
||||
label="Address"
|
||||
value={value}
|
||||
onChange={(address) => set((p) => ({ ...p, address }))}
|
||||
/>
|
||||
),
|
||||
address: (value) => (
|
||||
<ConfigInput
|
||||
label="Address"
|
||||
value={value}
|
||||
onChange={(address) => set((p) => ({ ...p, address }))}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ConfigLayout>;
|
||||
};
|
||||
|
||||
const ServerConfig = ({ id }: { id: string }) => {
|
||||
const config = useRead("GetServer", { id }).data?.config;
|
||||
if (!config) return null;
|
||||
return <div></div>;
|
||||
};
|
||||
|
||||
export const ServerPage = () => {
|
||||
const id = useParams().serverId;
|
||||
@@ -37,25 +132,3 @@ export const ServerPage = () => {
|
||||
</Resource>
|
||||
);
|
||||
};
|
||||
|
||||
export const ServerCard = ({ id }: { id: string }) => {
|
||||
const servers = useRead("ListServers", {}).data;
|
||||
const server = servers?.find((server) => server.id === id);
|
||||
if (!server) return null;
|
||||
|
||||
return (
|
||||
<Link to={`/servers/${server.id}`} key={server.id}>
|
||||
<ResourceCard
|
||||
title={server.name}
|
||||
description={server.status}
|
||||
statusIcon={<ServerStatusIcon serverId={server.id} />}
|
||||
// icon={<Server className="w-4 h-4" />}
|
||||
>
|
||||
<div className="flex flex-col text-sm">
|
||||
<ServerSpecs server_id={server.id} />
|
||||
<ServerRegion serverId={server.id} />
|
||||
</div>
|
||||
</ResourceCard>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ export const ServerInfo = ({ serverId }: { serverId: string | undefined }) => {
|
||||
<div className="flex items-center gap-4 text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<ServerStatusIcon serverId={serverId} />
|
||||
<div> {server?.status}</div>
|
||||
<div> {server?.info.status}</div>
|
||||
</div>
|
||||
<CardDescription className="hidden md:block">|</CardDescription>
|
||||
{serverId && <ServerSpecs server_id={serverId} />}
|
||||
@@ -57,7 +57,7 @@ export const ServerRegion = ({
|
||||
return (
|
||||
<div className="flex gap-2 items-center text-muted-foreground">
|
||||
<MapPin className="w-4 h-4" />
|
||||
{server?.region || "region unknown"}
|
||||
{server?.info.region || "region unknown"}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -75,9 +75,9 @@ export const ServerStatusIcon = ({
|
||||
<Server
|
||||
className={cn(
|
||||
"w-4 h-4 stroke-primary",
|
||||
server?.status === ServerStatus.Ok && "fill-green-500",
|
||||
server?.status === ServerStatus.NotOk && "fill-red-500",
|
||||
server?.status === ServerStatus.Disabled && "fill-blue-500",
|
||||
server?.info.status === ServerStatus.Ok && "fill-green-500",
|
||||
server?.info.status === ServerStatus.NotOk && "fill-red-500",
|
||||
server?.info.status === ServerStatus.Disabled && "fill-blue-500",
|
||||
sm && "w-3 h-3"
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -300,4 +300,4 @@ export function readableImageNameTag(
|
||||
}
|
||||
|
||||
export const fmt_update_date = (d: Date) =>
|
||||
`${d.getDate()}/${d.getMonth()} @ ${d.getHours()}:${d.getMinutes()}`;
|
||||
`${d.getDate()}/${d.getMonth() + 1} @ ${d.getHours()}:${d.getMinutes()}`;
|
||||
|
||||
Reference in New Issue
Block a user