feat: ClusterLogs + ClusterOverview
This commit is contained in:
parent
f823d610d0
commit
7c8e5139aa
18 changed files with 438 additions and 117 deletions
BIN
desktop/src/assets/logos/fabric.png
Normal file
BIN
desktop/src/assets/logos/fabric.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
desktop/src/assets/logos/forge.png
Normal file
BIN
desktop/src/assets/logos/forge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
desktop/src/assets/logos/quilt.png
Normal file
BIN
desktop/src/assets/logos/quilt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
desktop/src/assets/logos/vanilla.png
Normal file
BIN
desktop/src/assets/logos/vanilla.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -1,5 +1,5 @@
|
|||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { type UnlistenFn, listen } from '@tauri-apps/api/event';
|
||||
|
||||
const _test_cluster = {
|
||||
cluster: {
|
||||
|
|
@ -32,6 +32,28 @@ export async function getCluster(uuid: string): Promise<Core.ClusterWithManifest
|
|||
);
|
||||
}
|
||||
|
||||
export async function getClusterLogs(uuid: string): Promise<string[]> {
|
||||
return ['test log'];
|
||||
try {
|
||||
return await invoke<string[]>(
|
||||
'plugin:onelauncher|get_cluster_logs',
|
||||
{ uuid },
|
||||
);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getClusterLog(uuid: string, log: string): Promise<string> {
|
||||
return 'test log';
|
||||
return await invoke<string>(
|
||||
'plugin:onelauncher|get_cluster_log',
|
||||
{ uuid, log },
|
||||
);
|
||||
}
|
||||
|
||||
export async function getClustersGrouped(): Promise<Map<string, WithIndex<Core.ClusterWithManifest>[]>> {
|
||||
const clusters = await getClusters();
|
||||
const map = new Map<string, WithIndex<Core.ClusterWithManifest>[]>();
|
||||
|
|
@ -80,19 +102,55 @@ interface LaunchCallbacks {
|
|||
on_stderr: (line: string) => any;
|
||||
};
|
||||
|
||||
export async function launchCluster(uuid: string, callbacks: LaunchCallbacks): Promise<number> {
|
||||
const unlisten_launch = await listen<number>('game:launch', e => callbacks.on_launch(e.payload));
|
||||
const unlisten_stdout = await listen<string>('game:stdout', e => callbacks.on_stdout(e.payload));
|
||||
const unlisten_stderr = await listen<string>('game:stderr', e => callbacks.on_stderr(e.payload));
|
||||
export function launchCluster(uuid: string, callbacks?: LaunchCallbacks): Promise<number> {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve) => {
|
||||
let unlisten_stdout: UnlistenFn | undefined;
|
||||
let unlisten_stderr: UnlistenFn | undefined;
|
||||
let pid: number = -1;
|
||||
|
||||
const exit_code = await invoke<number>(
|
||||
'plugin:onelauncher|launch_cluster',
|
||||
{ uuid },
|
||||
);
|
||||
function guard(passed_pid: number, fn: () => any | undefined) {
|
||||
if (passed_pid !== pid)
|
||||
return;
|
||||
|
||||
unlisten_launch();
|
||||
unlisten_stdout();
|
||||
unlisten_stderr();
|
||||
if (fn)
|
||||
fn();
|
||||
}
|
||||
|
||||
return exit_code;
|
||||
const unlisten_launch = await listen<number>('game:launch', async (e) => {
|
||||
pid = e.payload;
|
||||
|
||||
if (callbacks)
|
||||
callbacks.on_launch(pid);
|
||||
|
||||
unlisten_launch!();
|
||||
});
|
||||
|
||||
const unlisten_exit = await listen<[number, number]>(`game:exit`, e => guard(e.payload[0], () => {
|
||||
if (callbacks) {
|
||||
unlisten_stdout!();
|
||||
unlisten_stderr!();
|
||||
}
|
||||
|
||||
unlisten_exit!();
|
||||
resolve(e.payload[1]);
|
||||
}));
|
||||
|
||||
if (callbacks) {
|
||||
unlisten_stdout = await listen<[number, string]>(`game:stdout`, e => guard(
|
||||
e.payload[0],
|
||||
callbacks.on_stdout(e.payload[1]),
|
||||
));
|
||||
|
||||
unlisten_stderr = await listen<[number, string]>(`game:stderr`, e => guard(
|
||||
e.payload[0],
|
||||
callbacks.on_stderr(e.payload[1]),
|
||||
));
|
||||
}
|
||||
|
||||
await invoke<number>(
|
||||
'plugin:onelauncher|launch_cluster',
|
||||
{ uuid },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ img {
|
|||
}
|
||||
/* end lynith's native experience (tm) */
|
||||
|
||||
h1 { @apply text-2xl font-semibold; }
|
||||
h2 { @apply text-xxl font-semibold; }
|
||||
h1 { @apply text-2xl font-semibold; line-height: 1.5 !important; }
|
||||
h2 { @apply text-xxl font-semibold; line-height: 1.25 !important; }
|
||||
h3 { @apply text-xl font-medium; }
|
||||
h4 { @apply text-lg font-medium; }
|
||||
|
||||
|
|
@ -31,3 +31,7 @@ h4 { @apply text-lg font-medium; }
|
|||
--os-handle-bg-hover: theme(colors.gray[0.10]); */
|
||||
/* --os-handle-bg-active: theme(colors.gray[0.05]); */
|
||||
}
|
||||
|
||||
.os-hide-horizontal-scrollbar > .os-scrollbar-horizontal {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,17 +8,19 @@ import App from './ui/App';
|
|||
import HomePage from './ui/pages/Home';
|
||||
import BrowserPage from './ui/pages/Browser';
|
||||
import UpdatesPage from './ui/pages/Updates';
|
||||
import ClusterPage from './ui/pages/cluster';
|
||||
import ClusterOverview from './ui/pages/cluster/ClusterOverview';
|
||||
import ClusterMods from './ui/pages/cluster/ClusterMods';
|
||||
import ClusterRoot from '~ui/pages/cluster/ClusterRoot';
|
||||
import ClusterLogs from '~ui/pages/cluster/ClusterLogs';
|
||||
|
||||
render(() => (
|
||||
<Router root={App}>
|
||||
<Route path="/" component={HomePage} />
|
||||
<Route path="/browser" component={BrowserPage} />
|
||||
<Route path="/updates" component={UpdatesPage} />
|
||||
<Route path="/clusters" component={ClusterPage}>
|
||||
<Route path="/clusters" component={ClusterRoot}>
|
||||
<Route path="/" component={ClusterOverview} />
|
||||
<Route path="/logs" component={ClusterLogs} />
|
||||
<Route path="/mods" component={ClusterMods} />
|
||||
</Route>
|
||||
</Router>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import type { ParentProps } from 'solid-js';
|
||||
import { Transition } from 'solid-transition-group';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid';
|
||||
import environment from '../utils/environment';
|
||||
import WindowFrame from './components/WindowFrame';
|
||||
import Navbar from './components/Navbar';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
import AnimatedRoutes from './components/AnimatedRoutes';
|
||||
|
||||
function App(props: ParentProps) {
|
||||
if (!environment.isDev()) {
|
||||
|
|
@ -14,8 +14,8 @@ function App(props: ParentProps) {
|
|||
}
|
||||
|
||||
return (
|
||||
<main class="flex flex-col bg-primary w-full min-h-screen overflow-hidden h-screen max-h-screen text-white">
|
||||
<WindowFrame />
|
||||
<main class="flex flex-col bg-primary w-full min-h-screen overflow-hidden h-screen max-h-screen text-fg-primary">
|
||||
<ErrorBoundary><WindowFrame /></ErrorBoundary>
|
||||
<div class="flex flex-col px-8">
|
||||
<Navbar />
|
||||
</div>
|
||||
|
|
@ -23,71 +23,16 @@ function App(props: ParentProps) {
|
|||
<div class="relative h-full w-full overflow-hidden">
|
||||
<div class="absolute top-0 left-0 flex flex-col h-full w-full overflow-x-hidden">
|
||||
<ErrorBoundary>
|
||||
<OverlayScrollbarsComponent class="absolute top-0 left-0 flex flex-col h-full w-full overflow-x-hidden overflow-y-auto px-8">
|
||||
<OverlayScrollbarsComponent class="os-hide-horizontal-scrollbar absolute top-0 left-0 flex flex-col h-full w-full overflow-x-hidden overflow-y-auto px-8 pb-8">
|
||||
<AnimatedRoutes>
|
||||
{props.children}
|
||||
</AnimatedRoutes>
|
||||
<div class="pb-8" />
|
||||
</OverlayScrollbarsComponent>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
function AnimatedRoutes(props: ParentProps) {
|
||||
const keyframesEnter = [
|
||||
{
|
||||
opacity: 0,
|
||||
transform: 'translateX(-100px)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
transform: 'translateX(0px)',
|
||||
},
|
||||
];
|
||||
|
||||
const keyframesExit = [
|
||||
{
|
||||
opacity: 1,
|
||||
transform: 'translateX(0px)',
|
||||
},
|
||||
{
|
||||
opacity: 0,
|
||||
transform: 'translateX(100px)',
|
||||
},
|
||||
];
|
||||
|
||||
const properties: KeyframeAnimationOptions = {
|
||||
duration: 100,
|
||||
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition
|
||||
mode="outin"
|
||||
onEnter={(element, done) => {
|
||||
const animation = element.animate(
|
||||
keyframesEnter,
|
||||
properties,
|
||||
);
|
||||
|
||||
animation.onfinish = done;
|
||||
}}
|
||||
onExit={(element, done) => {
|
||||
const animation = element.animate(
|
||||
keyframesExit,
|
||||
properties,
|
||||
);
|
||||
|
||||
animation.onfinish = done;
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
export default App;
|
||||
57
desktop/src/ui/components/AnimatedRoutes.tsx
Normal file
57
desktop/src/ui/components/AnimatedRoutes.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import type { ParentProps } from 'solid-js';
|
||||
import { Transition } from 'solid-transition-group';
|
||||
|
||||
function AnimatedRoutes(props: ParentProps) {
|
||||
const keyframesEnter = [
|
||||
{
|
||||
opacity: 0,
|
||||
transform: 'translateX(-100px)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
transform: 'translateX(0px)',
|
||||
},
|
||||
];
|
||||
|
||||
const keyframesExit = [
|
||||
{
|
||||
opacity: 1,
|
||||
transform: 'translateX(0px)',
|
||||
},
|
||||
{
|
||||
opacity: 0,
|
||||
transform: 'translateX(100px)',
|
||||
},
|
||||
];
|
||||
|
||||
const properties: KeyframeAnimationOptions = {
|
||||
duration: 100,
|
||||
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition
|
||||
mode="outin"
|
||||
onEnter={(element, done) => {
|
||||
const animation = element.animate(
|
||||
keyframesEnter,
|
||||
properties,
|
||||
);
|
||||
|
||||
animation.onfinish = done;
|
||||
}}
|
||||
onExit={(element, done) => {
|
||||
const animation = element.animate(
|
||||
keyframesExit,
|
||||
properties,
|
||||
);
|
||||
|
||||
animation.onfinish = done;
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
||||
export default AnimatedRoutes;
|
||||
70
desktop/src/ui/components/Sidebar.tsx
Normal file
70
desktop/src/ui/components/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { useLocation, useNavigate } from '@solidjs/router';
|
||||
import type { JSX, ParentProps } from 'solid-js';
|
||||
import { For, createEffect } from 'solid-js';
|
||||
|
||||
type SidebarProps = ParentProps & {
|
||||
state: {
|
||||
[key: string]: any;
|
||||
};
|
||||
base: string;
|
||||
links: {
|
||||
[key: string]: [JSX.Element, string, string][];
|
||||
};
|
||||
};
|
||||
|
||||
function Sidebar(props: SidebarProps) {
|
||||
const searchParams = new URLSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
createEffect(() => {
|
||||
if (props.base.endsWith('/'))
|
||||
throw new Error('Base should not end with a slash');
|
||||
|
||||
Object.keys(props.state).forEach((key) => {
|
||||
searchParams.set(key, props.state[key]);
|
||||
});
|
||||
});
|
||||
|
||||
function goto(href: string) {
|
||||
const url = `${props.base}${href}?${searchParams.toString()}`;
|
||||
navigate(url);
|
||||
}
|
||||
|
||||
function isActive(link: string) {
|
||||
return location.pathname === `${props.base}${link}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col w-52">
|
||||
<For each={Object.keys(props.links)}>
|
||||
{section => (
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<div>
|
||||
<h3 class="text-fg-secondary text-xs font-medium ml-1.5 m-1">{section.toUpperCase()}</h3>
|
||||
<div class="flex flex-col gap-y-1 fill-fg-primary text-fg-primary font-medium">
|
||||
<For each={props.links[section]}>
|
||||
{link => (
|
||||
<a
|
||||
onClick={() => goto(link[2])}
|
||||
class={
|
||||
`px-3 py-1 rounded-md text-md hover:bg-component-bg-hover active:bg-component-bg-pressed ${isActive(link[2]) ? 'bg-gray-0.05' : ''}`
|
||||
}
|
||||
>
|
||||
<span class="flex flex-row items-center gap-x-3 *:w-5">
|
||||
{link[0]}
|
||||
{link[1]}
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sidebar;
|
||||
15
desktop/src/ui/components/game/ClusterCover.tsx
Normal file
15
desktop/src/ui/components/game/ClusterCover.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { type JSX, splitProps } from 'solid-js';
|
||||
import defaultCover from '~assets/images/default_instance_cover.jpg';
|
||||
|
||||
type ClusterCoverProps = JSX.HTMLAttributes<HTMLImageElement> & {
|
||||
cluster: Core.Cluster;
|
||||
};
|
||||
|
||||
function ClusterCover(props: ClusterCoverProps) {
|
||||
const [split, rest] = splitProps(props, ['cluster']);
|
||||
return (
|
||||
<img {...rest} src={split.cluster.cover || defaultCover} />
|
||||
);
|
||||
}
|
||||
|
||||
export default ClusterCover;
|
||||
31
desktop/src/ui/components/game/LoaderIcon.tsx
Normal file
31
desktop/src/ui/components/game/LoaderIcon.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { type JSX, splitProps } from 'solid-js';
|
||||
import fabric from '~assets/logos/fabric.png';
|
||||
import forge from '~assets/logos/forge.png';
|
||||
import quilt from '~assets/logos/quilt.png';
|
||||
import vanilla from '~assets/logos/vanilla.png';
|
||||
|
||||
export function getLoaderIcon(loader: Core.Loader) {
|
||||
switch (loader) {
|
||||
case 'Vanilla':
|
||||
return vanilla;
|
||||
case 'Forge':
|
||||
return forge;
|
||||
case 'Fabric':
|
||||
return fabric;
|
||||
case 'Quilt':
|
||||
return quilt;
|
||||
}
|
||||
}
|
||||
|
||||
type LoaderIconProp = JSX.HTMLAttributes<HTMLImageElement> & {
|
||||
loader: Core.Loader;
|
||||
};
|
||||
|
||||
function LoaderIcon(props: LoaderIconProp) {
|
||||
const [split, rest] = splitProps(props, ['loader']);
|
||||
return (
|
||||
<img {...rest} src={getLoaderIcon(split.loader)} alt={split.loader} />
|
||||
);
|
||||
}
|
||||
|
||||
export default LoaderIcon;
|
||||
13
desktop/src/ui/hooks/useCluster.ts
Normal file
13
desktop/src/ui/hooks/useCluster.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { Resource } from 'solid-js';
|
||||
import { createResource } from 'solid-js';
|
||||
import { getCluster } from '../../bridge/game';
|
||||
|
||||
function useCluster(uuid: string | undefined | null): Resource<Core.ClusterWithManifest> | null {
|
||||
if (typeof uuid !== 'string' || uuid.length === 0)
|
||||
return null;
|
||||
|
||||
const [resource] = createResource(uuid, getCluster);
|
||||
return resource;
|
||||
}
|
||||
|
||||
export default useCluster;
|
||||
|
|
@ -10,12 +10,12 @@ import type { Accessor, Setter } from 'solid-js';
|
|||
import { For, Show, createResource, createSignal } from 'solid-js';
|
||||
import { useNavigate } from '@solidjs/router';
|
||||
import image from '../../assets/images/header.png';
|
||||
import defaultCover from '../../assets/images/default_instance_cover.jpg';
|
||||
import ContextMenu from '../components/overlay/ContextMenu';
|
||||
import { createCluster, getClustersGrouped, launchCluster } from '../../bridge/game';
|
||||
import Button from '../components/base/Button';
|
||||
import Tag from '../components/base/Tag';
|
||||
import TextField from '../components/base/TextField';
|
||||
import ClusterCover from '~ui/components/game/ClusterCover';
|
||||
|
||||
// TODO: Replace this into it's own component
|
||||
// possibly with the core ui lirbary with autogenerated and optimized svgs pls
|
||||
|
|
@ -142,9 +142,9 @@ function ClusterCard(props: Core.ClusterWithManifest) {
|
|||
class="absolute h-full w-full group-hover:!scale-110 transition-transform"
|
||||
style={{ '-webkit-transform': 'translateZ(0)' }}
|
||||
>
|
||||
<img
|
||||
class={`object-cover h-full w-full ${props.cluster.cover ? '' : ' filter grayscale'}`}
|
||||
src={props.cluster.cover || defaultCover}
|
||||
<ClusterCover
|
||||
cluster={props.cluster}
|
||||
class="object-cover h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
61
desktop/src/ui/pages/cluster/ClusterLogs.tsx
Normal file
61
desktop/src/ui/pages/cluster/ClusterLogs.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { useSearchParams } from '@solidjs/router';
|
||||
import { For, Show, createEffect, createResource, createSignal } from 'solid-js';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid';
|
||||
import { getClusterLog, getClusterLogs } from '../../../bridge/game';
|
||||
|
||||
function ClusterLogs() {
|
||||
const [params] = useSearchParams();
|
||||
const [logs] = createResource(params.id, getClusterLogs);
|
||||
|
||||
const [activeLogFile, setActiveLogFile] = createSignal<string | null>(null);
|
||||
const [log, setLog] = createSignal<string | null>(null);
|
||||
|
||||
createEffect(() => {
|
||||
const log = logs()?.[0];
|
||||
|
||||
// Set default log to the first file
|
||||
if (log && activeLogFile() === null)
|
||||
setActiveLogFile(log);
|
||||
|
||||
// Fetch log content
|
||||
if (activeLogFile() !== null) {
|
||||
getClusterLog(params.id!, activeLogFile()!).then((log) => {
|
||||
setLog(log);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="flex flex-col flex-1">
|
||||
<h1>Logs</h1>
|
||||
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex flex-row gap-x-1 overflow-auto">
|
||||
<For each={logs() || []}>
|
||||
{(log) => {
|
||||
const pretty = log.split('/').pop();
|
||||
return (
|
||||
<button
|
||||
class={`border border-gray-0.10 rounded-md px-2 py-1 ${activeLogFile() === log ? 'bg-gray-0.10' : ''}`}
|
||||
onClick={() => setActiveLogFile(log)}
|
||||
children={pretty}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
{/* TODO: Try fix this when using OverlayScrollbarComponent https://github.com/KingSora/OverlayScrollbars/issues/627 */}
|
||||
<Show when={log() !== null} fallback={<span>No logs found</span>}>
|
||||
<div class="bg-component-bg flex flex-1 h-full font-mono font-medium overflow-auto p-2 rounded-md mt-2">
|
||||
<OverlayScrollbarsComponent class="flex-1 h-full relative">
|
||||
<code class="whitespace-pre select-text absolute">{log()}</code>
|
||||
</OverlayScrollbarsComponent>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClusterLogs;
|
||||
|
|
@ -1,7 +1,68 @@
|
|||
import { useSearchParams } from '@solidjs/router';
|
||||
import { Show } from 'solid-js';
|
||||
import { PlayIcon, Share07Icon } from '@untitled-theme/icons-solid';
|
||||
import useCluster from '../../hooks/useCluster';
|
||||
import ClusterCover from '../../components/game/ClusterCover';
|
||||
import LoaderIcon from '../../components/game/LoaderIcon';
|
||||
import Button from '../../components/base/Button';
|
||||
import { launchCluster } from '../../../bridge/game';
|
||||
|
||||
function ClusterOverview() {
|
||||
const [params] = useSearchParams();
|
||||
const cluster = useCluster(params.id);
|
||||
if (cluster === null)
|
||||
throw new Error('Cluster doesn\'t exist');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>This is the overview</h1>
|
||||
<div class="flex flex-col flex-1">
|
||||
<h1>Overview</h1>
|
||||
|
||||
<Show when={!cluster.loading}>
|
||||
<Banner {...cluster()!} />
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line solid/no-destructure
|
||||
function Banner({ cluster, manifest }: Core.ClusterWithManifest) {
|
||||
return (
|
||||
<div class="flex flex-row bg-component-bg rounded-xl p-2.5 gap-x-2.5">
|
||||
<div class="w-48 rounded-lg overflow-hidden border border-gray-0.10">
|
||||
<ClusterCover cluster={cluster} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-1 text-fg-primary">
|
||||
<div class="flex-1">
|
||||
<h2 class="text-2xl">{cluster.name}</h2>
|
||||
<span class="flex flex-row items-center gap-x-1">
|
||||
<LoaderIcon loader={cluster.client.type} class="w-5" />
|
||||
<span>{cluster.client.type}</span>
|
||||
<span>{manifest.manifest.id}</span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-fg-secondary">
|
||||
Played for
|
||||
{' '}
|
||||
<b>3.9</b>
|
||||
{' '}
|
||||
hours
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-end gap-x-2.5 *:h-8 *:w-8">
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
iconLeft={<Share07Icon />}
|
||||
/>
|
||||
<Button
|
||||
buttonStyle="primary"
|
||||
iconLeft={<PlayIcon />}
|
||||
children="Launch"
|
||||
class="!w-auto"
|
||||
onClick={() => launchCluster(cluster.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
38
desktop/src/ui/pages/cluster/ClusterRoot.tsx
Normal file
38
desktop/src/ui/pages/cluster/ClusterRoot.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { useSearchParams } from '@solidjs/router';
|
||||
import type { ParentProps } from 'solid-js';
|
||||
import { EyeIcon, File06Icon, PackagePlusIcon } from '@untitled-theme/icons-solid';
|
||||
import Sidebar from '../../components/Sidebar';
|
||||
import AnimatedRoutes from '../../components/AnimatedRoutes';
|
||||
import ErrorBoundary from '../../components/ErrorBoundary';
|
||||
|
||||
function ClusterRoot(props: ParentProps) {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
return (
|
||||
<div class="flex flex-row flex-1 h-full gap-x-7">
|
||||
<div class="mt-8">
|
||||
<Sidebar
|
||||
base="/clusters"
|
||||
state={{ id: searchParams.id }}
|
||||
links={{
|
||||
Cluster: [
|
||||
[<EyeIcon />, 'Overview', '/'],
|
||||
[<PackagePlusIcon />, 'Mods', '/mods'],
|
||||
[<File06Icon />, 'Logs', '/logs'],
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full h-full">
|
||||
<AnimatedRoutes>
|
||||
<ErrorBoundary>
|
||||
{props.children}
|
||||
</ErrorBoundary>
|
||||
</AnimatedRoutes>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClusterRoot;
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import { A, type AnchorProps, useNavigate, useSearchParams } from '@solidjs/router';
|
||||
import { type ParentProps, createEffect, splitProps } from 'solid-js';
|
||||
|
||||
function Link(props: AnchorProps) {
|
||||
const [split, rest] = splitProps(props, ['href']);
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
return (
|
||||
<A {...rest} href={`${split.href}?id=${searchParams.id}`} />
|
||||
);
|
||||
}
|
||||
|
||||
function ClusterPage(props: ParentProps) {
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof searchParams.id !== 'string')
|
||||
navigate('/');
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Link href="/clusters/">Overview</Link>
|
||||
<Link href="/clusters/mods">mods</Link>
|
||||
</div>
|
||||
{props.children}
|
||||
{searchParams.id}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClusterPage;
|
||||
Loading…
Add table
Reference in a new issue