feat: ClusterLogs + ClusterOverview

This commit is contained in:
LynithDev 2024-05-11 22:06:36 +02:00
parent f823d610d0
commit 7c8e5139aa
18 changed files with 438 additions and 117 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -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 },
);
});
}

View file

@ -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;
}

View file

@ -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>

View file

@ -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;

View 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;

View 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;

View 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;

View 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;

View 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;

View file

@ -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>

View 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;

View file

@ -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>
);
}

View 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;

View file

@ -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;