milestone 9: react spa admin ui, frontend ci and embedded dist
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
import { queryOptions, type QueryClient } from "@tanstack/react-query";
|
||||
import * as api from "@/lib/api";
|
||||
import type {
|
||||
BlocklistInput,
|
||||
ClientEdit,
|
||||
ClientPrefixInput,
|
||||
ForwardZoneInput,
|
||||
GroupInput,
|
||||
LocalRecordInput,
|
||||
PausePost,
|
||||
Period,
|
||||
QueriesFilter,
|
||||
RuleInput,
|
||||
SettingsPatch,
|
||||
UpstreamInput,
|
||||
} from "@/lib/types";
|
||||
|
||||
export const queryKeys = {
|
||||
health: ["health"] as const,
|
||||
version: ["version"] as const,
|
||||
stats: (period: Period) => ["stats", period] as const,
|
||||
timeseries: (period: Period) => ["stats", "timeseries", period] as const,
|
||||
queries: (filter: QueriesFilter) => ["queries", filter] as const,
|
||||
upstreamHealth: ["upstream-health"] as const,
|
||||
lookup: (domain: string, groupId?: number) => ["lookup", domain, groupId ?? null] as const,
|
||||
groups: ["groups"] as const,
|
||||
groupSources: (id: number) => ["groups", id, "sources"] as const,
|
||||
blocklists: ["blocklists"] as const,
|
||||
/** Fed only by POST /api/blocklists/update's 202 snapshot; no GET exists. */
|
||||
blocklistSources: ["blocklists", "sources"] as const,
|
||||
rules: ["rules"] as const,
|
||||
localRecords: ["local-records"] as const,
|
||||
forwardZones: ["forward-zones"] as const,
|
||||
clients: ["clients"] as const,
|
||||
clientPrefixes: ["client-prefixes"] as const,
|
||||
upstreams: ["upstreams"] as const,
|
||||
pause: ["pause"] as const,
|
||||
settings: ["settings"] as const,
|
||||
};
|
||||
|
||||
export const healthQuery = () =>
|
||||
queryOptions({ queryKey: queryKeys.health, queryFn: api.getHealth, refetchInterval: 10_000 });
|
||||
|
||||
export const versionQuery = () =>
|
||||
queryOptions({ queryKey: queryKeys.version, queryFn: api.getVersion, staleTime: Infinity });
|
||||
|
||||
export const statsQuery = (period: Period = "24h") =>
|
||||
queryOptions({ queryKey: queryKeys.stats(period), queryFn: () => api.getStats(period), refetchInterval: 30_000 });
|
||||
|
||||
export const timeseriesQuery = (period: Period = "24h") =>
|
||||
queryOptions({
|
||||
queryKey: queryKeys.timeseries(period),
|
||||
queryFn: () => api.getStatsTimeseries(period),
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
|
||||
export const queriesQuery = (filter: QueriesFilter = {}) =>
|
||||
queryOptions({ queryKey: queryKeys.queries(filter), queryFn: () => api.getQueries(filter) });
|
||||
|
||||
export const upstreamHealthQuery = () =>
|
||||
queryOptions({ queryKey: queryKeys.upstreamHealth, queryFn: api.getUpstreamHealth, refetchInterval: 30_000 });
|
||||
|
||||
export const lookupQuery = (domain: string, groupId?: number) =>
|
||||
queryOptions({ queryKey: queryKeys.lookup(domain, groupId), queryFn: () => api.getLookup(domain, groupId) });
|
||||
|
||||
export const groupsQuery = () => queryOptions({ queryKey: queryKeys.groups, queryFn: api.listGroups });
|
||||
|
||||
export const groupSourcesQuery = (id: number) =>
|
||||
queryOptions({ queryKey: queryKeys.groupSources(id), queryFn: () => api.getGroupSources(id) });
|
||||
|
||||
export const blocklistsQuery = () => queryOptions({ queryKey: queryKeys.blocklists, queryFn: api.listBlocklists });
|
||||
|
||||
export const rulesQuery = () => queryOptions({ queryKey: queryKeys.rules, queryFn: api.listRules });
|
||||
|
||||
export const localRecordsQuery = () =>
|
||||
queryOptions({ queryKey: queryKeys.localRecords, queryFn: api.listLocalRecords });
|
||||
|
||||
export const forwardZonesQuery = () =>
|
||||
queryOptions({ queryKey: queryKeys.forwardZones, queryFn: api.listForwardZones });
|
||||
|
||||
export const clientsQuery = () => queryOptions({ queryKey: queryKeys.clients, queryFn: api.listClients });
|
||||
|
||||
export const clientPrefixesQuery = () =>
|
||||
queryOptions({ queryKey: queryKeys.clientPrefixes, queryFn: api.listClientPrefixes });
|
||||
|
||||
export const upstreamsQuery = () => queryOptions({ queryKey: queryKeys.upstreams, queryFn: api.listUpstreams });
|
||||
|
||||
export const pauseQuery = () => queryOptions({ queryKey: queryKeys.pause, queryFn: api.getPause });
|
||||
|
||||
export const settingsQuery = () => queryOptions({ queryKey: queryKeys.settings, queryFn: api.getSettings });
|
||||
|
||||
// Mutation option factories. Usage: useMutation(groupCreateMutation(useQueryClient())).
|
||||
// Group membership and names feed lookup verdicts and the group columns on
|
||||
// clients, prefixes and rules, hence the wide invalidation on group mutations.
|
||||
|
||||
function invalidateGroupWorld(qc: QueryClient): Promise<unknown> {
|
||||
return Promise.all([
|
||||
qc.invalidateQueries({ queryKey: queryKeys.groups }),
|
||||
qc.invalidateQueries({ queryKey: ["lookup"] }),
|
||||
qc.invalidateQueries({ queryKey: queryKeys.clients }),
|
||||
qc.invalidateQueries({ queryKey: queryKeys.clientPrefixes }),
|
||||
qc.invalidateQueries({ queryKey: queryKeys.rules }),
|
||||
]);
|
||||
}
|
||||
|
||||
export const groupCreateMutation = (qc: QueryClient) => ({
|
||||
mutationFn: (input: GroupInput) => api.createGroup(input),
|
||||
onSuccess: () => invalidateGroupWorld(qc),
|
||||
});
|
||||
|
||||
export const groupUpdateMutation = (qc: QueryClient) => ({
|
||||
mutationFn: ({ id, input }: { id: number; input: GroupInput }) => api.updateGroup(id, input),
|
||||
onSuccess: () => invalidateGroupWorld(qc),
|
||||
});
|
||||
|
||||
export const groupDeleteMutation = (qc: QueryClient) => ({
|
||||
mutationFn: (id: number) => api.deleteGroup(id),
|
||||
onSuccess: () => invalidateGroupWorld(qc),
|
||||
});
|
||||
|
||||
export const groupSourcesPutMutation = (qc: QueryClient) => ({
|
||||
mutationFn: ({ id, sourceIds }: { id: number; sourceIds: number[] }) => api.putGroupSources(id, sourceIds),
|
||||
onSuccess: (sourceIds: number[], { id }: { id: number; sourceIds: number[] }) => {
|
||||
qc.setQueryData(queryKeys.groupSources(id), sourceIds);
|
||||
return qc.invalidateQueries({ queryKey: ["lookup"] });
|
||||
},
|
||||
});
|
||||
|
||||
function invalidateBlocklistWorld(qc: QueryClient): Promise<unknown> {
|
||||
return Promise.all([
|
||||
qc.invalidateQueries({ queryKey: queryKeys.blocklists }),
|
||||
qc.invalidateQueries({ queryKey: ["lookup"] }),
|
||||
qc.invalidateQueries({ queryKey: ["groups"] }),
|
||||
]);
|
||||
}
|
||||
|
||||
export const blocklistCreateMutation = (qc: QueryClient) => ({
|
||||
mutationFn: (input: BlocklistInput) => api.createBlocklist(input),
|
||||
onSuccess: () => invalidateBlocklistWorld(qc),
|
||||
});
|
||||
|
||||
export const blocklistUpdateMutation = (qc: QueryClient) => ({
|
||||
mutationFn: ({ id, input }: { id: number; input: BlocklistInput }) => api.updateBlocklist(id, input),
|
||||
onSuccess: () => invalidateBlocklistWorld(qc),
|
||||
});
|
||||
|
||||
export const blocklistDeleteMutation = (qc: QueryClient) => ({
|
||||
mutationFn: (id: number) => api.deleteBlocklist(id),
|
||||
onSuccess: () => invalidateBlocklistWorld(qc),
|
||||
});
|
||||
|
||||
/** Ruling 12: the 202 snapshot REPLACES the sources cache; counters refresh. */
|
||||
export const blocklistsUpdateNowMutation = (qc: QueryClient) => ({
|
||||
mutationFn: () => api.updateBlocklistsNow(),
|
||||
onSuccess: (sources: Awaited<ReturnType<typeof api.updateBlocklistsNow>>) => {
|
||||
qc.setQueryData(queryKeys.blocklistSources, sources);
|
||||
return Promise.all([
|
||||
qc.invalidateQueries({ queryKey: queryKeys.blocklists }),
|
||||
qc.invalidateQueries({ queryKey: ["lookup"] }),
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
function invalidateRules(qc: QueryClient): Promise<unknown> {
|
||||
return Promise.all([
|
||||
qc.invalidateQueries({ queryKey: queryKeys.rules }),
|
||||
qc.invalidateQueries({ queryKey: ["lookup"] }),
|
||||
]);
|
||||
}
|
||||
|
||||
export const ruleCreateMutation = (qc: QueryClient) => ({
|
||||
mutationFn: (input: RuleInput) => api.createRule(input),
|
||||
onSuccess: () => invalidateRules(qc),
|
||||
});
|
||||
|
||||
export const ruleUpdateMutation = (qc: QueryClient) => ({
|
||||
mutationFn: ({ id, input }: { id: number; input: RuleInput }) => api.updateRule(id, input),
|
||||
onSuccess: () => invalidateRules(qc),
|
||||
});
|
||||
|
||||
export const ruleDeleteMutation = (qc: QueryClient) => ({
|
||||
mutationFn: (id: number) => api.deleteRule(id),
|
||||
onSuccess: () => invalidateRules(qc),
|
||||
});
|
||||
|
||||
function invalidateLocalRecords(qc: QueryClient): Promise<unknown> {
|
||||
return Promise.all([
|
||||
qc.invalidateQueries({ queryKey: queryKeys.localRecords }),
|
||||
qc.invalidateQueries({ queryKey: ["lookup"] }),
|
||||
]);
|
||||
}
|
||||
|
||||
export const localRecordCreateMutation = (qc: QueryClient) => ({
|
||||
mutationFn: (input: LocalRecordInput) => api.createLocalRecord(input),
|
||||
onSuccess: () => invalidateLocalRecords(qc),
|
||||
});
|
||||
|
||||
export const localRecordUpdateMutation = (qc: QueryClient) => ({
|
||||
mutationFn: ({ id, input }: { id: number; input: LocalRecordInput }) => api.updateLocalRecord(id, input),
|
||||
onSuccess: () => invalidateLocalRecords(qc),
|
||||
});
|
||||
|
||||
export const localRecordDeleteMutation = (qc: QueryClient) => ({
|
||||
mutationFn: (id: number) => api.deleteLocalRecord(id),
|
||||
onSuccess: () => invalidateLocalRecords(qc),
|
||||
});
|
||||
|
||||
function invalidateForwardZones(qc: QueryClient): Promise<unknown> {
|
||||
return Promise.all([
|
||||
qc.invalidateQueries({ queryKey: queryKeys.forwardZones }),
|
||||
qc.invalidateQueries({ queryKey: ["lookup"] }),
|
||||
]);
|
||||
}
|
||||
|
||||
export const forwardZoneCreateMutation = (qc: QueryClient) => ({
|
||||
mutationFn: (input: ForwardZoneInput) => api.createForwardZone(input),
|
||||
onSuccess: () => invalidateForwardZones(qc),
|
||||
});
|
||||
|
||||
export const forwardZoneUpdateMutation = (qc: QueryClient) => ({
|
||||
mutationFn: ({ id, input }: { id: number; input: ForwardZoneInput }) => api.updateForwardZone(id, input),
|
||||
onSuccess: () => invalidateForwardZones(qc),
|
||||
});
|
||||
|
||||
export const forwardZoneDeleteMutation = (qc: QueryClient) => ({
|
||||
mutationFn: (id: number) => api.deleteForwardZone(id),
|
||||
onSuccess: () => invalidateForwardZones(qc),
|
||||
});
|
||||
|
||||
export const clientUpdateMutation = (qc: QueryClient) => ({
|
||||
mutationFn: ({ id, edit }: { id: number; edit: ClientEdit }) => api.updateClient(id, edit),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.clients }),
|
||||
});
|
||||
|
||||
export const clientDeleteMutation = (qc: QueryClient) => ({
|
||||
mutationFn: (id: number) => api.deleteClient(id),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: queryKeys.clients }),
|
||||
});
|
||||
|
||||
export const clientPrefixesPutMutation = (qc: QueryClient) => ({
|
||||
mutationFn: (prefixes: ClientPrefixInput[]) => api.putClientPrefixes(prefixes),
|
||||
onSuccess: (stored: Awaited<ReturnType<typeof api.putClientPrefixes>>) => {
|
||||
qc.setQueryData(queryKeys.clientPrefixes, stored);
|
||||
},
|
||||
});
|
||||
|
||||
function invalidateUpstreams(qc: QueryClient): Promise<unknown> {
|
||||
return qc.invalidateQueries({ queryKey: queryKeys.upstreams });
|
||||
}
|
||||
|
||||
export const upstreamCreateMutation = (qc: QueryClient) => ({
|
||||
mutationFn: (input: UpstreamInput) => api.createUpstream(input),
|
||||
onSuccess: () => invalidateUpstreams(qc),
|
||||
});
|
||||
|
||||
export const upstreamUpdateMutation = (qc: QueryClient) => ({
|
||||
mutationFn: ({ id, input }: { id: number; input: UpstreamInput }) => api.updateUpstream(id, input),
|
||||
onSuccess: () => invalidateUpstreams(qc),
|
||||
});
|
||||
|
||||
export const upstreamDeleteMutation = (qc: QueryClient) => ({
|
||||
mutationFn: (id: number) => api.deleteUpstream(id),
|
||||
onSuccess: () => invalidateUpstreams(qc),
|
||||
});
|
||||
|
||||
export const pauseMutation = (qc: QueryClient) => ({
|
||||
mutationFn: (body: PausePost) => api.postPause(body),
|
||||
onSuccess: (state: Awaited<ReturnType<typeof api.postPause>>) => {
|
||||
qc.setQueryData(queryKeys.pause, state);
|
||||
},
|
||||
});
|
||||
|
||||
export const settingsPutMutation = (qc: QueryClient) => ({
|
||||
mutationFn: (patch: SettingsPatch) => api.putSettings(patch),
|
||||
onSuccess: (envelope: Awaited<ReturnType<typeof api.putSettings>>) => {
|
||||
qc.setQueryData(queryKeys.settings, envelope);
|
||||
return qc.invalidateQueries({ queryKey: queryKeys.settings });
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user