feat: Cluster log page
This commit is contained in:
parent
e4edbb85fa
commit
a09f6eb483
8 changed files with 169 additions and 45 deletions
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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('/');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue