feat: OPNsense integration and widget (#3424)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com> Co-authored-by: deepsource-io[bot] <42547082+deepsource-io[bot]@users.noreply.github.com>
This commit is contained in:
215
packages/api/src/router/widgets/firewall.ts
Normal file
215
packages/api/src/router/widgets/firewall.ts
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
import { observable } from "@trpc/server/observable";
|
||||||
|
|
||||||
|
import type { Modify } from "@homarr/common/types";
|
||||||
|
import type { Integration } from "@homarr/db/schema";
|
||||||
|
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||||
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
|
import type {
|
||||||
|
FirewallCpuSummary,
|
||||||
|
FirewallInterfacesSummary,
|
||||||
|
FirewallMemorySummary,
|
||||||
|
FirewallVersionSummary,
|
||||||
|
} from "@homarr/integrations";
|
||||||
|
import {
|
||||||
|
firewallCpuRequestHandler,
|
||||||
|
firewallInterfacesRequestHandler,
|
||||||
|
firewallMemoryRequestHandler,
|
||||||
|
firewallVersionRequestHandler,
|
||||||
|
} from "@homarr/request-handler/firewall";
|
||||||
|
|
||||||
|
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
|
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||||
|
|
||||||
|
export const firewallRouter = createTRPCRouter({
|
||||||
|
getFirewallCpuStatus: publicProcedure
|
||||||
|
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")))
|
||||||
|
.query(async ({ ctx }) => {
|
||||||
|
const results = await Promise.all(
|
||||||
|
ctx.integrations.map(async (integration) => {
|
||||||
|
const innerHandler = firewallCpuRequestHandler.handler(integration, {});
|
||||||
|
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||||
|
|
||||||
|
return {
|
||||||
|
integration: {
|
||||||
|
id: integration.id,
|
||||||
|
name: integration.name,
|
||||||
|
kind: integration.kind,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
},
|
||||||
|
summary: data,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return results;
|
||||||
|
}),
|
||||||
|
subscribeFirewallCpuStatus: publicProcedure
|
||||||
|
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")))
|
||||||
|
.subscription(({ ctx }) => {
|
||||||
|
return observable<{
|
||||||
|
integration: Modify<Integration, { kind: IntegrationKindByCategory<"firewall"> }>;
|
||||||
|
summary: FirewallCpuSummary;
|
||||||
|
}>((emit) => {
|
||||||
|
const unsubscribes: (() => void)[] = [];
|
||||||
|
for (const integrationWithSecrets of ctx.integrations) {
|
||||||
|
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
|
||||||
|
const innerHandler = firewallCpuRequestHandler.handler(integrationWithSecrets, {});
|
||||||
|
const unsubscribe = innerHandler.subscribe((summary) => {
|
||||||
|
emit.next({
|
||||||
|
integration,
|
||||||
|
summary,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
unsubscribes.push(unsubscribe);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
unsubscribes.forEach((unsubscribe) => {
|
||||||
|
unsubscribe();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
getFirewallInterfacesStatus: publicProcedure
|
||||||
|
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")))
|
||||||
|
.query(async ({ ctx }) => {
|
||||||
|
const results = await Promise.all(
|
||||||
|
ctx.integrations.map(async (integration) => {
|
||||||
|
const innerHandler = firewallInterfacesRequestHandler.handler(integration, {});
|
||||||
|
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||||
|
|
||||||
|
return {
|
||||||
|
integration: {
|
||||||
|
id: integration.id,
|
||||||
|
name: integration.name,
|
||||||
|
kind: integration.kind,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
},
|
||||||
|
summary: data,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return results;
|
||||||
|
}),
|
||||||
|
subscribeFirewallInterfacesStatus: publicProcedure
|
||||||
|
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")))
|
||||||
|
.subscription(({ ctx }) => {
|
||||||
|
return observable<{
|
||||||
|
integration: Modify<Integration, { kind: IntegrationKindByCategory<"firewall"> }>;
|
||||||
|
summary: FirewallInterfacesSummary[];
|
||||||
|
}>((emit) => {
|
||||||
|
const unsubscribes: (() => void)[] = [];
|
||||||
|
for (const integrationWithSecrets of ctx.integrations) {
|
||||||
|
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
|
||||||
|
const innerHandler = firewallInterfacesRequestHandler.handler(integrationWithSecrets, {});
|
||||||
|
const unsubscribe = innerHandler.subscribe((summary) => {
|
||||||
|
emit.next({
|
||||||
|
integration,
|
||||||
|
summary,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
unsubscribes.push(unsubscribe);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
unsubscribes.forEach((unsubscribe) => {
|
||||||
|
unsubscribe();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
getFirewallVersionStatus: publicProcedure
|
||||||
|
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")))
|
||||||
|
.query(async ({ ctx }) => {
|
||||||
|
const results = await Promise.all(
|
||||||
|
ctx.integrations.map(async (integration) => {
|
||||||
|
const innerHandler = firewallVersionRequestHandler.handler(integration, {});
|
||||||
|
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||||
|
|
||||||
|
return {
|
||||||
|
integration: {
|
||||||
|
id: integration.id,
|
||||||
|
name: integration.name,
|
||||||
|
kind: integration.kind,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
},
|
||||||
|
summary: data,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return results;
|
||||||
|
}),
|
||||||
|
subscribeFirewallVersionStatus: publicProcedure
|
||||||
|
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")))
|
||||||
|
.subscription(({ ctx }) => {
|
||||||
|
return observable<{
|
||||||
|
integration: Modify<Integration, { kind: IntegrationKindByCategory<"firewall"> }>;
|
||||||
|
summary: FirewallVersionSummary;
|
||||||
|
}>((emit) => {
|
||||||
|
const unsubscribes: (() => void)[] = [];
|
||||||
|
for (const integrationWithSecrets of ctx.integrations) {
|
||||||
|
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
|
||||||
|
const innerHandler = firewallVersionRequestHandler.handler(integrationWithSecrets, {});
|
||||||
|
const unsubscribe = innerHandler.subscribe((summary) => {
|
||||||
|
emit.next({
|
||||||
|
integration,
|
||||||
|
summary,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
unsubscribes.push(unsubscribe);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
unsubscribes.forEach((unsubscribe) => {
|
||||||
|
unsubscribe();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
getFirewallMemoryStatus: publicProcedure
|
||||||
|
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")))
|
||||||
|
.query(async ({ ctx }) => {
|
||||||
|
const results = await Promise.all(
|
||||||
|
ctx.integrations.map(async (integration) => {
|
||||||
|
const innerHandler = firewallMemoryRequestHandler.handler(integration, {});
|
||||||
|
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||||
|
|
||||||
|
return {
|
||||||
|
integration: {
|
||||||
|
id: integration.id,
|
||||||
|
name: integration.name,
|
||||||
|
kind: integration.kind,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
},
|
||||||
|
summary: data,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return results;
|
||||||
|
}),
|
||||||
|
subscribeFirewallMemoryStatus: publicProcedure
|
||||||
|
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")))
|
||||||
|
.subscription(({ ctx }) => {
|
||||||
|
return observable<{
|
||||||
|
integration: Modify<Integration, { kind: IntegrationKindByCategory<"firewall"> }>;
|
||||||
|
summary: FirewallMemorySummary;
|
||||||
|
}>((emit) => {
|
||||||
|
const unsubscribes: (() => void)[] = [];
|
||||||
|
for (const integrationWithSecrets of ctx.integrations) {
|
||||||
|
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
|
||||||
|
const innerHandler = firewallMemoryRequestHandler.handler(integrationWithSecrets, {});
|
||||||
|
const unsubscribe = innerHandler.subscribe((summary) => {
|
||||||
|
emit.next({
|
||||||
|
integration,
|
||||||
|
summary,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
unsubscribes.push(unsubscribe);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
unsubscribes.forEach((unsubscribe) => {
|
||||||
|
unsubscribe();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -3,6 +3,7 @@ import { appRouter } from "./app";
|
|||||||
import { calendarRouter } from "./calendar";
|
import { calendarRouter } from "./calendar";
|
||||||
import { dnsHoleRouter } from "./dns-hole";
|
import { dnsHoleRouter } from "./dns-hole";
|
||||||
import { downloadsRouter } from "./downloads";
|
import { downloadsRouter } from "./downloads";
|
||||||
|
import { firewallRouter } from "./firewall";
|
||||||
import { healthMonitoringRouter } from "./health-monitoring";
|
import { healthMonitoringRouter } from "./health-monitoring";
|
||||||
import { indexerManagerRouter } from "./indexer-manager";
|
import { indexerManagerRouter } from "./indexer-manager";
|
||||||
import { mediaReleaseRouter } from "./media-release";
|
import { mediaReleaseRouter } from "./media-release";
|
||||||
@@ -40,5 +41,6 @@ export const widgetRouter = createTRPCRouter({
|
|||||||
options: optionsRouter,
|
options: optionsRouter,
|
||||||
releases: releasesRouter,
|
releases: releasesRouter,
|
||||||
networkController: networkControllerRouter,
|
networkController: networkControllerRouter,
|
||||||
|
firewall: firewallRouter,
|
||||||
notifications: notificationsRouter,
|
notifications: notificationsRouter,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { checkCron } from "./validation";
|
import { checkCron } from "./validation";
|
||||||
|
|
||||||
export const EVERY_5_SECONDS = checkCron("*/5 * * * * *") satisfies string;
|
export const EVERY_5_SECONDS = checkCron("*/5 * * * * *") satisfies string;
|
||||||
|
export const EVERY_30_SECONDS = checkCron("*/30 * * * * *") satisfies string;
|
||||||
export const EVERY_MINUTE = checkCron("* * * * *") satisfies string;
|
export const EVERY_MINUTE = checkCron("* * * * *") satisfies string;
|
||||||
export const EVERY_5_MINUTES = checkCron("*/5 * * * *") satisfies string;
|
export const EVERY_5_MINUTES = checkCron("*/5 * * * *") satisfies string;
|
||||||
export const EVERY_10_MINUTES = checkCron("*/10 * * * *") satisfies string;
|
export const EVERY_10_MINUTES = checkCron("*/10 * * * *") satisfies string;
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ import { dockerContainersJob } from "./jobs/docker";
|
|||||||
import { iconsUpdaterJob } from "./jobs/icons-updater";
|
import { iconsUpdaterJob } from "./jobs/icons-updater";
|
||||||
import { dnsHoleJob } from "./jobs/integrations/dns-hole";
|
import { dnsHoleJob } from "./jobs/integrations/dns-hole";
|
||||||
import { downloadsJob } from "./jobs/integrations/downloads";
|
import { downloadsJob } from "./jobs/integrations/downloads";
|
||||||
|
import {
|
||||||
|
firewallCpuJob,
|
||||||
|
firewallInterfacesJob,
|
||||||
|
firewallMemoryJob,
|
||||||
|
firewallVersionJob,
|
||||||
|
} from "./jobs/integrations/firewall";
|
||||||
import { healthMonitoringJob } from "./jobs/integrations/health-monitoring";
|
import { healthMonitoringJob } from "./jobs/integrations/health-monitoring";
|
||||||
import { smartHomeEntityStateJob } from "./jobs/integrations/home-assistant";
|
import { smartHomeEntityStateJob } from "./jobs/integrations/home-assistant";
|
||||||
import { indexerManagerJob } from "./jobs/integrations/indexer-manager";
|
import { indexerManagerJob } from "./jobs/integrations/indexer-manager";
|
||||||
@@ -39,6 +45,10 @@ export const jobGroup = createCronJobGroup({
|
|||||||
minecraftServerStatus: minecraftServerStatusJob,
|
minecraftServerStatus: minecraftServerStatusJob,
|
||||||
dockerContainers: dockerContainersJob,
|
dockerContainers: dockerContainersJob,
|
||||||
networkController: networkControllerJob,
|
networkController: networkControllerJob,
|
||||||
|
firewallCpu: firewallCpuJob,
|
||||||
|
firewallMemory: firewallMemoryJob,
|
||||||
|
firewallVersion: firewallVersionJob,
|
||||||
|
firewallInterfaces: firewallInterfacesJob,
|
||||||
refreshNotifications: refreshNotificationsJob,
|
refreshNotifications: refreshNotificationsJob,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
46
packages/cron-jobs/src/jobs/integrations/firewall.ts
Normal file
46
packages/cron-jobs/src/jobs/integrations/firewall.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { EVERY_5_SECONDS, EVERY_30_SECONDS, EVERY_HOUR, EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions";
|
||||||
|
import {
|
||||||
|
firewallCpuRequestHandler,
|
||||||
|
firewallInterfacesRequestHandler,
|
||||||
|
firewallMemoryRequestHandler,
|
||||||
|
firewallVersionRequestHandler,
|
||||||
|
} from "@homarr/request-handler/firewall";
|
||||||
|
import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
|
||||||
|
|
||||||
|
import { createCronJob } from "../../lib";
|
||||||
|
|
||||||
|
export const firewallCpuJob = createCronJob("firewallCpu", EVERY_5_SECONDS).withCallback(
|
||||||
|
createRequestIntegrationJobHandler(firewallCpuRequestHandler.handler, {
|
||||||
|
widgetKinds: ["firewall"],
|
||||||
|
getInput: {
|
||||||
|
firewall: () => ({}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const firewallMemoryJob = createCronJob("firewallMemory", EVERY_MINUTE).withCallback(
|
||||||
|
createRequestIntegrationJobHandler(firewallMemoryRequestHandler.handler, {
|
||||||
|
widgetKinds: ["firewall"],
|
||||||
|
getInput: {
|
||||||
|
firewall: () => ({}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const firewallInterfacesJob = createCronJob("firewallInterfaces", EVERY_30_SECONDS).withCallback(
|
||||||
|
createRequestIntegrationJobHandler(firewallInterfacesRequestHandler.handler, {
|
||||||
|
widgetKinds: ["firewall"],
|
||||||
|
getInput: {
|
||||||
|
firewall: () => ({}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const firewallVersionJob = createCronJob("firewallVersion", EVERY_HOUR).withCallback(
|
||||||
|
createRequestIntegrationJobHandler(firewallVersionRequestHandler.handler, {
|
||||||
|
widgetKinds: ["firewall"],
|
||||||
|
getInput: {
|
||||||
|
firewall: () => ({}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -172,6 +172,12 @@ export const integrationDefs = {
|
|||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png",
|
||||||
category: ["networkController"],
|
category: ["networkController"],
|
||||||
},
|
},
|
||||||
|
opnsense: {
|
||||||
|
name: "OPNsense",
|
||||||
|
secretKinds: [["username", "password"]],
|
||||||
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/opnsense.svg",
|
||||||
|
category: ["firewall"],
|
||||||
|
},
|
||||||
github: {
|
github: {
|
||||||
name: "Github",
|
name: "Github",
|
||||||
secretKinds: [[], ["personalAccessToken"]],
|
secretKinds: [[], ["personalAccessToken"]],
|
||||||
@@ -318,6 +324,7 @@ export const integrationCategories = [
|
|||||||
"networkController",
|
"networkController",
|
||||||
"releasesProvider",
|
"releasesProvider",
|
||||||
"notifications",
|
"notifications",
|
||||||
|
"firewall",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type IntegrationCategory = (typeof integrationCategories)[number];
|
export type IntegrationCategory = (typeof integrationCategories)[number];
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const widgetKinds = [
|
|||||||
"releases",
|
"releases",
|
||||||
"mediaReleases",
|
"mediaReleases",
|
||||||
"dockerContainers",
|
"dockerContainers",
|
||||||
|
"firewall",
|
||||||
"notifications",
|
"notifications",
|
||||||
] as const;
|
] as const;
|
||||||
export type WidgetKind = (typeof widgetKinds)[number];
|
export type WidgetKind = (typeof widgetKinds)[number];
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { NextcloudIntegration } from "../nextcloud/nextcloud.integration";
|
|||||||
import { NPMIntegration } from "../npm/npm-integration";
|
import { NPMIntegration } from "../npm/npm-integration";
|
||||||
import { NTFYIntegration } from "../ntfy/ntfy-integration";
|
import { NTFYIntegration } from "../ntfy/ntfy-integration";
|
||||||
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
|
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
|
||||||
|
import { OPNsenseIntegration } from "../opnsense/opnsense-integration";
|
||||||
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
|
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
|
||||||
import { createPiHoleIntegrationAsync } from "../pi-hole/pi-hole-integration-factory";
|
import { createPiHoleIntegrationAsync } from "../pi-hole/pi-hole-integration-factory";
|
||||||
import { PlexIntegration } from "../plex/plex-integration";
|
import { PlexIntegration } from "../plex/plex-integration";
|
||||||
@@ -102,6 +103,7 @@ export const integrationCreators = {
|
|||||||
emby: EmbyIntegration,
|
emby: EmbyIntegration,
|
||||||
nextcloud: NextcloudIntegration,
|
nextcloud: NextcloudIntegration,
|
||||||
unifiController: UnifiControllerIntegration,
|
unifiController: UnifiControllerIntegration,
|
||||||
|
opnsense: OPNsenseIntegration,
|
||||||
github: GithubIntegration,
|
github: GithubIntegration,
|
||||||
dockerHub: DockerHubIntegration,
|
dockerHub: DockerHubIntegration,
|
||||||
gitlab: GitlabIntegration,
|
gitlab: GitlabIntegration,
|
||||||
|
|||||||
@@ -21,13 +21,20 @@ export { PiHoleIntegrationV5 } from "./pi-hole/v5/pi-hole-integration-v5";
|
|||||||
export { PiHoleIntegrationV6 } from "./pi-hole/v6/pi-hole-integration-v6";
|
export { PiHoleIntegrationV6 } from "./pi-hole/v6/pi-hole-integration-v6";
|
||||||
export { PlexIntegration } from "./plex/plex-integration";
|
export { PlexIntegration } from "./plex/plex-integration";
|
||||||
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
|
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
|
||||||
|
export { OPNsenseIntegration } from "./opnsense/opnsense-integration";
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
export type { IntegrationInput } from "./base/integration";
|
export type { IntegrationInput } from "./base/integration";
|
||||||
export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data";
|
export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data";
|
||||||
export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items";
|
export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items";
|
||||||
export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status";
|
export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status";
|
||||||
|
export type {
|
||||||
|
FirewallInterface,
|
||||||
|
FirewallCpuSummary,
|
||||||
|
FirewallInterfacesSummary,
|
||||||
|
FirewallVersionSummary,
|
||||||
|
FirewallMemorySummary,
|
||||||
|
} from "./interfaces/firewall-summary/firewall-summary-types";
|
||||||
export type { SystemHealthMonitoring } from "./interfaces/health-monitoring/health-monitoring-types";
|
export type { SystemHealthMonitoring } from "./interfaces/health-monitoring/health-monitoring-types";
|
||||||
export { MediaRequestStatus } from "./interfaces/media-requests/media-request-types";
|
export { MediaRequestStatus } from "./interfaces/media-requests/media-request-types";
|
||||||
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request-types";
|
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request-types";
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type {
|
||||||
|
FirewallCpuSummary,
|
||||||
|
FirewallInterfacesSummary,
|
||||||
|
FirewallMemorySummary,
|
||||||
|
FirewallVersionSummary,
|
||||||
|
} from "./firewall-summary-types";
|
||||||
|
|
||||||
|
export interface FirewallSummaryIntegration {
|
||||||
|
getFirewallCpuAsync(): Promise<FirewallCpuSummary>;
|
||||||
|
getFirewallMemoryAsync(): Promise<FirewallMemorySummary>;
|
||||||
|
getFirewallInterfacesAsync(): Promise<FirewallInterfacesSummary[]>;
|
||||||
|
getFirewallVersionAsync(): Promise<FirewallVersionSummary>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export interface FirewallInterfacesSummary {
|
||||||
|
data: FirewallInterface[];
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FirewallInterface {
|
||||||
|
name: string;
|
||||||
|
receive: number;
|
||||||
|
transmit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FirewallVersionSummary {
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FirewallCpuSummary {
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FirewallMemorySummary {
|
||||||
|
used: number;
|
||||||
|
total: number;
|
||||||
|
percent: number;
|
||||||
|
}
|
||||||
189
packages/integrations/src/opnsense/opnsense-integration.ts
Normal file
189
packages/integrations/src/opnsense/opnsense-integration.ts
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
|
import { ParseError, ResponseError } from "@homarr/common/server";
|
||||||
|
import { createChannelEventHistory } from "@homarr/redis";
|
||||||
|
|
||||||
|
import { HandleIntegrationErrors } from "../base/errors/decorator";
|
||||||
|
import type { IntegrationTestingInput } from "../base/integration";
|
||||||
|
import { Integration } from "../base/integration";
|
||||||
|
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||||
|
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||||
|
import type { FirewallSummaryIntegration } from "../interfaces/firewall-summary/firewall-summary-integration";
|
||||||
|
import type {
|
||||||
|
FirewallCpuSummary,
|
||||||
|
FirewallInterface,
|
||||||
|
FirewallInterfacesSummary,
|
||||||
|
FirewallMemorySummary,
|
||||||
|
FirewallVersionSummary,
|
||||||
|
} from "../interfaces/firewall-summary/firewall-summary-types";
|
||||||
|
import {
|
||||||
|
opnsenseCPUSchema,
|
||||||
|
opnsenseInterfacesSchema,
|
||||||
|
opnsenseMemorySchema,
|
||||||
|
opnsenseSystemSummarySchema,
|
||||||
|
} from "./opnsense-types";
|
||||||
|
|
||||||
|
@HandleIntegrationErrors([])
|
||||||
|
export class OPNsenseIntegration extends Integration implements FirewallSummaryIntegration {
|
||||||
|
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||||
|
const response = await input.fetchAsync(this.url("/api/diagnostics/system/system_information"), {
|
||||||
|
headers: {
|
||||||
|
Authorization: this.getAuthHeaders(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) return TestConnectionError.StatusResult(response);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (typeof result === "object" && result !== null) return { success: true };
|
||||||
|
|
||||||
|
return TestConnectionError.ParseResult(new ParseError("Expected object data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAuthHeaders() {
|
||||||
|
const username = super.getSecretValue("username");
|
||||||
|
const password = super.getSecretValue("password");
|
||||||
|
return `Basic ${btoa(`${username}:${password}`)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getFirewallVersionAsync(): Promise<FirewallVersionSummary> {
|
||||||
|
const responseVersion = await fetchWithTrustedCertificatesAsync(
|
||||||
|
this.url("/api/diagnostics/system/system_information"),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: this.getAuthHeaders(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!responseVersion.ok) {
|
||||||
|
throw new ResponseError(responseVersion);
|
||||||
|
}
|
||||||
|
const summary = opnsenseSystemSummarySchema.parse(await responseVersion.json());
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: summary.versions.at(0) ?? "Unknown",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getInterfacesChannel() {
|
||||||
|
return createChannelEventHistory<FirewallInterface[]>(`integration:${this.integration.id}:interfaces`, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getFirewallInterfacesAsync(): Promise<FirewallInterfacesSummary[]> {
|
||||||
|
const channel = this.getInterfacesChannel();
|
||||||
|
|
||||||
|
const responseInterfaces = await fetchWithTrustedCertificatesAsync(this.url("/api/diagnostics/traffic/interface"), {
|
||||||
|
headers: {
|
||||||
|
Authorization: this.getAuthHeaders(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!responseInterfaces.ok) {
|
||||||
|
throw new ResponseError(responseInterfaces);
|
||||||
|
}
|
||||||
|
const interfaces = opnsenseInterfacesSchema.parse(await responseInterfaces.json());
|
||||||
|
|
||||||
|
const returnValue: FirewallInterface[] = [];
|
||||||
|
const interfaceKeys = Object.keys(interfaces.interfaces);
|
||||||
|
|
||||||
|
for (const key of interfaceKeys) {
|
||||||
|
const inter = interfaces.interfaces[key];
|
||||||
|
if (!inter) continue;
|
||||||
|
|
||||||
|
const bytesTransmitted = inter["bytes transmitted"];
|
||||||
|
const bytesReceived = inter["bytes received"];
|
||||||
|
const receiveValue = parseInt(bytesReceived, 10);
|
||||||
|
const transmitValue = parseInt(bytesTransmitted, 10);
|
||||||
|
|
||||||
|
returnValue.push({
|
||||||
|
name: inter.name,
|
||||||
|
receive: receiveValue,
|
||||||
|
transmit: transmitValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await channel.pushAsync(returnValue);
|
||||||
|
|
||||||
|
return await channel.getSliceAsync(0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getFirewallMemoryAsync(): Promise<FirewallMemorySummary> {
|
||||||
|
const responseMemory = await fetchWithTrustedCertificatesAsync(
|
||||||
|
this.url("/api/diagnostics/system/systemResources"),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: this.getAuthHeaders(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!responseMemory.ok) {
|
||||||
|
throw new ResponseError(responseMemory);
|
||||||
|
}
|
||||||
|
|
||||||
|
const memory = opnsenseMemorySchema.parse(await responseMemory.json());
|
||||||
|
|
||||||
|
// Using parseInt for memoryTotal is normal, the api sends the total memory as a string
|
||||||
|
const memoryTotal = parseInt(memory.memory.total);
|
||||||
|
const memoryUsed = memory.memory.used;
|
||||||
|
const memoryPercent = (100 * memoryUsed) / memoryTotal;
|
||||||
|
return {
|
||||||
|
total: memoryTotal,
|
||||||
|
used: memoryUsed,
|
||||||
|
percent: memoryPercent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getFirewallCpuAsync(): Promise<FirewallCpuSummary> {
|
||||||
|
const responseCpu = await fetchWithTrustedCertificatesAsync(this.url("/api/diagnostics/cpu_usage/stream"), {
|
||||||
|
headers: {
|
||||||
|
Authorization: this.getAuthHeaders(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!responseCpu.ok) {
|
||||||
|
throw new ResponseError(responseCpu);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!responseCpu.body) {
|
||||||
|
throw new Error("ReadableStream not supported in this environment.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = responseCpu.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let loopCounter = 0;
|
||||||
|
try {
|
||||||
|
while (loopCounter < 10) {
|
||||||
|
loopCounter++;
|
||||||
|
const result = await reader.read();
|
||||||
|
if (result.done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!(result.value instanceof Uint8Array)) {
|
||||||
|
throw new Error("Received value is not an Uint8Array.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: AllowSharedBufferSource = result.value;
|
||||||
|
|
||||||
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
|
const lines = chunk.split("\n");
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.startsWith("data:")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (loopCounter < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const data = line.substring(5).trim();
|
||||||
|
const cpuValues = opnsenseCPUSchema.parse(JSON.parse(data));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...cpuValues,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("No valid CPU data found.");
|
||||||
|
} finally {
|
||||||
|
await reader.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
packages/integrations/src/opnsense/opnsense-types.ts
Normal file
30
packages/integrations/src/opnsense/opnsense-types.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// API documentation : https://docs.opnsense.org/development/api.html#core-api
|
||||||
|
|
||||||
|
export const opnsenseSystemSummarySchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
versions: z.array(z.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const opnsenseMemorySchema = z.object({
|
||||||
|
memory: z.object({
|
||||||
|
total: z.string(),
|
||||||
|
used: z.number(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const interfaceSchema = z.object({
|
||||||
|
"bytes received": z.string(),
|
||||||
|
"bytes transmitted": z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const opnsenseInterfacesSchema = z.object({
|
||||||
|
interfaces: z.record(interfaceSchema),
|
||||||
|
time: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const opnsenseCPUSchema = z.object({
|
||||||
|
total: z.number(),
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from "./interfaces/calendar/calendar-types";
|
export * from "./interfaces/calendar/calendar-types";
|
||||||
export * from "./interfaces/dns-hole-summary/dns-hole-summary-types";
|
export * from "./interfaces/dns-hole-summary/dns-hole-summary-types";
|
||||||
export * from "./interfaces/network-controller-summary/network-controller-summary-types";
|
export * from "./interfaces/network-controller-summary/network-controller-summary-types";
|
||||||
|
export * from "./interfaces/firewall-summary/firewall-summary-types";
|
||||||
export * from "./interfaces/health-monitoring/health-monitoring-types";
|
export * from "./interfaces/health-monitoring/health-monitoring-types";
|
||||||
export * from "./interfaces/indexer-manager/indexer-manager-types";
|
export * from "./interfaces/indexer-manager/indexer-manager-types";
|
||||||
export * from "./interfaces/media-requests/media-request-types";
|
export * from "./interfaces/media-requests/media-request-types";
|
||||||
@@ -8,4 +9,5 @@ export * from "./base/searchable-integration";
|
|||||||
export * from "./homeassistant/homeassistant-types";
|
export * from "./homeassistant/homeassistant-types";
|
||||||
export * from "./proxmox/proxmox-types";
|
export * from "./proxmox/proxmox-types";
|
||||||
export * from "./unifi-controller/unifi-controller-types";
|
export * from "./unifi-controller/unifi-controller-types";
|
||||||
|
export * from "./opnsense/opnsense-types";
|
||||||
export * from "./interfaces/media-releases";
|
export * from "./interfaces/media-releases";
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export {
|
|||||||
createIntegrationOptionsChannel,
|
createIntegrationOptionsChannel,
|
||||||
createWidgetOptionsChannel,
|
createWidgetOptionsChannel,
|
||||||
createChannelWithLatestAndEvents,
|
createChannelWithLatestAndEvents,
|
||||||
|
createChannelEventHistory,
|
||||||
handshakeAsync,
|
handshakeAsync,
|
||||||
createSubPubChannel,
|
createSubPubChannel,
|
||||||
createGetSetChannel,
|
createGetSetChannel,
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ export const createChannelEventHistory = <TData>(channelName: string, maxElement
|
|||||||
if (length <= maxElements) {
|
if (length <= maxElements) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await getSetClient.ltrim(channelName, length - maxElements, length);
|
await getSetClient.ltrim(channelName, 0, maxElements - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
64
packages/request-handler/src/firewall.ts
Normal file
64
packages/request-handler/src/firewall.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||||
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
|
import type {
|
||||||
|
FirewallCpuSummary,
|
||||||
|
FirewallInterfacesSummary,
|
||||||
|
FirewallMemorySummary,
|
||||||
|
FirewallVersionSummary,
|
||||||
|
} from "@homarr/integrations/types";
|
||||||
|
|
||||||
|
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
||||||
|
|
||||||
|
export const firewallCpuRequestHandler = createCachedIntegrationRequestHandler<
|
||||||
|
FirewallCpuSummary,
|
||||||
|
IntegrationKindByCategory<"firewall">,
|
||||||
|
Record<string, never>
|
||||||
|
>({
|
||||||
|
async requestAsync(integration, _input) {
|
||||||
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
|
return integrationInstance.getFirewallCpuAsync();
|
||||||
|
},
|
||||||
|
cacheDuration: dayjs.duration(5, "seconds"),
|
||||||
|
queryKey: "firewallCpuSummary",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const firewallMemoryRequestHandler = createCachedIntegrationRequestHandler<
|
||||||
|
FirewallMemorySummary,
|
||||||
|
IntegrationKindByCategory<"firewall">,
|
||||||
|
Record<string, never>
|
||||||
|
>({
|
||||||
|
async requestAsync(integration, _input) {
|
||||||
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
|
return await integrationInstance.getFirewallMemoryAsync();
|
||||||
|
},
|
||||||
|
cacheDuration: dayjs.duration(15, "seconds"),
|
||||||
|
queryKey: "firewallMemorySummary",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const firewallInterfacesRequestHandler = createCachedIntegrationRequestHandler<
|
||||||
|
FirewallInterfacesSummary[],
|
||||||
|
IntegrationKindByCategory<"firewall">,
|
||||||
|
Record<string, never>
|
||||||
|
>({
|
||||||
|
async requestAsync(integration, _input) {
|
||||||
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
|
return await integrationInstance.getFirewallInterfacesAsync();
|
||||||
|
},
|
||||||
|
cacheDuration: dayjs.duration(30, "seconds"),
|
||||||
|
queryKey: "firewallInterfacesSummary",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const firewallVersionRequestHandler = createCachedIntegrationRequestHandler<
|
||||||
|
FirewallVersionSummary,
|
||||||
|
IntegrationKindByCategory<"firewall">,
|
||||||
|
Record<string, never>
|
||||||
|
>({
|
||||||
|
async requestAsync(integration, _input) {
|
||||||
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
|
return await integrationInstance.getFirewallVersionAsync();
|
||||||
|
},
|
||||||
|
cacheDuration: dayjs.duration(1, "hour"),
|
||||||
|
queryKey: "firewallVersionSummary",
|
||||||
|
});
|
||||||
@@ -2413,6 +2413,35 @@
|
|||||||
"internalServerError": "Failed to fetch Network Controller Summary"
|
"internalServerError": "Failed to fetch Network Controller Summary"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"firewall": {
|
||||||
|
"name": "Firewall Monitoring",
|
||||||
|
"description": "Displays a summary of firewalls",
|
||||||
|
"tab": {
|
||||||
|
"system": "System",
|
||||||
|
"interfaces": "Interfaces"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"internalServerError": "Unable to get data from firewall"
|
||||||
|
},
|
||||||
|
"option": {
|
||||||
|
"interfaces": "Network interfaces to display"
|
||||||
|
},
|
||||||
|
"widget": {
|
||||||
|
"fwname": "Name",
|
||||||
|
"version": "Version",
|
||||||
|
"versiontitle": "Versions",
|
||||||
|
"cputitle": "CPU usage",
|
||||||
|
"memorytitle": "Memory usage",
|
||||||
|
"cpu": "CPU",
|
||||||
|
"memory": "Memory",
|
||||||
|
"interfaces": {
|
||||||
|
"name": "name",
|
||||||
|
"trans": "Transmited",
|
||||||
|
"recv": "Received",
|
||||||
|
"title": "Network Interfaces"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"name": "Notifications",
|
"name": "Notifications",
|
||||||
"description": "Display notification history from an integration",
|
"description": "Display notification history from an integration",
|
||||||
@@ -3192,6 +3221,18 @@
|
|||||||
},
|
},
|
||||||
"dockerContainers": {
|
"dockerContainers": {
|
||||||
"label": "Docker containers"
|
"label": "Docker containers"
|
||||||
|
},
|
||||||
|
"firewallCpu": {
|
||||||
|
"label": "Firewall CPU"
|
||||||
|
},
|
||||||
|
"firewallMemory": {
|
||||||
|
"label": "Firewall Memory"
|
||||||
|
},
|
||||||
|
"firewallVersion": {
|
||||||
|
"label": "Firewall Version"
|
||||||
|
},
|
||||||
|
"firewallInterfaces": {
|
||||||
|
"label": "Firewall Interfaces"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"interval": {
|
"interval": {
|
||||||
|
|||||||
397
packages/widgets/src/firewall/component.tsx
Normal file
397
packages/widgets/src/firewall/component.tsx
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { Accordion, Box, Center, Flex, Group, RingProgress, ScrollArea, Text } from "@mantine/core";
|
||||||
|
import { useLocalStorage } from "@mantine/hooks";
|
||||||
|
import { IconArrowBarDown, IconArrowBarUp, IconBrain, IconCpu, IconTopologyBus } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
import type { FirewallInterface, FirewallInterfacesSummary } from "@homarr/integrations";
|
||||||
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
|
import type { WidgetComponentProps } from "../definition";
|
||||||
|
import { FirewallMenu } from "./firewall-menu";
|
||||||
|
import { FirewallVersion } from "./firewall-version";
|
||||||
|
|
||||||
|
export interface Firewall {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FirewallWidget({ integrationIds, width, itemId }: WidgetComponentProps<"firewall">) {
|
||||||
|
const [selectedFirewall, setSelectedFirewall] = useState<string>("");
|
||||||
|
|
||||||
|
const handleSelect = useCallback((value: string | null) => {
|
||||||
|
if (value !== null) {
|
||||||
|
setSelectedFirewall(value);
|
||||||
|
} else {
|
||||||
|
setSelectedFirewall("default_value");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const firewallsCpuData = useUpdatingCpuStatus(integrationIds);
|
||||||
|
const firewallsMemoryData = useUpdatingMemoryStatus(integrationIds);
|
||||||
|
const firewallsVersionData = useUpdatingVersionStatus(integrationIds);
|
||||||
|
const firewallsInterfacesData = useUpdatingInterfacesStatus(integrationIds);
|
||||||
|
|
||||||
|
const initialSelectedFirewall = firewallsVersionData[0] ? firewallsVersionData[0].integration.id : "undefined";
|
||||||
|
const isTiny = width < 256;
|
||||||
|
|
||||||
|
const [accordionValue, setAccordionValue] = useLocalStorage<string | null>({
|
||||||
|
key: `homarr-${itemId}-firewall`,
|
||||||
|
defaultValue: "interfaces",
|
||||||
|
});
|
||||||
|
|
||||||
|
const dropdownItems = firewallsVersionData.map((firewall) => ({
|
||||||
|
label: firewall.integration.name,
|
||||||
|
value: firewall.integration.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollArea h="100%">
|
||||||
|
<Group justify="space-between" w="100%" style={{ padding: "8px" }}>
|
||||||
|
<FirewallMenu
|
||||||
|
onChange={handleSelect}
|
||||||
|
selectedFirewall={selectedFirewall || initialSelectedFirewall}
|
||||||
|
dropdownItems={dropdownItems}
|
||||||
|
isTiny={isTiny}
|
||||||
|
/>
|
||||||
|
<FirewallVersion
|
||||||
|
firewallsVersionData={firewallsVersionData}
|
||||||
|
selectedFirewall={selectedFirewall || initialSelectedFirewall}
|
||||||
|
isTiny={isTiny}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Flex justify="center" align="center" wrap="wrap">
|
||||||
|
{/* Render CPU and Memory data */}
|
||||||
|
{firewallsCpuData
|
||||||
|
.filter(({ integration }) => integration.id === (selectedFirewall || initialSelectedFirewall))
|
||||||
|
.map(({ summary, integration }) => (
|
||||||
|
<RingProgress
|
||||||
|
key={`${integration.name}-cpu`}
|
||||||
|
roundCaps
|
||||||
|
size={isTiny ? 50 : 100}
|
||||||
|
thickness={isTiny ? 4 : 8}
|
||||||
|
label={
|
||||||
|
<Center style={{ flexDirection: "column" }}>
|
||||||
|
<Text size={isTiny ? "8px" : "xs"}>{`${summary.total.toFixed(2)}%`}</Text>
|
||||||
|
<IconCpu size={isTiny ? 8 : 16} />
|
||||||
|
</Center>
|
||||||
|
}
|
||||||
|
sections={[
|
||||||
|
{
|
||||||
|
value: Number(summary.total.toFixed(1)),
|
||||||
|
color: summary.total > 50 ? (summary.total < 75 ? "yellow" : "red") : "green",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{firewallsMemoryData
|
||||||
|
.filter(({ integration }) => integration.id === (selectedFirewall || initialSelectedFirewall))
|
||||||
|
.map(({ summary, integration }) => (
|
||||||
|
<RingProgress
|
||||||
|
key={`${integration.name}-memory`}
|
||||||
|
roundCaps
|
||||||
|
size={isTiny ? 50 : 100}
|
||||||
|
thickness={isTiny ? 4 : 8}
|
||||||
|
label={
|
||||||
|
<Center style={{ flexDirection: "column" }}>
|
||||||
|
<Text size={isTiny ? "8px" : "xs"}>{`${summary.percent.toFixed(1)}%`}</Text>
|
||||||
|
<IconBrain size={isTiny ? 8 : 16} />
|
||||||
|
</Center>
|
||||||
|
}
|
||||||
|
sections={[
|
||||||
|
{
|
||||||
|
value: Number(summary.percent.toFixed(1)),
|
||||||
|
color: summary.percent > 50 ? (summary.percent < 75 ? "yellow" : "red") : "green",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
{firewallsInterfacesData
|
||||||
|
.filter(({ integration }) => integration.id === (selectedFirewall || initialSelectedFirewall))
|
||||||
|
.map(({ summary }) => (
|
||||||
|
<Accordion key="interfaces" value={accordionValue} onChange={setAccordionValue}>
|
||||||
|
<Accordion.Item value="interfaces">
|
||||||
|
<Accordion.Control icon={isTiny ? null : <IconTopologyBus size={16} />}>
|
||||||
|
<Text size={isTiny ? "8px" : "xs"}> {t("widget.firewall.widget.interfaces.title")} </Text>
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Flex direction="column" key="interfaces">
|
||||||
|
{Array.isArray(summary) && summary.every((item) => Array.isArray(item.data)) ? (
|
||||||
|
calculateBandwidth(summary).data.map(({ name, receive, transmit }) => (
|
||||||
|
<Flex
|
||||||
|
key={name}
|
||||||
|
direction={isTiny ? "column" : "row"}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
padding: isTiny ? "2px" : "0px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex w={isTiny ? "100%" : "33%"} style={{ justifyContent: "flex-start" }}>
|
||||||
|
<Text
|
||||||
|
size={isTiny ? "8px" : "xs"}
|
||||||
|
color="lightblue"
|
||||||
|
style={{
|
||||||
|
maxWidth: "100px",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
textAlign: "left",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
gap="4"
|
||||||
|
w={isTiny ? "100%" : "33%"}
|
||||||
|
style={{ justifyContent: "flex-start" }}
|
||||||
|
>
|
||||||
|
<IconArrowBarUp size={isTiny ? "8" : "12"} color="lightgreen" />
|
||||||
|
<Text size={isTiny ? "8px" : "xs"} color="lightgreen" style={{ textAlign: "left" }}>
|
||||||
|
{formatBitsPerSec(transmit, 2)}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
gap="4"
|
||||||
|
w={isTiny ? "100%" : "33%"}
|
||||||
|
style={{ justifyContent: "flex-start" }}
|
||||||
|
>
|
||||||
|
<IconArrowBarDown size={isTiny ? "8" : "12"} color="yellow" />
|
||||||
|
<Text size={isTiny ? "8px" : "xs"} color="yellow" style={{ textAlign: "left" }}>
|
||||||
|
{formatBitsPerSec(receive, 2)}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Box>No data available</Box>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
))}
|
||||||
|
</ScrollArea>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdatingCpuStatus = (integrationIds: string[]) => {
|
||||||
|
const utils = clientApi.useUtils();
|
||||||
|
const [firewallsCpuData] = clientApi.widget.firewall.getFirewallCpuStatus.useSuspenseQuery(
|
||||||
|
{
|
||||||
|
integrationIds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
retry: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
clientApi.widget.firewall.subscribeFirewallCpuStatus.useSubscription(
|
||||||
|
{
|
||||||
|
integrationIds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onData: (data) => {
|
||||||
|
utils.widget.firewall.getFirewallCpuStatus.setData(
|
||||||
|
{
|
||||||
|
integrationIds,
|
||||||
|
},
|
||||||
|
(prevData) => {
|
||||||
|
if (!prevData) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prevData.map((item) =>
|
||||||
|
item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return firewallsCpuData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdatingMemoryStatus = (integrationIds: string[]) => {
|
||||||
|
const utils = clientApi.useUtils();
|
||||||
|
const [firewallsMemoryData] = clientApi.widget.firewall.getFirewallMemoryStatus.useSuspenseQuery(
|
||||||
|
{
|
||||||
|
integrationIds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
retry: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
clientApi.widget.firewall.subscribeFirewallMemoryStatus.useSubscription(
|
||||||
|
{
|
||||||
|
integrationIds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onData: (data) => {
|
||||||
|
utils.widget.firewall.getFirewallMemoryStatus.setData(
|
||||||
|
{
|
||||||
|
integrationIds,
|
||||||
|
},
|
||||||
|
(prevData) => {
|
||||||
|
if (!prevData) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prevData.map((item) =>
|
||||||
|
item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return firewallsMemoryData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdatingVersionStatus = (integrationIds: string[]) => {
|
||||||
|
const utils = clientApi.useUtils();
|
||||||
|
const [firewallsVersionData] = clientApi.widget.firewall.getFirewallVersionStatus.useSuspenseQuery(
|
||||||
|
{
|
||||||
|
integrationIds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
retry: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
clientApi.widget.firewall.subscribeFirewallVersionStatus.useSubscription(
|
||||||
|
{
|
||||||
|
integrationIds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onData: (data) => {
|
||||||
|
utils.widget.firewall.getFirewallVersionStatus.setData(
|
||||||
|
{
|
||||||
|
integrationIds,
|
||||||
|
},
|
||||||
|
(prevData) => {
|
||||||
|
if (!prevData) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prevData.map((item) =>
|
||||||
|
item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return firewallsVersionData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdatingInterfacesStatus = (integrationIds: string[]) => {
|
||||||
|
const utils = clientApi.useUtils();
|
||||||
|
const [firewallsInterfacesData] = clientApi.widget.firewall.getFirewallInterfacesStatus.useSuspenseQuery(
|
||||||
|
{
|
||||||
|
integrationIds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
retry: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
clientApi.widget.firewall.subscribeFirewallInterfacesStatus.useSubscription(
|
||||||
|
{
|
||||||
|
integrationIds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onData: (data) => {
|
||||||
|
utils.widget.firewall.getFirewallInterfacesStatus.setData(
|
||||||
|
{
|
||||||
|
integrationIds,
|
||||||
|
},
|
||||||
|
(prevData) => {
|
||||||
|
if (!prevData) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return prevData.map((item) =>
|
||||||
|
item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return firewallsInterfacesData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatBitsPerSec(bytes: number, decimals: number): string {
|
||||||
|
if (bytes === 0) return "0 b/s";
|
||||||
|
|
||||||
|
const kilobyte = 1024;
|
||||||
|
const sizes = ["b/s", "kb/s", "Mb/s", "Gb/s", "Tb/s", "Pb/s", "Eb/s", "Zb/s", "Yb/s"];
|
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(kilobyte));
|
||||||
|
|
||||||
|
return `${parseFloat((bytes / Math.pow(kilobyte, i)).toFixed(decimals))} ${sizes[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateBandwidth(data: FirewallInterfacesSummary[]): { data: FirewallInterface[] } {
|
||||||
|
const result = {
|
||||||
|
data: [] as FirewallInterface[],
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.length > 1) {
|
||||||
|
const firstData = data[0];
|
||||||
|
const secondData = data[1];
|
||||||
|
|
||||||
|
if (firstData && secondData) {
|
||||||
|
const time1 = new Date(firstData.timestamp);
|
||||||
|
const time2 = new Date(secondData.timestamp);
|
||||||
|
const timeDiffInSeconds = (time1.getTime() - time2.getTime()) / 1000;
|
||||||
|
|
||||||
|
firstData.data.forEach((iface) => {
|
||||||
|
const ifaceName = iface.name;
|
||||||
|
const recv1 = iface.receive;
|
||||||
|
const trans1 = iface.transmit;
|
||||||
|
|
||||||
|
const iface2 = secondData.data.find((i) => i.name === ifaceName);
|
||||||
|
|
||||||
|
if (iface2) {
|
||||||
|
const recv2 = iface2.receive;
|
||||||
|
const trans2 = iface2.transmit;
|
||||||
|
const recvDiff = recv1 - recv2;
|
||||||
|
const transDiff = trans1 - trans2;
|
||||||
|
|
||||||
|
result.data.push({
|
||||||
|
name: ifaceName,
|
||||||
|
receive: (8 * recvDiff) / timeDiffInSeconds,
|
||||||
|
transmit: (8 * transDiff) / timeDiffInSeconds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
27
packages/widgets/src/firewall/firewall-menu.tsx
Normal file
27
packages/widgets/src/firewall/firewall-menu.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Box, Select } from "@mantine/core";
|
||||||
|
|
||||||
|
import type { Firewall } from "./component";
|
||||||
|
|
||||||
|
interface FirewallMenuProps {
|
||||||
|
onChange: (value: string | null) => void;
|
||||||
|
dropdownItems: Firewall[];
|
||||||
|
selectedFirewall: string;
|
||||||
|
isTiny: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FirewallMenu = ({ onChange, isTiny, dropdownItems, selectedFirewall }: FirewallMenuProps) => (
|
||||||
|
<Box>
|
||||||
|
<Select
|
||||||
|
value={selectedFirewall}
|
||||||
|
onChange={onChange}
|
||||||
|
size={isTiny ? "8px" : "xs"}
|
||||||
|
color="lightgray"
|
||||||
|
data={dropdownItems}
|
||||||
|
styles={{
|
||||||
|
input: {
|
||||||
|
minHeight: "24px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
41
packages/widgets/src/firewall/firewall-version.tsx
Normal file
41
packages/widgets/src/firewall/firewall-version.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Badge, Box } from "@mantine/core";
|
||||||
|
|
||||||
|
import type { FirewallVersionSummary } from "@homarr/integrations";
|
||||||
|
|
||||||
|
interface FirewallVersionProps {
|
||||||
|
firewallsVersionData: {
|
||||||
|
integration: FirewallIntegration;
|
||||||
|
summary: FirewallVersionSummary;
|
||||||
|
}[];
|
||||||
|
selectedFirewall: string;
|
||||||
|
isTiny: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FirewallIntegration {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
kind: string;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FirewallVersion = ({ firewallsVersionData, selectedFirewall, isTiny }: FirewallVersionProps) => (
|
||||||
|
<Box>
|
||||||
|
<Badge autoContrast variant="outline" color="lightgray" size={isTiny ? "8px" : "xs"} style={{ minHeight: "24px" }}>
|
||||||
|
{firewallsVersionData
|
||||||
|
.filter(({ integration }) => integration.id === selectedFirewall)
|
||||||
|
.map(({ summary, integration }) => (
|
||||||
|
<span key={integration.id}>{formatVersion(summary.version)}</span>
|
||||||
|
))}
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
function formatVersion(inputString: string): string {
|
||||||
|
const regex = /([\d._]+)/;
|
||||||
|
const match = regex.exec(inputString);
|
||||||
|
if (match?.[1]) {
|
||||||
|
return match[1];
|
||||||
|
} else {
|
||||||
|
return "Unknown Version";
|
||||||
|
}
|
||||||
|
}
|
||||||
7
packages/widgets/src/firewall/firewall.module.css
Normal file
7
packages/widgets/src/firewall/firewall.module.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[data-mantine-color-scheme="light"] .card {
|
||||||
|
background-color: var(--mantine-color-gray-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-mantine-color-scheme="dark"] .card {
|
||||||
|
background-color: var(--mantine-color-dark-7);
|
||||||
|
}
|
||||||
20
packages/widgets/src/firewall/index.ts
Normal file
20
packages/widgets/src/firewall/index.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { IconWall, IconWallOff } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
|
|
||||||
|
import { createWidgetDefinition } from "../definition";
|
||||||
|
import { optionsBuilder } from "../options";
|
||||||
|
|
||||||
|
export const { definition, componentLoader } = createWidgetDefinition("firewall", {
|
||||||
|
icon: IconWall,
|
||||||
|
createOptions() {
|
||||||
|
return optionsBuilder.from(() => ({}));
|
||||||
|
},
|
||||||
|
supportedIntegrations: getIntegrationKindsByCategory("firewall"),
|
||||||
|
errors: {
|
||||||
|
INTERNAL_SERVER_ERROR: {
|
||||||
|
icon: IconWallOff,
|
||||||
|
message: (t) => t("widget.firewall.error.internalServerError"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).withDynamicImport(() => import("./component"));
|
||||||
@@ -16,6 +16,7 @@ import * as dnsHoleControls from "./dns-hole/controls";
|
|||||||
import * as dnsHoleSummary from "./dns-hole/summary";
|
import * as dnsHoleSummary from "./dns-hole/summary";
|
||||||
import * as dockerContainers from "./docker";
|
import * as dockerContainers from "./docker";
|
||||||
import * as downloads from "./downloads";
|
import * as downloads from "./downloads";
|
||||||
|
import * as firewall from "./firewall";
|
||||||
import * as healthMonitoring from "./health-monitoring";
|
import * as healthMonitoring from "./health-monitoring";
|
||||||
import * as iframe from "./iframe";
|
import * as iframe from "./iframe";
|
||||||
import type { WidgetImportRecord } from "./import";
|
import type { WidgetImportRecord } from "./import";
|
||||||
@@ -69,6 +70,7 @@ export const widgetImports = {
|
|||||||
minecraftServerStatus,
|
minecraftServerStatus,
|
||||||
dockerContainers,
|
dockerContainers,
|
||||||
releases,
|
releases,
|
||||||
|
firewall,
|
||||||
notifications,
|
notifications,
|
||||||
mediaReleases,
|
mediaReleases,
|
||||||
} satisfies WidgetImportRecord;
|
} satisfies WidgetImportRecord;
|
||||||
|
|||||||
Reference in New Issue
Block a user