feat: filter disk type on templates
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@ -1,15 +1,27 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
export function FilterButton({ children, onClick, active }: { children: ReactNode, onClick?: () => Promise<void> | void, active?: boolean }) {
|
export function FilterButton({
|
||||||
return <div
|
children,
|
||||||
className={classNames("rounded-full outline outline-1 px-4 py-1 cursor-pointer select-none",
|
onClick,
|
||||||
|
active,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
onClick?: () => Promise<void> | void;
|
||||||
|
active?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"rounded-full outline outline-1 px-4 py-1 cursor-pointer select-none",
|
||||||
{
|
{
|
||||||
"bg-neutral-800 outline-neutral-300": active,
|
"bg-neutral-800 outline-neutral-300": active,
|
||||||
"bg-neutral-900 outline-neutral-800 text-neutral-500": !active
|
"bg-neutral-900 outline-neutral-800 text-neutral-500": !active,
|
||||||
}
|
},
|
||||||
)}
|
)}
|
||||||
onClick={onClick}>
|
onClick={onClick}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
@ -6,23 +6,38 @@ interface Price {
|
|||||||
}
|
}
|
||||||
type Cost = Price & { interval_type?: string };
|
type Cost = Price & { interval_type?: string };
|
||||||
|
|
||||||
export default function CostLabel({ cost }: { cost: Cost & { other_price?: Array<Price> } }) {
|
export default function CostLabel({
|
||||||
|
cost,
|
||||||
|
}: {
|
||||||
|
cost: Cost & { other_price?: Array<Price> };
|
||||||
|
}) {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
|
|
||||||
if (cost.currency === login?.currency) {
|
if (cost.currency === login?.currency) {
|
||||||
return <CostAmount cost={cost} converted={false} />
|
return <CostAmount cost={cost} converted={false} />;
|
||||||
} else {
|
} else {
|
||||||
const converted_price = cost.other_price?.find((p) => p.currency === login?.currency);
|
const converted_price = cost.other_price?.find(
|
||||||
|
(p) => p.currency === login?.currency,
|
||||||
|
);
|
||||||
if (converted_price) {
|
if (converted_price) {
|
||||||
return <div>
|
return (
|
||||||
<CostAmount cost={{
|
<div>
|
||||||
|
<CostAmount
|
||||||
|
cost={{
|
||||||
...converted_price,
|
...converted_price,
|
||||||
interval_type: cost.interval_type
|
interval_type: cost.interval_type,
|
||||||
}} converted={true} />
|
}}
|
||||||
<CostAmount cost={cost} converted={false} className="text-sm text-neutral-400" />
|
converted={true}
|
||||||
|
/>
|
||||||
|
<CostAmount
|
||||||
|
cost={cost}
|
||||||
|
converted={false}
|
||||||
|
className="text-sm text-neutral-400"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return <CostAmount cost={cost} converted={false} />
|
return <CostAmount cost={cost} converted={false} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -38,11 +53,19 @@ function intervalName(n: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CostAmount({ cost, converted, className }: { cost: Cost, converted: boolean, className?: string }) {
|
export function CostAmount({
|
||||||
const formatter = new Intl.NumberFormat('en-US', {
|
cost,
|
||||||
style: 'currency',
|
converted,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
cost: Cost;
|
||||||
|
converted: boolean;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const formatter = new Intl.NumberFormat("en-US", {
|
||||||
|
style: "currency",
|
||||||
currency: cost.currency,
|
currency: cost.currency,
|
||||||
trailingZeroDisplay: 'stripIfInteger'
|
trailingZeroDisplay: "stripIfInteger",
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
|
@ -3,7 +3,9 @@ import { useEffect, useRef } from "react";
|
|||||||
import { VmCostPlan } from "../api";
|
import { VmCostPlan } from "../api";
|
||||||
|
|
||||||
interface RevolutProps {
|
interface RevolutProps {
|
||||||
amount: VmCostPlan | {
|
amount:
|
||||||
|
| VmCostPlan
|
||||||
|
| {
|
||||||
amount: number;
|
amount: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
tax?: number;
|
tax?: number;
|
||||||
|
@ -46,19 +46,19 @@ export default function VmActions({
|
|||||||
title="Reinstall"
|
title="Reinstall"
|
||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if(confirm("Are you sure you want to re-install your vm?\nTHIS WILL DELETE ALL DATA!!")) {
|
if (
|
||||||
|
confirm(
|
||||||
|
"Are you sure you want to re-install your vm?\nTHIS WILL DELETE ALL DATA!!",
|
||||||
|
)
|
||||||
|
) {
|
||||||
await login?.api.reisntallVm(vm.id);
|
await login?.api.reisntallVm(vm.id);
|
||||||
onReload?.();
|
onReload?.();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="bg-neutral-700 hover:bg-neutral-600"
|
className="bg-neutral-700 hover:bg-neutral-600"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon name="refresh-1" size={30} />
|
||||||
name="refresh-1"
|
|
||||||
size={30}
|
|
||||||
/>
|
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -8,9 +8,7 @@ export default function VpsCard({ spec }: { spec: VmTemplate }) {
|
|||||||
<div className="rounded-xl border border-neutral-600 px-3 py-2 flex flex-col gap-2">
|
<div className="rounded-xl border border-neutral-600 px-3 py-2 flex flex-col gap-2">
|
||||||
<div className="text-xl">{spec.name}</div>
|
<div className="text-xl">{spec.name}</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>CPU: {spec.cpu}vCPU</li>
|
||||||
CPU: {spec.cpu}vCPU
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
RAM: <BytesSize value={spec.memory} />
|
RAM: <BytesSize value={spec.memory} />
|
||||||
</li>
|
</li>
|
||||||
@ -19,7 +17,9 @@ export default function VpsCard({ spec }: { spec: VmTemplate }) {
|
|||||||
</li>
|
</li>
|
||||||
<li>Location: {spec.region?.name}</li>
|
<li>Location: {spec.region?.name}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className="text-lg">{spec.cost_plan && <CostLabel cost={spec.cost_plan} />}</div>
|
<div className="text-lg">
|
||||||
|
{spec.cost_plan && <CostLabel cost={spec.cost_plan} />}
|
||||||
|
</div>
|
||||||
<VpsPayButton spec={spec} />
|
<VpsPayButton spec={spec} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -21,8 +21,9 @@ export function VpsCustomOrder({
|
|||||||
const [cpu, setCpu] = useState(params.min_cpu ?? 1);
|
const [cpu, setCpu] = useState(params.min_cpu ?? 1);
|
||||||
const [diskType, setDiskType] = useState(params.disks.at(0));
|
const [diskType, setDiskType] = useState(params.disks.at(0));
|
||||||
const [ram, setRam] = useState(Math.floor((params.min_memory ?? GiB) / GiB));
|
const [ram, setRam] = useState(Math.floor((params.min_memory ?? GiB) / GiB));
|
||||||
const [disk, setDisk] = useState(Math.floor((diskType?.min_disk ?? GiB) / GiB));
|
const [disk, setDisk] = useState(
|
||||||
|
Math.floor((diskType?.min_disk ?? GiB) / GiB),
|
||||||
|
);
|
||||||
|
|
||||||
const [price, setPrice] = useState<VmCustomPrice>();
|
const [price, setPrice] = useState<VmCustomPrice>();
|
||||||
|
|
||||||
@ -57,14 +58,18 @@ export function VpsCustomOrder({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 bg-neutral-900 rounded-xl px-4 py-6">
|
<div className="flex flex-col gap-4 bg-neutral-900 rounded-xl px-4 py-6">
|
||||||
<div className="text-lg">Custom VPS Order</div>
|
<div className="text-lg">Custom VPS Order</div>
|
||||||
{params.disks.length > 1 && <div className="flex gap-2">
|
{params.disks.length > 1 && (
|
||||||
{params.disks.map((d) =>
|
<div className="flex gap-2">
|
||||||
<FilterButton active={diskType?.disk_type === d.disk_type}
|
{params.disks.map((d) => (
|
||||||
onClick={() => setDiskType(d)}>
|
<FilterButton
|
||||||
|
active={diskType?.disk_type === d.disk_type}
|
||||||
|
onClick={() => setDiskType(d)}
|
||||||
|
>
|
||||||
{d.disk_type.toUpperCase()}
|
{d.disk_type.toUpperCase()}
|
||||||
</FilterButton>
|
</FilterButton>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>}
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="min-w-[100px]">{cpu} CPU</div>
|
<div className="min-w-[100px]">{cpu} CPU</div>
|
||||||
<input
|
<input
|
||||||
|
@ -49,10 +49,14 @@ export default function VpsPayment({
|
|||||||
className="cursor-pointer rounded-xl overflow-hidden"
|
className="cursor-pointer rounded-xl overflow-hidden"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div>{((payment.amount + payment.tax) / 1000).toLocaleString()} sats</div>
|
<div>
|
||||||
{payment.tax > 0 && <div className="text-xs">
|
{((payment.amount + payment.tax) / 1000).toLocaleString()} sats
|
||||||
|
</div>
|
||||||
|
{payment.tax > 0 && (
|
||||||
|
<div className="text-xs">
|
||||||
including {(payment.tax / 1000).toLocaleString()} sats tax
|
including {(payment.tax / 1000).toLocaleString()} sats tax
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="monospace select-all break-all text-center text-sm">
|
<div className="monospace select-all break-all text-center text-sm">
|
||||||
{invoice}
|
{invoice}
|
||||||
|
@ -19,7 +19,8 @@ export default function useLogin() {
|
|||||||
system,
|
system,
|
||||||
currency: session.currency,
|
currency: session.currency,
|
||||||
api: new LNVpsApi(ApiUrl, LoginState.getSigner()),
|
api: new LNVpsApi(ApiUrl, LoginState.getSigner()),
|
||||||
update: (fx: (ses: LoginSession) => void) => LoginState.updateSession(fx),
|
update: (fx: (ses: LoginSession) => void) =>
|
||||||
|
LoginState.updateSession(fx),
|
||||||
logout: () => LoginState.logout(),
|
logout: () => LoginState.logout(),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
@ -43,7 +43,7 @@ class LoginStore extends ExternalStore<LoginSession | undefined> {
|
|||||||
this.#session = {
|
this.#session = {
|
||||||
type: type ?? "nip7",
|
type: type ?? "nip7",
|
||||||
publicKey: pubkey,
|
publicKey: pubkey,
|
||||||
currency: "EUR"
|
currency: "EUR",
|
||||||
};
|
};
|
||||||
this.#save();
|
this.#save();
|
||||||
}
|
}
|
||||||
@ -54,7 +54,7 @@ class LoginStore extends ExternalStore<LoginSession | undefined> {
|
|||||||
type: "nsec",
|
type: "nsec",
|
||||||
publicKey: s.getPubKey(),
|
publicKey: s.getPubKey(),
|
||||||
privateKey: key,
|
privateKey: key,
|
||||||
currency: "EUR"
|
currency: "EUR",
|
||||||
};
|
};
|
||||||
this.#save();
|
this.#save();
|
||||||
}
|
}
|
||||||
@ -65,7 +65,7 @@ class LoginStore extends ExternalStore<LoginSession | undefined> {
|
|||||||
publicKey: remotePubkey,
|
publicKey: remotePubkey,
|
||||||
privateKey: localKey,
|
privateKey: localKey,
|
||||||
bunker: url,
|
bunker: url,
|
||||||
currency: "EUR"
|
currency: "EUR",
|
||||||
};
|
};
|
||||||
this.#save();
|
this.#save();
|
||||||
}
|
}
|
||||||
|
@ -15,19 +15,23 @@ export function AccountSettings() {
|
|||||||
}, [login]);
|
}, [login]);
|
||||||
|
|
||||||
if (!acc) return;
|
if (!acc) return;
|
||||||
return <div className="flex flex-col gap-4">
|
return (
|
||||||
<h3>
|
<div className="flex flex-col gap-4">
|
||||||
Account Settings
|
<h3>Account Settings</h3>
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<h4>Country</h4>
|
<h4>Country</h4>
|
||||||
<select value={acc?.country_code}
|
<select
|
||||||
|
value={acc?.country_code}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setAcc((s) => (s ? { ...s, country_code: e.target.value } : undefined))
|
setAcc((s) =>
|
||||||
|
s ? { ...s, country_code: e.target.value } : undefined,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{iso.all().map(c => <option value={c.alpha3}>{c.country}</option>)}
|
{iso.all().map((c) => (
|
||||||
|
<option value={c.alpha3}>{c.country}</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -83,4 +87,5 @@ export function AccountSettings() {
|
|||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,37 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, ReactNode } from "react";
|
||||||
import { LNVpsApi, VmHostRegion, VmTemplateResponse } from "../api";
|
import { DiskType, LNVpsApi, VmHostRegion, VmTemplateResponse } from "../api";
|
||||||
import VpsCard from "../components/vps-card";
|
import VpsCard from "../components/vps-card";
|
||||||
import { ApiUrl, NostrProfile } from "../const";
|
import { ApiUrl, NostrProfile } from "../const";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { VpsCustomOrder } from "../components/vps-custom";
|
import { VpsCustomOrder } from "../components/vps-custom";
|
||||||
import { LatestNews } from "../components/latest-news";
|
import { LatestNews } from "../components/latest-news";
|
||||||
import { FilterButton } from "../components/button-filter";
|
import { FilterButton } from "../components/button-filter";
|
||||||
import { dedupe } from "@snort/shared";
|
import { appendDedupe, dedupe } from "@snort/shared";
|
||||||
import useLogin from "../hooks/login";
|
import useLogin from "../hooks/login";
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const [offers, setOffers] = useState<VmTemplateResponse>();
|
const [offers, setOffers] = useState<VmTemplateResponse>();
|
||||||
const [region, setRegion] = useState<Array<number>>([]);
|
const [region, setRegion] = useState<Array<number>>([]);
|
||||||
|
const [diskType, setDiskType] = useState<Array<DiskType>>([]);
|
||||||
|
|
||||||
const regions = (offers?.templates.map((t) => t.region) ?? []).reduce((acc, v) => {
|
const regions = (offers?.templates.map((t) => t.region) ?? []).reduce(
|
||||||
|
(acc, v) => {
|
||||||
if (acc[v.id] === undefined) {
|
if (acc[v.id] === undefined) {
|
||||||
acc[v.id] = v;
|
acc[v.id] = v;
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<number, VmHostRegion>);
|
},
|
||||||
|
{} as Record<number, VmHostRegion>,
|
||||||
|
);
|
||||||
|
const diskTypes = dedupe(offers?.templates.map((t) => t.disk_type) ?? []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const api = new LNVpsApi(ApiUrl, undefined);
|
const api = new LNVpsApi(ApiUrl, undefined);
|
||||||
api.listOffers().then((o) => {
|
api.listOffers().then((o) => {
|
||||||
setOffers(o)
|
setOffers(o);
|
||||||
setRegion(dedupe(o.templates.map((z) => z.region.id)));
|
setRegion(dedupe(o.templates.map((z) => z.region.id)));
|
||||||
|
setDiskType(dedupe(o.templates.map((z) => z.disk_type)));
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -38,24 +44,58 @@ export default function HomePage() {
|
|||||||
Virtual Private Server hosting with flexible plans, high uptime, and
|
Virtual Private Server hosting with flexible plans, high uptime, and
|
||||||
dedicated support, tailored to your needs.
|
dedicated support, tailored to your needs.
|
||||||
</div>
|
</div>
|
||||||
{Object.keys(regions).length > 1 && <div className="flex gap-2 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
Regions:
|
{Object.keys(regions).length > 1 && (
|
||||||
|
<FilterSection header={"Region"}>
|
||||||
{Object.values(regions).map((r) => {
|
{Object.values(regions).map((r) => {
|
||||||
return <FilterButton
|
return (
|
||||||
|
<FilterButton
|
||||||
active={region.includes(r.id)}
|
active={region.includes(r.id)}
|
||||||
onClick={() => setRegion(x => {
|
onClick={() =>
|
||||||
|
setRegion((x) => {
|
||||||
if (x.includes(r.id)) {
|
if (x.includes(r.id)) {
|
||||||
return x.filter(y => y != r.id);
|
return x.filter((y) => y != r.id);
|
||||||
} else {
|
} else {
|
||||||
return [...x, r.id];
|
return appendDedupe(x, [r.id]);
|
||||||
}
|
}
|
||||||
})}>
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
{r.name}
|
{r.name}
|
||||||
</FilterButton>;
|
</FilterButton>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</div>}
|
</FilterSection>
|
||||||
|
)}
|
||||||
|
{diskTypes.length > 1 && (
|
||||||
|
<FilterSection header={"Disk"}>
|
||||||
|
{diskTypes.map((d) => (
|
||||||
|
<FilterButton
|
||||||
|
active={diskType.includes(d)}
|
||||||
|
onClick={() => {
|
||||||
|
setDiskType((s) => {
|
||||||
|
if (s?.includes(d)) {
|
||||||
|
return s.filter((y) => y !== d);
|
||||||
|
} else {
|
||||||
|
return appendDedupe(s, [d]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{d.toUpperCase()}
|
||||||
|
</FilterButton>
|
||||||
|
))}
|
||||||
|
</FilterSection>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
{offers?.templates.filter((t) => region.includes(t.region.id)).sort((a, b) => a.cost_plan.amount - b.cost_plan.amount).map((a) => <VpsCard spec={a} key={a.id} />)}
|
{offers?.templates
|
||||||
|
.filter(
|
||||||
|
(t) =>
|
||||||
|
region.includes(t.region.id) && diskType.includes(t.disk_type),
|
||||||
|
)
|
||||||
|
.sort((a, b) => a.cost_plan.amount - b.cost_plan.amount)
|
||||||
|
.map((a) => <VpsCard spec={a} key={a.id} />)}
|
||||||
{offers?.templates !== undefined && offers.templates.length === 0 && (
|
{offers?.templates !== undefined && offers.templates.length === 0 && (
|
||||||
<div className="text-red-500 bold text-xl uppercase">
|
<div className="text-red-500 bold text-xl uppercase">
|
||||||
No offers available
|
No offers available
|
||||||
@ -66,7 +106,8 @@ export default function HomePage() {
|
|||||||
<VpsCustomOrder templates={offers.custom_template} />
|
<VpsCustomOrder templates={offers.custom_template} />
|
||||||
)}
|
)}
|
||||||
<small className="text-neutral-400 text-center">
|
<small className="text-neutral-400 text-center">
|
||||||
All VPS come with 1x IPv4 and 1x IPv6 address and unmetered traffic, all prices are excluding taxes.
|
All VPS come with 1x IPv4 and 1x IPv6 address and unmetered traffic,
|
||||||
|
all prices are excluding taxes.
|
||||||
</small>
|
</small>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@ -93,18 +134,29 @@ export default function HomePage() {
|
|||||||
Speedtest
|
Speedtest
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{import.meta.env.VITE_FOOTER_NOTE_1 && <div className="text-xs text-center text-neutral-400">
|
{import.meta.env.VITE_FOOTER_NOTE_1 && (
|
||||||
|
<div className="text-xs text-center text-neutral-400">
|
||||||
{import.meta.env.VITE_FOOTER_NOTE_1}
|
{import.meta.env.VITE_FOOTER_NOTE_1}
|
||||||
</div>}
|
</div>
|
||||||
{import.meta.env.VITE_FOOTER_NOTE_2 && <div className="text-xs text-center text-neutral-400">
|
)}
|
||||||
|
{import.meta.env.VITE_FOOTER_NOTE_2 && (
|
||||||
|
<div className="text-xs text-center text-neutral-400">
|
||||||
{import.meta.env.VITE_FOOTER_NOTE_2}
|
{import.meta.env.VITE_FOOTER_NOTE_2}
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
<div className="text-sm text-center">
|
<div className="text-sm text-center">
|
||||||
Currency:
|
Currency:{" "}
|
||||||
{" "}
|
<select
|
||||||
<select value={login?.currency ?? "EUR"}
|
value={login?.currency ?? "EUR"}
|
||||||
onChange={(e) => login?.update(s => s.currency = e.target.value)}>
|
onChange={(e) =>
|
||||||
{["BTC", "EUR", "USD", "GBP", "AUD", "CAD", "CHF", "JPY"].map((a) => <option>{a}</option>)}
|
login?.update((s) => (s.currency = e.target.value))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{["BTC", "EUR", "USD", "GBP", "AUD", "CAD", "CHF", "JPY"].map(
|
||||||
|
(a) => (
|
||||||
|
<option>{a}</option>
|
||||||
|
),
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -112,3 +164,18 @@ export default function HomePage() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FilterSection({
|
||||||
|
header,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
header?: ReactNode;
|
||||||
|
children?: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 bg-neutral-900 px-3 py-2 rounded-xl">
|
||||||
|
<div className="text-md text-neutral-400">{header}</div>
|
||||||
|
<div className="flex gap-2 items-center">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -10,7 +10,6 @@ import { AttachAddon } from "@xterm/addon-attach";
|
|||||||
const fit = new FitAddon();
|
const fit = new FitAddon();
|
||||||
|
|
||||||
export function VmConsolePage() {
|
export function VmConsolePage() {
|
||||||
|
|
||||||
const { state } = useLocation() as { state?: VmInstance };
|
const { state } = useLocation() as { state?: VmInstance };
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const [term, setTerm] = useState<Terminal>();
|
const [term, setTerm] = useState<Terminal>();
|
||||||
@ -32,7 +31,7 @@ export function VmConsolePage() {
|
|||||||
if (t) {
|
if (t) {
|
||||||
t.dispose();
|
t.dispose();
|
||||||
}
|
}
|
||||||
return te
|
return te;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,8 +48,10 @@ export function VmConsolePage() {
|
|||||||
openTerminal();
|
openTerminal();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <div className="flex flex-col gap-4">
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
<div className="text-xl">VM #{state?.id} Terminal:</div>
|
<div className="text-xl">VM #{state?.id} Terminal:</div>
|
||||||
{term && <div className="border p-2" ref={termRef}></div>}
|
{term && <div className="border p-2" ref={termRef}></div>}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
@ -10,8 +10,6 @@ import { Icon } from "../components/icon";
|
|||||||
import Modal from "../components/modal";
|
import Modal from "../components/modal";
|
||||||
import SSHKeySelector from "../components/ssh-keys";
|
import SSHKeySelector from "../components/ssh-keys";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function VmPage() {
|
export default function VmPage() {
|
||||||
const location = useLocation() as { state?: VmInstance };
|
const location = useLocation() as { state?: VmInstance };
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
@ -66,14 +64,9 @@ export default function VmPage() {
|
|||||||
if ((state.ip_assignments?.length ?? 0) === 0) {
|
if ((state.ip_assignments?.length ?? 0) === 0) {
|
||||||
return <div className="text-sm text-red-500">No IP's assigned</div>;
|
return <div className="text-sm text-red-500">No IP's assigned</div>;
|
||||||
}
|
}
|
||||||
return (
|
return <>{state.ip_assignments?.map((i) => ipRow(i, true))}</>;
|
||||||
<>
|
|
||||||
{state.ip_assignments?.map((i) => ipRow(i, true))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const t = setInterval(() => reloadVmState(), 5000);
|
const t = setInterval(() => reloadVmState(), 5000);
|
||||||
return () => clearInterval(t);
|
return () => clearInterval(t);
|
||||||
|
Reference in New Issue
Block a user