Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add beszel-extension extension #16437

Merged
merged 9 commits into from
Jan 28, 2025
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: improve error handling
wyattjoh committed Jan 17, 2025
commit 1580f4b959b8e913a9950c8fa7d062d91ef2c395
24 changes: 19 additions & 5 deletions extensions/beszel-extension/src/helpers/get-client.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { getPreferenceValues, LocalStorage } from "@raycast/api";
import PocketBase, { AsyncAuthStore } from "pocketbase";
import { fetch } from "undici";
import { EventSource } from "eventsource";
import { fetch } from "undici";

// Raycast doesn't provide a fetch global or EventSource global.

// @ts-expect-error - types are expecting node fetch but undici is compatible.
global.fetch = fetch;
global.EventSource = EventSource;

const BESZEL_TOKEN_KEY = "beszel-token";

let cachedClient: Promise<PocketBase> | null = null;
let cachedCredentials: Preferences.SearchSystems | null = null;

@@ -25,23 +27,33 @@ export async function getClient() {
return cachedClient;
}

// The credentials have changed or the client doesn't exist. If the client
// exists, clear the auth store.
if (cachedClient) {
const client = await cachedClient;
client.authStore.clear();

// Clear the token from local storage.
await LocalStorage.removeItem(BESZEL_TOKEN_KEY);
}

cachedClient = createClient({ url, username, password });
cachedCredentials = { url, username, password };

return cachedClient;
}

async function createClient({ url, username, password }: Preferences.SearchSystems): Promise<PocketBase> {
const initial = await LocalStorage.getItem<string>("beszel-token");
export async function createClient({ url, username, password }: Preferences.SearchSystems): Promise<PocketBase> {
const initial = await LocalStorage.getItem<string>(BESZEL_TOKEN_KEY);

// Create a new auth store.
const store = new AsyncAuthStore({
save: async (token) => {
await LocalStorage.setItem("beszel-token", token);
await LocalStorage.setItem(BESZEL_TOKEN_KEY, token);
},
initial,
clear: async () => {
await LocalStorage.removeItem("beszel-token");
await LocalStorage.removeItem(BESZEL_TOKEN_KEY);
},
});

@@ -50,6 +62,8 @@ async function createClient({ url, username, password }: Preferences.SearchSyste
// Disable auto-cancellation of requests.
client.autoCancellation(false);

// If the provided credentials are not valid, then perform an auth request to
// validate them and create the new token.
if (!client.authStore.isValid) {
await client.collection("users").authWithPassword(username, password);
}
75 changes: 42 additions & 33 deletions extensions/beszel-extension/src/hooks/use-systems.ts
Original file line number Diff line number Diff line change
@@ -78,52 +78,61 @@ export function useSystems() {
const [state, setState] = useCachedState<{
isLoading: boolean;
systems: BeszelSystem[] | undefined;
}>("beszel-systems", { isLoading: true, systems: undefined });
error: string | undefined;
}>("beszel-systems", { isLoading: true, systems: undefined, error: undefined });

useEffect(() => {
const controller = new AbortController();

let unsubscribe: (() => void) | undefined;
async function subscribe() {
setState(({ systems }) => ({ isLoading: true, systems }));

const client = await getClient();
setState(({ ...rest }) => ({ ...rest, isLoading: true }));

try {
const client = await getClient();

const systems = await client.collection("systems").getFullList<BeszelSystem>({
signal: controller.signal,
});

setState({ isLoading: false, systems });
} catch {
return;
setState({ isLoading: false, systems, error: undefined });

unsubscribe = await client.collection("systems").subscribe<BeszelSystem>(
"*",
(event) => {
setState(({ systems, ...rest }) => {
if (!systems) return { ...rest, systems };

switch (event.action) {
case "delete":
return { ...rest, systems: systems.filter((system) => system.id !== event.record.id) };
case "update":
return {
...rest,
systems: systems.map((system) => (system.id === event.record.id ? event.record : system)),
};
case "create":
return { ...rest, systems: [...systems, event.record] };
default:
return { ...rest, systems };
}
});
},
{
signal: controller.signal,
},
);
} catch (error) {
if (error instanceof Error) {
if ("isAbort" in error && error.isAbort) {
return;
}

setState({ isLoading: false, systems: undefined, error: error.message });
} else {
setState({ isLoading: false, systems: undefined, error: "An unknown error occurred" });
}
}

unsubscribe = await client.collection("systems").subscribe<BeszelSystem>(
"*",
(event) => {
setState(({ isLoading, systems }) => {
if (!systems) return { isLoading, systems };

switch (event.action) {
case "delete":
return { isLoading, systems: systems.filter((system) => system.id !== event.record.id) };
case "update":
return {
isLoading,
systems: systems.map((system) => (system.id === event.record.id ? event.record : system)),
};
case "create":
return { isLoading, systems: [...systems, event.record] };
default:
return { isLoading, systems };
}
});
},
{
signal: controller.signal,
},
);
}

subscribe();
40 changes: 38 additions & 2 deletions extensions/beszel-extension/src/search-systems.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
import { Action, ActionPanel, Color, getPreferenceValues, Icon, List } from "@raycast/api";
import { fetch } from "undici";
import { EventSource } from "eventsource";

// Raycast doesn't provide a fetch global or EventSource global.

// @ts-expect-error - types are expecting node fetch but undici is compatible.
global.fetch = fetch;
global.EventSource = EventSource;

import {
Action,
ActionPanel,
Color,
getPreferenceValues,
Icon,
List,
openExtensionPreferences,
showToast,
Toast,
} from "@raycast/api";

import { type BeszelSystem, useSystems } from "./hooks/use-systems";
import { secondsToUptime } from "./helpers/seconds-to-uptime";
import { useEffect } from "react";

function getSystemIconTintColor(system: BeszelSystem): Color {
switch (system.status) {
@@ -18,7 +38,23 @@ function getSystemIconTintColor(system: BeszelSystem): Color {

export default function Command() {
const { url } = getPreferenceValues<Preferences.SearchSystems>();
const { systems, isLoading } = useSystems();
const { systems, isLoading, error } = useSystems();

useEffect(() => {
if (!error) return;

showToast({
style: Toast.Style.Failure,
title: "Something went wrong",
message: error,
primaryAction: {
title: "Open Extension Preferences",
onAction: () => {
openExtensionPreferences();
},
},
});
}, [error]);

return (
<List isLoading={isLoading} isShowingDetail>