feat: Cluster log page

This commit is contained in:
LynithDev 2024-07-30 14:12:58 +02:00
parent e4edbb85fa
commit a09f6eb483
8 changed files with 169 additions and 45 deletions

View file

@ -31,6 +31,8 @@ macro_rules! collect_commands {
get_cluster,
get_clusters,
run_cluster,
get_cluster_logs,
get_cluster_log,
// Processor
get_running_clusters,
get_processes_by_path,
@ -175,6 +177,23 @@ pub async fn get_cluster(uuid: Uuid) -> Result<Cluster, String> {
}
}
#[specta::specta]
#[tauri::command]
pub async fn get_cluster_logs(uuid: Uuid) -> Result<Vec<String>, String> {
let cluster = cluster::get_by_uuid(uuid, None).await?.ok_or("cluster not found")?;
let logs = cluster::get_logs(&cluster.cluster_path()).await?;
Ok(logs)
}
#[specta::specta]
#[tauri::command]
pub async fn get_cluster_log(uuid: Uuid, log_name: String) -> Result<String, String> {
let cluster = cluster::get_by_uuid(uuid, None).await?.ok_or("cluster not found")?;
let log = cluster::get_log(&cluster.cluster_path(), log_name).await?;
Ok(log)
}
#[specta::specta]
#[tauri::command]
pub async fn get_clusters() -> Result<Vec<Cluster>, String> {

View file

@ -8,7 +8,6 @@ type SidebarProps = ParentProps & {
};
function Sidebar(props: SidebarProps) {
const searchParams = new URLSearchParams();
const navigate = useNavigate();
const location = useLocation();
@ -18,7 +17,7 @@ function Sidebar(props: SidebarProps) {
});
function goto(href: string) {
const url = `${props.base}${href}?${searchParams.toString()}`;
const url = `${props.base}${href}${location.search.toString()}`;
navigate(url);
}

View file

@ -1,6 +1,10 @@
.dropdown {
@apply relative h-full;
&[data-disabled="true"] {
@apply pointer-events-none opacity-50;
}
.dropdown_elements_container {
@apply flex flex-col gap-1;
}
@ -20,7 +24,7 @@
}
.row {
@apply w-full flex flex-row justify-start items-center gap-1.5 h-4 text-nowrap overflow-hidden;
@apply w-full flex flex-row justify-start items-center gap-1.5 h-5 text-nowrap overflow-hidden;
> svg {
@apply min-w-4 min-h-4 w-4 h-4;
@ -34,4 +38,4 @@
}
}
}
}

View file

@ -21,7 +21,7 @@ export type DropdownProps = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'onChange'>
};
function Dropdown(props: DropdownProps) {
const [split, rest] = splitProps(props, ['children', 'class', 'text', 'onChange', 'selected', 'component', 'dropdownClass', 'listToolRow']);
const [split, rest] = splitProps(props, ['disabled', 'children', 'class', 'text', 'onChange', 'selected', 'component', 'dropdownClass', 'listToolRow']);
const [visible, setVisible] = createSignal(false);
// eslint-disable-next-line solid/reactivity -- todo
@ -60,7 +60,7 @@ function Dropdown(props: DropdownProps) {
);
return (
<div ref={ref} class={`${styles.dropdown} ${split.class || ''}`} {...rest}>
<div ref={ref} class={`${styles.dropdown} ${split.class || ''}`} data-disabled={split.disabled || false} {...rest}>
<Component />
<Popup

View file

@ -1,56 +1,98 @@
import { useSearchParams } from '@solidjs/router';
import { For, Show, createResource, createSignal } from 'solid-js';
import { For, Show, createEffect, createSignal, untrack } from 'solid-js';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid';
import { LinkExternal01Icon } from '@untitled-theme/icons-solid';
import { open } from '@tauri-apps/plugin-shell';
import useCommand, { tryResult } from '~ui/hooks/useCommand';
import { bridge } from '~imports';
import useClusterContext from '~ui/hooks/useCluster';
import Dropdown from '~ui/components/base/Dropdown';
import Button from '~ui/components/base/Button';
import useSettingsContext from '~ui/hooks/useSettings';
import joinPath from '~utils/helpers';
function ClusterLogs() {
const [params] = useSearchParams();
const [logs] = createResource(params.id, async (_uuid: string) => {
return [] as string[];
});
const [cluster] = useClusterContext();
const [logs] = useCommand(bridge.commands.getClusterLogs, cluster()!.uuid);
const settings = useSettingsContext();
const [activeLogFile, setActiveLogFile] = createSignal<string | null>(null);
const [log, _setLog] = createSignal<string | null>(null);
const [log, setLog] = createSignal<string | null>(null);
// createEffect(() => {
// const log = logs()?.[0];
function getAndSetLog(log_name: string) {
tryResult(bridge.commands.getClusterLog, cluster()!.uuid, log_name).then(setLog);
}
// // Set default log to the first file
// if (log && activeLogFile() === null)
// setActiveLogFile(log);
function changeLog(index: number) {
const log = untrack(() => logs());
if (log === undefined || log[index] === undefined)
return;
// // Fetch log content
// if (activeLogFile() !== null) {
// getClusterLog(params.id!, activeLogFile()!).then((log) => {
// setLog(log);
// });
// }
// });
setActiveLogFile(log[index]);
}
async function openFolder() {
const root = settings.config_dir;
const path = cluster()!.path;
if (root === null || root === undefined || path === null || path === undefined)
return;
const dir = joinPath(root, 'clusters', path);
open(joinPath(dir, 'logs'));
}
const missingLogs = () => logs() === undefined || logs()?.length === 0 || false;
createEffect(() => {
const log = logs()?.[0];
// Set default log to the first file
if (log !== undefined && activeLogFile() === null)
setActiveLogFile(log);
// Fetch log content
if (activeLogFile() !== null)
getAndSetLog(activeLogFile()!);
});
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-10 rounded-md px-2 py-1 ${activeLogFile() === log ? 'bg-gray-10' : ''}`}
onClick={() => setActiveLogFile(log)}
children={pretty}
/>
);
}}
</For>
<div class="flex flex-col flex-1 gap-y-2">
<div class="flex flex-row justify-between items-center gap-x-1 h-10">
<h1>Logs</h1>
<div class="flex flex-row gap-x-2">
<Dropdown
onChange={changeLog}
class="min-w-50"
disabled={missingLogs()}
>
<For each={logs() || ['None']}>
{(log) => {
const pretty = log.split('/').pop();
return (
<Dropdown.Row>
<div>
{pretty}
</div>
</Dropdown.Row>
);
}}
</For>
</Dropdown>
<Button
buttonStyle="primary"
onClick={openFolder}
children="Open Folder"
iconLeft={<LinkExternal01Icon />}
disabled={missingLogs()}
/>
</div>
</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>}>
<Show when={log() !== null} fallback={<span>No logs were 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>
<code class="whitespace-pre select-text absolute line-height-normal">{log()}</code>
</OverlayScrollbarsComponent>
</div>
</Show>

View file

@ -10,3 +10,32 @@ export function formatVersionRelease(release: VersionType): string {
return mapping[release];
}
/**
* A simple analog of Node.js's `path.join(...)`.
* https://gist.github.com/creationix/7435851#gistcomment-3698888
*/
export default function joinPath(...segments: string[]) {
const parts = segments.reduce<string[]>((parts, segment) => {
// Remove leading slashes from non-first part.
if (parts.length > 0)
segment = segment.replace(/^\//, '');
// Remove trailing slashes.
segment = segment.replace(/\/$/, '');
return parts.concat(segment.split('/'));
}, []);
const resultParts: string[] = [];
for (const part of parts) {
if (part === '.')
continue;
if (part === '..') {
resultParts.pop();
continue;
}
resultParts.push(part);
}
return resultParts.join('/');
}

View file

@ -239,6 +239,37 @@ pub async fn get_mod_path(
.into())
}
/// Get log names for a specific cluster by it's [`ClusterPath`].
#[tracing::instrument]
pub async fn get_logs(path: &ClusterPath) -> crate::Result<Vec<String>> {
let logs_dir = path.full_path().await?.join("logs");
let mut logs = io::read_dir(logs_dir).await?;
let mut log_names: Vec<String> = vec![];
while let Some(log) = logs.next_entry().await? {
if log.file_type().await?.is_file() {
let file_name = log.file_name().to_string_lossy().to_string();
if file_name == "latest.log" {
log_names.insert(0, file_name);
continue;
}
log_names.push(file_name);
}
}
Ok(log_names)
}
/// Get a log for a specific cluster by it's [`ClusterPath`] and log [`PathBuf`].
#[tracing::instrument]
pub async fn get_log(path: &ClusterPath, log_name: String) -> crate::Result<String> {
let log_path = path.full_path().await?.join("logs").join(log_name);
let log = io::read_to_string(&log_path).await?;
Ok(log)
}
/// edit a cluster with an async closure and it's [`ClusterPath`]
pub async fn edit<FutFn>(
path: &ClusterPath,

View file

@ -102,7 +102,7 @@ pub async fn install_java(java_version: u32) -> crate::Result<PathBuf> {
#[cfg(not(target_os = "macos"))]
{
base_path = base_path.join("bin").join(constants::JAVA_BIN)
base_path = base_path.join("bin").join(crate::constants::JAVA_BIN)
}
Ok(base_path)