Files
komodo/frontend/src/lib/hooks.ts
2024-06-07 21:02:28 -07:00

324 lines
8.8 KiB
TypeScript

import { AUTH_TOKEN_STORAGE_KEY, MONITOR_BASE_URL } from "@main";
import { MonitorClient as Client, Types } from "@monitor/client";
import {
AuthResponses,
ExecuteResponses,
ReadResponses,
UserResponses,
WriteResponses,
} from "@monitor/client/dist/responses";
import {
UseMutationOptions,
UseQueryOptions,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import { UsableResource } from "@types";
import { useToast } from "@ui/use-toast";
import { atom, useAtom } from "jotai";
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
// ============== RESOLVER ==============
const token = () => ({
jwt: localStorage.getItem(AUTH_TOKEN_STORAGE_KEY) ?? "",
});
const client = () => Client(MONITOR_BASE_URL, { type: "jwt", params: token() });
export const useLoginOptions = () =>
useQuery({
queryKey: ["GetLoginOptions"],
queryFn: () => client().auth({ type: "GetLoginOptions", params: {} }),
});
export const useUser = () =>
useQuery({
queryKey: ["GetUser"],
queryFn: () => client().auth({ type: "GetUser", params: {} }),
refetchInterval: 30_000,
});
export const useUserInvalidate = () => {
const qc = useQueryClient();
return () => {
qc.invalidateQueries({ queryKey: ["GetUser"] });
};
};
export const useRead = <
T extends Types.ReadRequest["type"],
R extends Extract<Types.ReadRequest, { type: T }>,
P extends R["params"],
C extends Omit<
UseQueryOptions<
ReadResponses[R["type"]],
unknown,
ReadResponses[R["type"]],
(T | P)[]
>,
"queryFn" | "queryKey"
>
>(
type: T,
params: P,
config?: C
) =>
useQuery({
queryKey: [type, params],
queryFn: () => client().read({ type, params } as R),
...config,
});
export const useInvalidate = () => {
const qc = useQueryClient();
return <
Type extends Types.ReadRequest["type"],
Params extends Extract<Types.ReadRequest, { type: Type }>["params"]
>(
...keys: Array<[Type] | [Type, Params]>
) => keys.forEach((key) => qc.invalidateQueries({ queryKey: key }));
};
export const useManageUser = <
T extends Types.UserRequest["type"],
R extends Extract<Types.UserRequest, { type: T }>,
P extends R["params"],
C extends Omit<
UseMutationOptions<UserResponses[T], unknown, P, unknown>,
"mutationKey" | "mutationFn"
>
>(
type: T,
config?: C
) => {
const { toast } = useToast();
return useMutation({
mutationKey: [type],
mutationFn: (params: P) => client().user({ type, params } as R),
onError: (e, v, c) => {
console.log("useManageUser error:", e);
toast({
title: `Request ${type} Failed`,
description: "See console for details",
variant: "destructive",
});
config?.onError && config.onError(e, v, c);
},
...config,
});
};
export const useWrite = <
T extends Types.WriteRequest["type"],
R extends Extract<Types.WriteRequest, { type: T }>,
P extends R["params"],
C extends Omit<
UseMutationOptions<WriteResponses[T], unknown, P, unknown>,
"mutationKey" | "mutationFn"
>
>(
type: T,
config?: C
) => {
const { toast } = useToast();
return useMutation({
mutationKey: [type],
mutationFn: (params: P) => client().write({ type, params } as R),
onError: (e, v, c) => {
console.log("useWrite error:", e);
toast({
title: `Write request ${type} failed`,
description: "See console for details",
variant: "destructive",
});
config?.onError && config.onError(e, v, c);
},
...config,
});
};
export const useExecute = <
T extends Types.ExecuteRequest["type"],
R extends Extract<Types.ExecuteRequest, { type: T }>,
P extends R["params"],
C extends Omit<
UseMutationOptions<ExecuteResponses[T], unknown, P, unknown>,
"mutationKey" | "mutationFn"
>
>(
type: T,
config?: C
) => {
const { toast } = useToast();
return useMutation({
mutationKey: [type],
mutationFn: (params: P) => client().execute({ type, params } as R),
onError: (e, v, c) => {
console.log("useExecute error:", e);
toast({
title: `Execute request ${type} failed`,
description: "See console for details",
variant: "destructive",
});
config?.onError && config.onError(e, v, c);
},
...config,
});
};
export const useAuth = <
T extends Types.AuthRequest["type"],
R extends Extract<Types.AuthRequest, { type: T }>,
P extends R["params"],
C extends Omit<
UseMutationOptions<AuthResponses[T], unknown, P, unknown>,
"mutationKey" | "mutationFn"
>
>(
type: T,
config?: C
) =>
useMutation({
mutationKey: [type],
mutationFn: (params: P) => client().auth({ type, params } as R),
...config,
});
// ============== UTILITY ==============
export const useResourceParamType = () => {
const type = useParams().type;
if (!type) return undefined;
if (type === "server-templates") return "ServerTemplate";
if (type === "resource-syncs") return "ResourceSync";
return (type[0].toUpperCase() + type.slice(1, -1)) as UsableResource;
};
export const usePushRecentlyViewed = ({ type, id }: Types.ResourceTarget) => {
const userInvalidate = useUserInvalidate();
const push = useManageUser("PushRecentlyViewed", {
onSuccess: userInvalidate,
}).mutate;
const exists = useRead(`List${type as UsableResource}s`, {}).data?.find(
(r) => r.id === id
)
? true
: false;
useEffect(() => {
exists && push({ resource: { type, id } });
}, [exists, push]);
return () => push({ resource: { type, id } });
};
export const useSetTitle = (more?: string) => {
const info = useRead("GetCoreInfo", {}).data;
const title = more ? `${more} | ${info?.title}` : info?.title;
useEffect(() => {
if (title) {
document.title = title;
}
}, [title]);
};
export const atomWithStorage = <T>(key: string, init: T) => {
const stored = localStorage.getItem(key);
const inner = atom(stored ? JSON.parse(stored) : init);
return atom(
(get) => get(inner),
(_, set, newValue) => {
set(inner, newValue);
localStorage.setItem(key, JSON.stringify(newValue));
}
);
};
export const tagsAtom = atomWithStorage<string[]>("tags-v0", []);
export const useTagsFilter = () => {
const [tags] = useAtom<string[]>(tagsAtom);
return tags;
};
/** returns function that takes a resource target and checks if it exists */
export const useCheckResourceExists = () => {
const servers = useRead("ListServers", {}).data;
const deployments = useRead("ListDeployments", {}).data;
const builds = useRead("ListBuilds", {}).data;
const repos = useRead("ListRepos", {}).data;
const procedures = useRead("ListProcedures", {}).data;
const builders = useRead("ListBuilders", {}).data;
const alerters = useRead("ListAlerters", {}).data;
return (target: Types.ResourceTarget) => {
switch (target.type) {
case "Server":
return servers?.some((resource) => resource.id === target.id) || false;
case "Deployment":
return (
deployments?.some((resource) => resource.id === target.id) || false
);
case "Build":
return builds?.some((resource) => resource.id === target.id) || false;
case "Repo":
return repos?.some((resource) => resource.id === target.id) || false;
case "Procedure":
return (
procedures?.some((resource) => resource.id === target.id) || false
);
case "Builder":
return builders?.some((resource) => resource.id === target.id) || false;
case "Alerter":
return alerters?.some((resource) => resource.id === target.id) || false;
default:
return false;
}
};
};
export const useFilterResources = <Info>(
resources?: Types.ResourceListItem<Info>[],
search?: string
) => {
const tags = useTagsFilter();
const searchSplit = search?.toLowerCase()?.split(" ") || [];
return (
resources?.filter(
(resource) =>
tags.every((tag: string) => resource.tags.includes(tag)) &&
(searchSplit.length > 0
? searchSplit.every((search) =>
resource.name.toLowerCase().includes(search)
)
: true)
) ?? []
);
};
export type LocalStorageSetter<T> = (state: T) => T;
export const useLocalStorage = <T>(
key: string,
init: T
): [T, (state: T | LocalStorageSetter<T>) => void] => {
const stored = localStorage.getItem(key);
const parsed = stored ? (JSON.parse(stored) as T) : undefined;
const [state, inner_set] = useState<T>(parsed ?? init);
const set = (state: T | LocalStorageSetter<T>) => {
inner_set((prev_state) => {
const new_val =
typeof state === "function"
? (state as LocalStorageSetter<T>)(prev_state)
: state;
localStorage.setItem(key, JSON.stringify(new_val));
return new_val;
});
};
return [state, set];
};