chore(release): automatic release v0.1.0
This commit is contained in:
10
.github/workflows/deployment-docker-image.yml
vendored
10
.github/workflows/deployment-docker-image.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
|||||||
node-version: [20]
|
node-version: [20]
|
||||||
steps:
|
steps:
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
if: ${{ github.events.inputs.send-notifications || true }}
|
if: ${{ github.events.inputs.send-notifications != false }}
|
||||||
env:
|
env:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
uses: Ilshidur/action-discord@master
|
uses: Ilshidur/action-discord@master
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
branch: dev
|
branch: dev
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
if: ${{ github.events.inputs.send-notifications || true }}
|
if: ${{ github.events.inputs.send-notifications != false }}
|
||||||
env:
|
env:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
uses: Ilshidur/action-discord@master
|
uses: Ilshidur/action-discord@master
|
||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
if: ${{ github.events.inputs.send-notifications || true }}
|
if: ${{ github.events.inputs.send-notifications != false }}
|
||||||
env:
|
env:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
uses: Ilshidur/action-discord@master
|
uses: Ilshidur/action-discord@master
|
||||||
@@ -120,14 +120,14 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SKIP_ENV_VALIDATION: true
|
SKIP_ENV_VALIDATION: true
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
if: ${{ github.events.inputs.send-notifications || true && (github.events.inputs.push-image == 'true' || github.events.inputs.push-image == null) }}
|
if: ${{ github.events.inputs.send-notifications != false && (github.events.inputs.push-image == 'true' || github.events.inputs.push-image == null) }}
|
||||||
env:
|
env:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
uses: Ilshidur/action-discord@master
|
uses: Ilshidur/action-discord@master
|
||||||
with:
|
with:
|
||||||
args: "Deployment of image has completed. Image ID is '${{ steps.buildPushAction.outputs.imageid }}'."
|
args: "Deployment of image has completed. Image ID is '${{ steps.buildPushAction.outputs.imageid }}'."
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
if: ${{ github.events.inputs.send-notifications || true && !(github.events.inputs.push-image == 'true' || github.events.inputs.push-image == null) }}
|
if: ${{ github.events.inputs.send-notifications != false && !(github.events.inputs.push-image == 'true' || github.events.inputs.push-image == null) }}
|
||||||
env:
|
env:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
uses: Ilshidur/action-discord@master
|
uses: Ilshidur/action-discord@master
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ jobs:
|
|||||||
- id: automerge
|
- id: automerge
|
||||||
if: ${{ steps.semver.outputs.bump != 'major' }}
|
if: ${{ steps.semver.outputs.bump != 'major' }}
|
||||||
name: automerge
|
name: automerge
|
||||||
uses: "pascalgn/automerge-action@v0.16.3"
|
uses: "pascalgn/automerge-action@v0.16.4"
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ steps.obtainMergeToken.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.obtainMergeToken.outputs.token }}
|
||||||
MERGE_METHOD: merge # we prefer merge commits for merging to master
|
MERGE_METHOD: merge # we prefer merge commits for merging to master
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM --platform=linux/amd64 node:20.17.0-alpine AS base
|
FROM node:20.17.0-alpine AS base
|
||||||
|
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
|
|||||||
@@ -36,16 +36,16 @@
|
|||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@homarr/widgets": "workspace:^0.1.0",
|
"@homarr/widgets": "workspace:^0.1.0",
|
||||||
"@mantine/colors-generator": "^7.12.2",
|
"@mantine/colors-generator": "^7.13.0",
|
||||||
"@mantine/core": "^7.12.2",
|
"@mantine/core": "^7.13.0",
|
||||||
"@mantine/hooks": "^7.12.2",
|
"@mantine/hooks": "^7.13.0",
|
||||||
"@mantine/modals": "^7.12.2",
|
"@mantine/modals": "^7.13.0",
|
||||||
"@mantine/tiptap": "^7.12.2",
|
"@mantine/tiptap": "^7.13.0",
|
||||||
"@million/lint": "1.0.0-rc.84",
|
"@million/lint": "1.0.0-rc.84",
|
||||||
"@t3-oss/env-nextjs": "^0.11.1",
|
"@t3-oss/env-nextjs": "^0.11.1",
|
||||||
"@tabler/icons-react": "^3.17.0",
|
"@tabler/icons-react": "^3.18.0",
|
||||||
"@tanstack/react-query": "^5.56.2",
|
"@tanstack/react-query": "^5.56.2",
|
||||||
"@tanstack/react-query-devtools": "^5.56.2",
|
"@tanstack/react-query-devtools": "^5.58.0",
|
||||||
"@tanstack/react-query-next-experimental": "5.56.2",
|
"@tanstack/react-query-next-experimental": "5.56.2",
|
||||||
"@trpc/client": "next",
|
"@trpc/client": "next",
|
||||||
"@trpc/next": "next",
|
"@trpc/next": "next",
|
||||||
@@ -79,13 +79,13 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/chroma-js": "2.4.4",
|
"@types/chroma-js": "2.4.4",
|
||||||
"@types/node": "^20.16.5",
|
"@types/node": "^20.16.10",
|
||||||
"@types/prismjs": "^1.26.4",
|
"@types/prismjs": "^1.26.4",
|
||||||
"@types/react": "^18.3.8",
|
"@types/react": "^18.3.10",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/swagger-ui-react": "^4.18.3",
|
"@types/swagger-ui-react": "^4.18.3",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"node-loader": "^2.0.0",
|
"node-loader": "^2.0.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
|
import { useState } from "react";
|
||||||
import type { MantineColorScheme, MantineColorSchemeManager } from "@mantine/core";
|
import type { MantineColorScheme, MantineColorSchemeManager } from "@mantine/core";
|
||||||
import { createTheme, isMantineColorScheme, MantineProvider } from "@mantine/core";
|
import { createTheme, isMantineColorScheme, MantineProvider } from "@mantine/core";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useSession } from "@homarr/auth/client";
|
import { useSession } from "@homarr/auth/client";
|
||||||
|
import { parseCookies, setClientCookie } from "@homarr/common";
|
||||||
|
|
||||||
export const CustomMantineProvider = ({ children }: PropsWithChildren) => {
|
export const CustomMantineProvider = ({ children }: PropsWithChildren) => {
|
||||||
const manager = useColorSchemeManager();
|
const manager = useColorSchemeManager();
|
||||||
@@ -50,7 +52,8 @@ function useColorSchemeManager(): MantineColorSchemeManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return (window.localStorage.getItem(key) as MantineColorScheme | undefined) ?? defaultValue;
|
const cookies = parseCookies(document.cookie);
|
||||||
|
return (cookies[key] as MantineColorScheme | undefined) ?? defaultValue;
|
||||||
} catch {
|
} catch {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
@@ -61,6 +64,7 @@ function useColorSchemeManager(): MantineColorSchemeManager {
|
|||||||
if (session) {
|
if (session) {
|
||||||
mutateColorScheme({ colorScheme: value });
|
mutateColorScheme({ colorScheme: value });
|
||||||
}
|
}
|
||||||
|
setClientCookie(key, value, { expires: dayjs().add(1, "year").toDate() });
|
||||||
window.localStorage.setItem(key, value);
|
window.localStorage.setItem(key, value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[@mantine/core] Local storage color scheme manager was unable to save color scheme.", error);
|
console.warn("[@mantine/core] Local storage color scheme manager was unable to save color scheme.", error);
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import "@homarr/spotlight/styles.css";
|
|||||||
import "@homarr/ui/styles.css";
|
import "@homarr/ui/styles.css";
|
||||||
import "~/styles/scroll-area.scss";
|
import "~/styles/scroll-area.scss";
|
||||||
|
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
import { env } from "@homarr/auth/env.mjs";
|
import { env } from "@homarr/auth/env.mjs";
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import { ModalProvider } from "@homarr/modals";
|
import { ModalProvider } from "@homarr/modals";
|
||||||
@@ -53,7 +55,7 @@ export const viewport: Viewport = {
|
|||||||
|
|
||||||
export default async function Layout(props: { children: React.ReactNode; params: { locale: string } }) {
|
export default async function Layout(props: { children: React.ReactNode; params: { locale: string } }) {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
const colorScheme = session?.user.colorScheme;
|
const colorScheme = cookies().get("homarr-color-scheme")?.value ?? "light";
|
||||||
|
|
||||||
const StackedProvider = composeWrappers([
|
const StackedProvider = composeWrappers([
|
||||||
(innerProps) => {
|
(innerProps) => {
|
||||||
|
|||||||
@@ -44,9 +44,9 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node": "^20.16.5",
|
"@types/node": "^20.16.10",
|
||||||
"dotenv-cli": "^7.4.2",
|
"dotenv-cli": "^7.4.2",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"tsx": "4.13.3",
|
"tsx": "4.13.3",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/ws": "^8.5.12",
|
"@types/ws": "^8.5.12",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"@vitest/coverage-v8": "^2.1.1",
|
"@vitest/coverage-v8": "^2.1.1",
|
||||||
"@vitest/ui": "^2.1.1",
|
"@vitest/ui": "^2.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"jsdom": "^25.0.0",
|
"jsdom": "^25.0.1",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"testcontainers": "^10.13.1",
|
"testcontainers": "^10.13.1",
|
||||||
"turbo": "^2.1.2",
|
"turbo": "^2.1.2",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/dockerode": "^3.3.31",
|
"@types/dockerode": "^3.3.31",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,68 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { observable } from "@trpc/server/observable";
|
||||||
|
|
||||||
|
import type { Modify } from "@homarr/common/types";
|
||||||
|
import type { Integration } from "@homarr/db/schema/sqlite";
|
||||||
|
import type { IntegrationKindByCategory, WidgetKind } from "@homarr/definitions";
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
import { integrationCreator } from "@homarr/integrations";
|
import { integrationCreator } from "@homarr/integrations";
|
||||||
import type { DnsHoleSummary } from "@homarr/integrations/types";
|
import type { DnsHoleSummary } from "@homarr/integrations/types";
|
||||||
import { logger } from "@homarr/log";
|
import { controlsInputSchema } from "@homarr/integrations/types";
|
||||||
import { createCacheChannel } from "@homarr/redis";
|
import { createItemAndIntegrationChannel } from "@homarr/redis";
|
||||||
|
import { z } from "@homarr/validation";
|
||||||
|
|
||||||
import { controlsInputSchema } from "../../../../integrations/src/pi-hole/pi-hole-types";
|
|
||||||
import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration";
|
import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||||
|
|
||||||
export const dnsHoleRouter = createTRPCRouter({
|
export const dnsHoleRouter = createTRPCRouter({
|
||||||
summary: publicProcedure
|
summary: publicProcedure
|
||||||
|
.input(z.object({ widgetKind: z.enum(["dnsHoleSummary", "dnsHoleControls"]) }))
|
||||||
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("dnsHole")))
|
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("dnsHole")))
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ input: { widgetKind }, ctx }) => {
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
ctx.integrations.map(async (integration) => {
|
ctx.integrations.map(async ({ decryptedSecrets: _, ...integration }) => {
|
||||||
const cache = createCacheChannel<DnsHoleSummary>(`dns-hole-summary:${integration.id}`);
|
const channel = createItemAndIntegrationChannel<DnsHoleSummary>(widgetKind, integration.id);
|
||||||
const { data } = await cache.consumeAsync(async () => {
|
const { data: summary, timestamp } = (await channel.getAsync()) ?? { data: null, timestamp: new Date(0) };
|
||||||
const client = integrationCreator(integration);
|
|
||||||
|
|
||||||
return await client.getSummaryAsync().catch((err) => {
|
|
||||||
logger.error("dns-hole router - ", err);
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
|
||||||
message: `Failed to fetch DNS Hole summary for ${integration.name} (${integration.id})`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
integrationId: integration.id,
|
integration,
|
||||||
integrationKind: integration.kind,
|
timestamp,
|
||||||
summary: data,
|
summary,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return results;
|
return results;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
subscribeToSummary: publicProcedure
|
||||||
|
.input(z.object({ widgetKind: z.enum(["dnsHoleSummary", "dnsHoleControls"]) }))
|
||||||
|
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("dnsHole")))
|
||||||
|
.subscription(({ input: { widgetKind }, ctx }) => {
|
||||||
|
return observable<{
|
||||||
|
integration: Modify<Integration, { kind: IntegrationKindByCategory<"dnsHole"> }>;
|
||||||
|
timestamp: Date;
|
||||||
|
summary: DnsHoleSummary;
|
||||||
|
}>((emit) => {
|
||||||
|
const unsubscribes: (() => void)[] = [];
|
||||||
|
for (const integrationWithSecrets of ctx.integrations) {
|
||||||
|
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
|
||||||
|
const channel = createItemAndIntegrationChannel<DnsHoleSummary>(widgetKind as WidgetKind, integration.id);
|
||||||
|
const unsubscribe = channel.subscribe((summary) => {
|
||||||
|
emit.next({
|
||||||
|
integration,
|
||||||
|
timestamp: new Date(),
|
||||||
|
summary,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
unsubscribes.push(unsubscribe);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
unsubscribes.forEach((unsubscribe) => {
|
||||||
|
unsubscribe();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
enable: publicProcedure
|
enable: publicProcedure
|
||||||
.unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole")))
|
.unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole")))
|
||||||
.mutation(async ({ ctx: { integration } }) => {
|
.mutation(async ({ ctx: { integration } }) => {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
|
|
||||||
|
import type { Modify } from "@homarr/common/types";
|
||||||
import type { Integration } from "@homarr/db/schema/sqlite";
|
import type { Integration } from "@homarr/db/schema/sqlite";
|
||||||
|
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
import type { DownloadClientJobsAndStatus } from "@homarr/integrations";
|
import type { DownloadClientJobsAndStatus } from "@homarr/integrations";
|
||||||
import { downloadClientItemSchema, integrationCreator } from "@homarr/integrations";
|
import { downloadClientItemSchema, integrationCreator } from "@homarr/integrations";
|
||||||
@@ -33,7 +35,11 @@ export const downloadsRouter = createTRPCRouter({
|
|||||||
subscribeToJobsAndStatuses: publicProcedure
|
subscribeToJobsAndStatuses: publicProcedure
|
||||||
.unstable_concat(createDownloadClientIntegrationMiddleware("query"))
|
.unstable_concat(createDownloadClientIntegrationMiddleware("query"))
|
||||||
.subscription(({ ctx }) => {
|
.subscription(({ ctx }) => {
|
||||||
return observable<{ integration: Integration; timestamp: Date; data: DownloadClientJobsAndStatus }>((emit) => {
|
return observable<{
|
||||||
|
integration: Modify<Integration, { kind: IntegrationKindByCategory<"downloadClient"> }>;
|
||||||
|
timestamp: Date;
|
||||||
|
data: DownloadClientJobsAndStatus;
|
||||||
|
}>((emit) => {
|
||||||
const unsubscribes: (() => void)[] = [];
|
const unsubscribes: (() => void)[] = [];
|
||||||
for (const integrationWithSecrets of ctx.integrations) {
|
for (const integrationWithSecrets of ctx.integrations) {
|
||||||
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
|
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import type { Adapter } from "@auth/core/adapters";
|
import type { Adapter } from "@auth/core/adapters";
|
||||||
|
import dayjs from "dayjs";
|
||||||
import type { NextAuthConfig } from "next-auth";
|
import type { NextAuthConfig } from "next-auth";
|
||||||
|
|
||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
@@ -52,12 +53,12 @@ export const createSessionCallback = (db: Database): NextAuthCallbackOf<"session
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const createSignInCallback =
|
export const createSignInCallback =
|
||||||
(adapter: Adapter, isCredentialsRequest: boolean): NextAuthCallbackOf<"signIn"> =>
|
(adapter: Adapter, db: Database, isCredentialsRequest: boolean): NextAuthCallbackOf<"signIn"> =>
|
||||||
async ({ user }) => {
|
async ({ user }) => {
|
||||||
if (!isCredentialsRequest) return true;
|
if (!isCredentialsRequest) return true;
|
||||||
|
|
||||||
// https://github.com/nextauthjs/next-auth/issues/6106
|
// https://github.com/nextauthjs/next-auth/issues/6106
|
||||||
if (!adapter.createSession) {
|
if (!adapter.createSession || !user.id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,8 +67,7 @@ export const createSignInCallback =
|
|||||||
|
|
||||||
await adapter.createSession({
|
await adapter.createSession({
|
||||||
sessionToken,
|
sessionToken,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
userId: user.id,
|
||||||
userId: user.id!,
|
|
||||||
expires: sessionExpires,
|
expires: sessionExpires,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,6 +79,21 @@ export const createSignInCallback =
|
|||||||
secure: true,
|
secure: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dbUser = await db.query.users.findFirst({
|
||||||
|
where: eq(users.id, user.id),
|
||||||
|
columns: {
|
||||||
|
colorScheme: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dbUser) return false;
|
||||||
|
|
||||||
|
// We use a cookie as localStorage is not shared with server (otherwise flickering would occur)
|
||||||
|
cookies().set("homarr-color-scheme", dbUser.colorScheme, {
|
||||||
|
path: "/",
|
||||||
|
expires: dayjs().add(1, "year").toDate(),
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const createConfiguration = (isCredentialsRequest: boolean, headers: Read
|
|||||||
]),
|
]),
|
||||||
callbacks: {
|
callbacks: {
|
||||||
session: createSessionCallback(db),
|
session: createSessionCallback(db),
|
||||||
signIn: createSignInCallback(adapter, isCredentialsRequest),
|
signIn: createSignInCallback(adapter, db, isCredentialsRequest),
|
||||||
},
|
},
|
||||||
redirectProxyUrl: createRedirectUri(headers, "/api/auth"),
|
redirectProxyUrl: createRedirectUri(headers, "/api/auth"),
|
||||||
secret: "secret-is-not-defined-yet", // TODO: This should be added later
|
secret: "secret-is-not-defined-yet", // TODO: This should be added later
|
||||||
|
|||||||
@@ -23,8 +23,8 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/core": "^0.35.0",
|
"@auth/core": "^0.35.2",
|
||||||
"@auth/drizzle-adapter": "^1.5.0",
|
"@auth/drizzle-adapter": "^1.5.2",
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/bcrypt": "5.0.2",
|
"@types/bcrypt": "5.0.2",
|
||||||
"@types/cookies": "0.9.0",
|
"@types/cookies": "0.9.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { cookies } from "next/headers";
|
|||||||
import type { Adapter, AdapterUser } from "@auth/core/adapters";
|
import type { Adapter, AdapterUser } from "@auth/core/adapters";
|
||||||
import type { Account } from "next-auth";
|
import type { Account } from "next-auth";
|
||||||
import type { JWT } from "next-auth/jwt";
|
import type { JWT } from "next-auth/jwt";
|
||||||
import { describe, expect, it, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema/sqlite";
|
import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema/sqlite";
|
||||||
import { createDb } from "@homarr/db/test";
|
import { createDb } from "@homarr/db/test";
|
||||||
@@ -15,6 +15,7 @@ import { createSessionCallback, createSignInCallback, getCurrentUserPermissionsA
|
|||||||
|
|
||||||
describe("getCurrentUserPermissions", () => {
|
describe("getCurrentUserPermissions", () => {
|
||||||
test("should return empty permissions when non existing user requested", async () => {
|
test("should return empty permissions when non existing user requested", async () => {
|
||||||
|
// Arrange
|
||||||
const db = createDb();
|
const db = createDb();
|
||||||
|
|
||||||
await db.insert(groups).values({
|
await db.insert(groups).values({
|
||||||
@@ -30,11 +31,16 @@ describe("getCurrentUserPermissions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const userId = "1";
|
const userId = "1";
|
||||||
|
|
||||||
|
// Act
|
||||||
const result = await getCurrentUserPermissionsAsync(db, userId);
|
const result = await getCurrentUserPermissionsAsync(db, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return empty permissions when user has no groups", async () => {
|
test("should return empty permissions when user has no groups", async () => {
|
||||||
|
// Arrange
|
||||||
const db = createDb();
|
const db = createDb();
|
||||||
const userId = "1";
|
const userId = "1";
|
||||||
|
|
||||||
@@ -50,11 +56,15 @@ describe("getCurrentUserPermissions", () => {
|
|||||||
id: userId,
|
id: userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
const result = await getCurrentUserPermissionsAsync(db, userId);
|
const result = await getCurrentUserPermissionsAsync(db, userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return permissions for user", async () => {
|
test("should return permissions for user", async () => {
|
||||||
|
// Arrange
|
||||||
const db = createDb();
|
const db = createDb();
|
||||||
const getPermissionsWithChildrenMock = vi
|
const getPermissionsWithChildrenMock = vi
|
||||||
.spyOn(definitions, "getPermissionsWithChildren")
|
.spyOn(definitions, "getPermissionsWithChildren")
|
||||||
@@ -77,14 +87,18 @@ describe("getCurrentUserPermissions", () => {
|
|||||||
permission: "admin",
|
permission: "admin",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
const result = await getCurrentUserPermissionsAsync(db, mockId);
|
const result = await getCurrentUserPermissionsAsync(db, mockId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(result).toEqual(["board-create"]);
|
expect(result).toEqual(["board-create"]);
|
||||||
expect(getPermissionsWithChildrenMock).toHaveBeenCalledWith(["admin"]);
|
expect(getPermissionsWithChildrenMock).toHaveBeenCalledWith(["admin"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("session callback", () => {
|
describe("session callback", () => {
|
||||||
it("should add id and name to session user", async () => {
|
test("should add id and name to session user", async () => {
|
||||||
|
// Arrange
|
||||||
const user: AdapterUser = {
|
const user: AdapterUser = {
|
||||||
id: "id",
|
id: "id",
|
||||||
name: "name",
|
name: "name",
|
||||||
@@ -94,6 +108,8 @@ describe("session callback", () => {
|
|||||||
const token: JWT = {};
|
const token: JWT = {};
|
||||||
const db = createDb();
|
const db = createDb();
|
||||||
const callback = createSessionCallback(db);
|
const callback = createSessionCallback(db);
|
||||||
|
|
||||||
|
// Act
|
||||||
const result = await callback({
|
const result = await callback({
|
||||||
session: {
|
session: {
|
||||||
user: {
|
user: {
|
||||||
@@ -112,6 +128,8 @@ describe("session callback", () => {
|
|||||||
trigger: "update",
|
trigger: "update",
|
||||||
newSession: {},
|
newSession: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(result.user).toBeDefined();
|
expect(result.user).toBeDefined();
|
||||||
expect(result.user!.id).toEqual(user.id);
|
expect(result.user!.id).toEqual(user.id);
|
||||||
expect(result.user!.name).toEqual(user.name);
|
expect(result.user!.name).toEqual(user.name);
|
||||||
@@ -169,37 +187,56 @@ vi.mock("next/headers", async (importOriginal) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("createSignInCallback", () => {
|
describe("createSignInCallback", () => {
|
||||||
it("should return true if not credentials request", async () => {
|
test("should return true if not credentials request and set colorScheme & sessionToken cookie", async () => {
|
||||||
|
// Arrange
|
||||||
const isCredentialsRequest = false;
|
const isCredentialsRequest = false;
|
||||||
const signInCallback = createSignInCallback(createAdapter(), isCredentialsRequest);
|
const db = await prepareDbForSigninAsync("1");
|
||||||
|
const signInCallback = createSignInCallback(createAdapter(), db, isCredentialsRequest);
|
||||||
|
|
||||||
|
// Act
|
||||||
const result = await signInCallback({
|
const result = await signInCallback({
|
||||||
user: { id: "1", emailVerified: new Date("2023-01-13") },
|
user: { id: "1", emailVerified: new Date("2023-01-13") },
|
||||||
account: {} as Account,
|
account: {} as Account,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false if no adapter.createSession", async () => {
|
test("should return false if no adapter.createSession", async () => {
|
||||||
|
// Arrange
|
||||||
const isCredentialsRequest = true;
|
const isCredentialsRequest = true;
|
||||||
|
const db = await prepareDbForSigninAsync("1");
|
||||||
const signInCallback = createSignInCallback(
|
const signInCallback = createSignInCallback(
|
||||||
// https://github.com/nextauthjs/next-auth/issues/6106
|
// https://github.com/nextauthjs/next-auth/issues/6106
|
||||||
{ createSession: undefined } as unknown as Adapter,
|
{ createSession: undefined } as unknown as Adapter,
|
||||||
|
db,
|
||||||
isCredentialsRequest,
|
isCredentialsRequest,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
const result = await signInCallback({
|
const result = await signInCallback({
|
||||||
user: { id: "1", emailVerified: new Date("2023-01-13") },
|
user: { id: "1", emailVerified: new Date("2023-01-13") },
|
||||||
account: {} as Account,
|
account: {} as Account,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should call adapter.createSession with correct input", async () => {
|
test("should call adapter.createSession with correct input", async () => {
|
||||||
|
// Arrange
|
||||||
const adapter = createAdapter();
|
const adapter = createAdapter();
|
||||||
const isCredentialsRequest = true;
|
const isCredentialsRequest = true;
|
||||||
const signInCallback = createSignInCallback(adapter, isCredentialsRequest);
|
const db = await prepareDbForSigninAsync("1");
|
||||||
|
const signInCallback = createSignInCallback(adapter, db, isCredentialsRequest);
|
||||||
const user = { id: "1", emailVerified: new Date("2023-01-13") };
|
const user = { id: "1", emailVerified: new Date("2023-01-13") };
|
||||||
const account = {} as Account;
|
const account = {} as Account;
|
||||||
|
|
||||||
|
// Act
|
||||||
await signInCallback({ user, account });
|
await signInCallback({ user, account });
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(adapter.createSession).toHaveBeenCalledWith({
|
expect(adapter.createSession).toHaveBeenCalledWith({
|
||||||
sessionToken: mockSessionToken,
|
sessionToken: mockSessionToken,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -213,4 +250,52 @@ describe("createSignInCallback", () => {
|
|||||||
secure: true,
|
secure: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should set colorScheme from db as cookie", async () => {
|
||||||
|
// Arrange
|
||||||
|
const isCredentialsRequest = false;
|
||||||
|
const db = await prepareDbForSigninAsync("1");
|
||||||
|
const signInCallback = createSignInCallback(createAdapter(), db, isCredentialsRequest);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await signInCallback({
|
||||||
|
user: { id: "1", emailVerified: new Date("2023-01-13") },
|
||||||
|
account: {} as Account,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(cookies().set).toHaveBeenCalledWith(
|
||||||
|
"homarr-color-scheme",
|
||||||
|
"dark",
|
||||||
|
expect.objectContaining({
|
||||||
|
path: "/",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return false if user not found in db", async () => {
|
||||||
|
// Arrange
|
||||||
|
const isCredentialsRequest = true;
|
||||||
|
const db = await prepareDbForSigninAsync("other-id");
|
||||||
|
const signInCallback = createSignInCallback(createAdapter(), db, isCredentialsRequest);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await signInCallback({
|
||||||
|
user: { id: "1", emailVerified: new Date("2023-01-13") },
|
||||||
|
account: {} as Account,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const prepareDbForSigninAsync = async (userId: string) => {
|
||||||
|
const db = createDb();
|
||||||
|
await db.insert(users).values({
|
||||||
|
id: userId,
|
||||||
|
colorScheme: "dark",
|
||||||
|
});
|
||||||
|
return db;
|
||||||
|
};
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,13 +28,13 @@
|
|||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"next": "^14.2.13",
|
"next": "^14.2.13",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"tldts": "^6.1.47"
|
"tldts": "^6.1.48"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import type { CookieSerializeOptions } from "cookie";
|
||||||
|
import { serialize } from "cookie";
|
||||||
|
|
||||||
export function parseCookies(cookieString: string) {
|
export function parseCookies(cookieString: string) {
|
||||||
const list: Record<string, string> = {};
|
const list: Record<string, string> = {};
|
||||||
const cookieHeader = cookieString;
|
const cookieHeader = cookieString;
|
||||||
@@ -15,3 +18,7 @@ export function parseCookies(cookieString: string) {
|
|||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setClientCookie(name: string, value: string, options: CookieSerializeOptions = {}) {
|
||||||
|
document.cookie = serialize(name, value, options);
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { analyticsJob } from "./jobs/analytics";
|
import { analyticsJob } from "./jobs/analytics";
|
||||||
import { iconsUpdaterJob } from "./jobs/icons-updater";
|
import { iconsUpdaterJob } from "./jobs/icons-updater";
|
||||||
|
import { dnsHoleJob } from "./jobs/integrations/dns-hole";
|
||||||
import { downloadsJob } from "./jobs/integrations/downloads";
|
import { downloadsJob } from "./jobs/integrations/downloads";
|
||||||
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";
|
||||||
@@ -19,6 +20,7 @@ export const jobGroup = createCronJobGroup({
|
|||||||
mediaServer: mediaServerJob,
|
mediaServer: mediaServerJob,
|
||||||
mediaOrganizer: mediaOrganizerJob,
|
mediaOrganizer: mediaOrganizerJob,
|
||||||
downloads: downloadsJob,
|
downloads: downloadsJob,
|
||||||
|
dnsHole: dnsHoleJob,
|
||||||
mediaRequests: mediaRequestsJob,
|
mediaRequests: mediaRequestsJob,
|
||||||
rssFeeds: rssFeedsJob,
|
rssFeeds: rssFeedsJob,
|
||||||
indexerManager: indexerManagerJob,
|
indexerManager: indexerManagerJob,
|
||||||
|
|||||||
28
packages/cron-jobs/src/jobs/integrations/dns-hole.ts
Normal file
28
packages/cron-jobs/src/jobs/integrations/dns-hole.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions";
|
||||||
|
import { db } from "@homarr/db";
|
||||||
|
import { getItemsWithIntegrationsAsync } from "@homarr/db/queries";
|
||||||
|
import { integrationCreatorFromSecrets } from "@homarr/integrations";
|
||||||
|
import type { DnsHoleSummary } from "@homarr/integrations/types";
|
||||||
|
import { logger } from "@homarr/log";
|
||||||
|
import { createItemAndIntegrationChannel } from "@homarr/redis";
|
||||||
|
|
||||||
|
import { createCronJob } from "../../lib";
|
||||||
|
|
||||||
|
export const dnsHoleJob = createCronJob("dnsHole", EVERY_5_SECONDS).withCallback(async () => {
|
||||||
|
const itemsForIntegration = await getItemsWithIntegrationsAsync(db, {
|
||||||
|
kinds: ["dnsHoleSummary", "dnsHoleControls"],
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const itemForIntegration of itemsForIntegration) {
|
||||||
|
for (const { integration } of itemForIntegration.integrations) {
|
||||||
|
const integrationInstance = integrationCreatorFromSecrets(integration);
|
||||||
|
await integrationInstance
|
||||||
|
.getSummaryAsync()
|
||||||
|
.then(async (data) => {
|
||||||
|
const channel = createItemAndIntegrationChannel<DnsHoleSummary>(itemForIntegration.kind, integration.id);
|
||||||
|
await channel.publishAndUpdateLastStateAsync(data);
|
||||||
|
})
|
||||||
|
.catch((error) => logger.error(`Could not retrieve data for ${integration.name}: "${error}"`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/core": "^0.35.0",
|
"@auth/core": "^0.35.2",
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/better-sqlite3": "7.6.11",
|
"@types/better-sqlite3": "7.6.11",
|
||||||
"dotenv-cli": "^7.4.2",
|
"dotenv-cli": "^7.4.2",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,13 +24,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/form": "^7.12.2"
|
"@mantine/form": "^7.13.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export class AdGuardHomeIntegration extends Integration implements DnsHoleSummar
|
|||||||
return {
|
return {
|
||||||
status: status.data.protection_enabled ? ("enabled" as const) : ("disabled" as const),
|
status: status.data.protection_enabled ? ("enabled" as const) : ("disabled" as const),
|
||||||
adsBlockedToday: blockedQueriesToday,
|
adsBlockedToday: blockedQueriesToday,
|
||||||
adsBlockedTodayPercentage: (queriesToday / blockedQueriesToday) * 100,
|
adsBlockedTodayPercentage: blockedQueriesToday > 0 ? (queriesToday / blockedQueriesToday) * 100 : 0,
|
||||||
domainsBeingBlocked: countFilteredDomains,
|
domainsBeingBlocked: countFilteredDomains,
|
||||||
dnsQueriesToday: queriesToday,
|
dnsQueriesToday: queriesToday,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export interface DnsHoleSummary {
|
export interface DnsHoleSummary {
|
||||||
status: "enabled" | "disabled";
|
status?: "enabled" | "disabled";
|
||||||
domainsBeingBlocked: number;
|
domainsBeingBlocked: number;
|
||||||
adsBlockedToday: number;
|
adsBlockedToday: number;
|
||||||
adsBlockedTodayPercentage: number;
|
adsBlockedTodayPercentage: number;
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export * from "./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/indexer-manager/indexer";
|
export * from "./interfaces/indexer-manager/indexer";
|
||||||
export * from "./interfaces/media-requests/media-request";
|
export * from "./interfaces/media-requests/media-request";
|
||||||
|
export * from "./pi-hole/pi-hole-types";
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,8 @@
|
|||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^7.12.2",
|
"@mantine/core": "^7.13.0",
|
||||||
"@tabler/icons-react": "^3.17.0",
|
"@tabler/icons-react": "^3.18.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"next": "^14.2.13",
|
"next": "^14.2.13",
|
||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config"
|
"prettier": "@homarr/prettier-config"
|
||||||
|
|||||||
@@ -24,15 +24,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^7.12.2",
|
"@mantine/core": "^7.13.0",
|
||||||
"@mantine/hooks": "^7.12.2",
|
"@mantine/hooks": "^7.13.0",
|
||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,14 +24,14 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@mantine/notifications": "^7.12.2",
|
"@mantine/notifications": "^7.13.0",
|
||||||
"@tabler/icons-react": "^3.17.0"
|
"@tabler/icons-react": "^3.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config"
|
"prettier": "@homarr/prettier-config"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config"
|
"prettier": "@homarr/prettier-config"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,10 +30,10 @@
|
|||||||
"@homarr/modals-collection": "workspace:^0.1.0",
|
"@homarr/modals-collection": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^7.12.2",
|
"@mantine/core": "^7.13.0",
|
||||||
"@mantine/hooks": "^7.12.2",
|
"@mantine/hooks": "^7.13.0",
|
||||||
"@mantine/spotlight": "^7.12.2",
|
"@mantine/spotlight": "^7.13.0",
|
||||||
"@tabler/icons-react": "^3.17.0",
|
"@tabler/icons-react": "^3.18.0",
|
||||||
"jotai": "^2.10.0",
|
"jotai": "^2.10.0",
|
||||||
"next": "^14.2.13",
|
"next": "^14.2.13",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config"
|
"prettier": "@homarr/prettier-config"
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -532,10 +532,6 @@ export default {
|
|||||||
},
|
},
|
||||||
beta: "Beta",
|
beta: "Beta",
|
||||||
error: "Error",
|
error: "Error",
|
||||||
errors: {
|
|
||||||
noData: "No data to show",
|
|
||||||
noIntegration: "No integration selected",
|
|
||||||
},
|
|
||||||
action: {
|
action: {
|
||||||
add: "Add",
|
add: "Add",
|
||||||
apply: "Apply",
|
apply: "Apply",
|
||||||
@@ -799,6 +795,7 @@ export default {
|
|||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
internalServerError: "Failed to fetch DNS Hole Summary",
|
internalServerError: "Failed to fetch DNS Hole Summary",
|
||||||
|
integrationsDisconnected: "No data available, all integrations disconnected",
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
adsBlockedToday: "Blocked today",
|
adsBlockedToday: "Blocked today",
|
||||||
@@ -839,6 +836,8 @@ export default {
|
|||||||
set: "Set",
|
set: "Set",
|
||||||
enabled: "Enabled",
|
enabled: "Enabled",
|
||||||
disabled: "Disabled",
|
disabled: "Disabled",
|
||||||
|
processing: "Processing",
|
||||||
|
disconnected: "Disconnected",
|
||||||
hours: "Hours",
|
hours: "Hours",
|
||||||
minutes: "Minutes",
|
minutes: "Minutes",
|
||||||
unlimited: "Leave blank to unlimited",
|
unlimited: "Leave blank to unlimited",
|
||||||
@@ -1098,6 +1097,7 @@ export default {
|
|||||||
logs: "Check logs for more details",
|
logs: "Check logs for more details",
|
||||||
},
|
},
|
||||||
noIntegration: "No integration selected",
|
noIntegration: "No integration selected",
|
||||||
|
noData: "No integration data available",
|
||||||
},
|
},
|
||||||
option: {},
|
option: {},
|
||||||
},
|
},
|
||||||
@@ -1842,6 +1842,9 @@ export default {
|
|||||||
indexerManager: {
|
indexerManager: {
|
||||||
label: "Indexer Manager",
|
label: "Indexer Manager",
|
||||||
},
|
},
|
||||||
|
dnsHole: {
|
||||||
|
label: "DNS Hole Data",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,10 +28,10 @@
|
|||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^7.12.2",
|
"@mantine/core": "^7.13.0",
|
||||||
"@mantine/dates": "^7.12.2",
|
"@mantine/dates": "^7.13.0",
|
||||||
"@mantine/hooks": "^7.12.2",
|
"@mantine/hooks": "^7.13.0",
|
||||||
"@tabler/icons-react": "^3.17.0",
|
"@tabler/icons-react": "^3.18.0",
|
||||||
"mantine-react-table": "2.0.0-beta.6",
|
"mantine-react-table": "2.0.0-beta.6",
|
||||||
"next": "^14.2.13",
|
"next": "^14.2.13",
|
||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/css-modules": "^1.0.5",
|
"@types/css-modules": "^1.0.5",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config"
|
"prettier": "@homarr/prettier-config"
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,24 +38,24 @@
|
|||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^7.12.2",
|
"@mantine/core": "^7.13.0",
|
||||||
"@mantine/hooks": "^7.12.2",
|
"@mantine/hooks": "^7.13.0",
|
||||||
"@tabler/icons-react": "^3.17.0",
|
"@tabler/icons-react": "^3.18.0",
|
||||||
"@tiptap/extension-color": "2.7.2",
|
"@tiptap/extension-color": "2.7.4",
|
||||||
"@tiptap/extension-highlight": "2.7.2",
|
"@tiptap/extension-highlight": "2.7.4",
|
||||||
"@tiptap/extension-image": "2.7.2",
|
"@tiptap/extension-image": "2.7.4",
|
||||||
"@tiptap/extension-link": "^2.7.2",
|
"@tiptap/extension-link": "^2.7.4",
|
||||||
"@tiptap/extension-table": "2.7.2",
|
"@tiptap/extension-table": "2.7.4",
|
||||||
"@tiptap/extension-table-cell": "2.7.2",
|
"@tiptap/extension-table-cell": "2.7.4",
|
||||||
"@tiptap/extension-table-header": "2.7.2",
|
"@tiptap/extension-table-header": "2.7.4",
|
||||||
"@tiptap/extension-table-row": "2.7.2",
|
"@tiptap/extension-table-row": "2.7.4",
|
||||||
"@tiptap/extension-task-item": "2.7.2",
|
"@tiptap/extension-task-item": "2.7.4",
|
||||||
"@tiptap/extension-task-list": "2.7.2",
|
"@tiptap/extension-task-list": "2.7.4",
|
||||||
"@tiptap/extension-text-align": "2.7.2",
|
"@tiptap/extension-text-align": "2.7.4",
|
||||||
"@tiptap/extension-text-style": "2.7.2",
|
"@tiptap/extension-text-style": "2.7.4",
|
||||||
"@tiptap/extension-underline": "2.7.2",
|
"@tiptap/extension-underline": "2.7.4",
|
||||||
"@tiptap/react": "^2.7.2",
|
"@tiptap/react": "^2.7.4",
|
||||||
"@tiptap/starter-kit": "^2.7.2",
|
"@tiptap/starter-kit": "^2.7.4",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"mantine-react-table": "2.0.0-beta.6",
|
"mantine-react-table": "2.0.0-beta.6",
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/video.js": "^7.3.58",
|
"@types/video.js": "^7.3.58",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,140 +1,228 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import "../../widgets-common.css";
|
||||||
import { ActionIcon, Badge, Button, Card, Flex, Image, Stack, Text, Tooltip, UnstyledButton } from "@mantine/core";
|
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
|
||||||
import { IconClockPause, IconPlayerPlay, IconPlayerStop } from "@tabler/icons-react";
|
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Flex,
|
||||||
|
Image,
|
||||||
|
ScrollArea,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
UnstyledButton,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
import { IconCircleFilled, IconClockPause, IconPlayerPlay, IconPlayerStop } from "@tabler/icons-react";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
import { useIntegrationsWithInteractAccess } from "@homarr/auth/client";
|
||||||
import { integrationDefs } from "@homarr/definitions";
|
import { integrationDefs } from "@homarr/definitions";
|
||||||
import type { TranslationFunction } from "@homarr/translation";
|
import type { TranslationFunction } from "@homarr/translation";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
|
import { widgetKind } from ".";
|
||||||
import type { WidgetComponentProps } from "../../definition";
|
import type { WidgetComponentProps } from "../../definition";
|
||||||
import { NoIntegrationSelectedError } from "../../errors";
|
import { NoIntegrationSelectedError } from "../../errors";
|
||||||
import TimerModal from "./TimerModal";
|
import TimerModal from "./TimerModal";
|
||||||
|
|
||||||
const dnsLightStatus = (enabled: boolean): "green" | "red" => (enabled ? "green" : "red");
|
const dnsLightStatus = (enabled: boolean | undefined) =>
|
||||||
|
`var(--mantine-color-${typeof enabled === "undefined" ? "blue" : enabled ? "green" : "red"}-6`;
|
||||||
|
|
||||||
export default function DnsHoleControlsWidget({ options, integrationIds }: WidgetComponentProps<"dnsHoleControls">) {
|
export default function DnsHoleControlsWidget({
|
||||||
if (integrationIds.length === 0) {
|
options,
|
||||||
throw new NoIntegrationSelectedError();
|
integrationIds,
|
||||||
}
|
isEditMode,
|
||||||
const t = useI18n();
|
serverData,
|
||||||
const [status, setStatus] = useState<{ integrationId: string; enabled: boolean }[]>(
|
}: WidgetComponentProps<typeof widgetKind>) {
|
||||||
integrationIds.map((id) => ({ integrationId: id, enabled: false })),
|
// DnsHole integrations with interaction permissions
|
||||||
);
|
const integrationsWithInteractions = useIntegrationsWithInteractAccess()
|
||||||
const [selectedIntegrationIds, setSelectedIntegrationIds] = useState<string[]>([]);
|
.map(({ id }) => id)
|
||||||
const [opened, { close, open }] = useDisclosure(false);
|
.filter((id) => integrationIds.includes(id));
|
||||||
|
|
||||||
const [data] = clientApi.widget.dnsHole.summary.useSuspenseQuery(
|
// Initial summaries, null summary means disconnected, undefined status means processing
|
||||||
|
const [summaries, setSummaries] = useState(serverData?.initialData ?? []);
|
||||||
|
|
||||||
|
// Subscribe to summary updates
|
||||||
|
clientApi.widget.dnsHole.subscribeToSummary.useSubscription(
|
||||||
{
|
{
|
||||||
|
widgetKind,
|
||||||
integrationIds,
|
integrationIds,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
refetchOnMount: false,
|
onData: (data) => {
|
||||||
retry: false,
|
setSummaries((prevSummaries) =>
|
||||||
|
prevSummaries.map((summary) => (summary.integration.id === data.integration.id ? data : summary)),
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
// Mutations for dnsHole state, set to undefined on click, and change again on settle
|
||||||
const newStatus = data.map((integrationData) => ({
|
|
||||||
integrationId: integrationData.integrationId,
|
|
||||||
enabled: integrationData.summary.status === "enabled",
|
|
||||||
}));
|
|
||||||
setStatus(newStatus);
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const { mutate: enableDns } = clientApi.widget.dnsHole.enable.useMutation({
|
const { mutate: enableDns } = clientApi.widget.dnsHole.enable.useMutation({
|
||||||
onSuccess: (_, variables) => {
|
onSettled: (_, error, { integrationId }) => {
|
||||||
setStatus((prevStatus) =>
|
setSummaries((prevSummaries) =>
|
||||||
prevStatus.map((item) => (item.integrationId === variables.integrationId ? { ...item, enabled: true } : item)),
|
prevSummaries.map((data) => ({
|
||||||
|
...data,
|
||||||
|
summary:
|
||||||
|
data.integration.id === integrationId && data.summary
|
||||||
|
? { ...data.summary, status: error ? "disabled" : "enabled" }
|
||||||
|
: data.summary,
|
||||||
|
})),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { mutate: disableDns } = clientApi.widget.dnsHole.disable.useMutation({
|
const { mutate: disableDns } = clientApi.widget.dnsHole.disable.useMutation({
|
||||||
onSuccess: (_, variables) => {
|
onSettled: (_, error, { integrationId }) => {
|
||||||
setStatus((prevStatus) =>
|
setSummaries((prevSummaries) =>
|
||||||
prevStatus.map((item) => (item.integrationId === variables.integrationId ? { ...item, enabled: false } : item)),
|
prevSummaries.map((data) => ({
|
||||||
|
...data,
|
||||||
|
summary:
|
||||||
|
data.integration.id === integrationId && data.summary
|
||||||
|
? { ...data.summary, status: error ? "enabled" : "disabled" }
|
||||||
|
: data.summary,
|
||||||
|
})),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const toggleDns = (integrationId: string) => {
|
const toggleDns = (integrationId: string) => {
|
||||||
const integrationStatus = status.find((item) => item.integrationId === integrationId);
|
const integrationStatus = summaries.find(({ integration }) => integration.id === integrationId);
|
||||||
if (integrationStatus?.enabled) {
|
if (!integrationStatus?.summary?.status) return;
|
||||||
|
setSummaries((prevSummaries) =>
|
||||||
|
prevSummaries.map((data) => ({
|
||||||
|
...data,
|
||||||
|
summary:
|
||||||
|
data.integration.id === integrationId && data.summary ? { ...data.summary, status: undefined } : data.summary,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
if (integrationStatus.summary.status === "enabled") {
|
||||||
disableDns({ integrationId, duration: 0 });
|
disableDns({ integrationId, duration: 0 });
|
||||||
} else {
|
} else {
|
||||||
enableDns({ integrationId });
|
enableDns({ integrationId });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const enabledIntegrations = integrationIds.filter((id) => status.find((item) => item.integrationId === id)?.enabled);
|
// make lists of enabled and disabled interactable integrations (with permissions, not disconnected and not processing)
|
||||||
const disabledIntegrations = integrationIds.filter(
|
const integrationsSummaries = summaries.reduce(
|
||||||
(id) => !status.find((item) => item.integrationId === id)?.enabled,
|
(acc, { summary, integration: { id } }) =>
|
||||||
|
integrationsWithInteractions.includes(id) && summary?.status != null ? (acc[summary.status].push(id), acc) : acc,
|
||||||
|
{ enabled: [] as string[], disabled: [] as string[] },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
// Timer modal setup
|
||||||
|
const [selectedIntegrationIds, setSelectedIntegrationIds] = useState<string[]>([]);
|
||||||
|
const [opened, { close, open }] = useDisclosure(false);
|
||||||
|
|
||||||
|
const controlAllButtonsVisible = options.showToggleAllButtons && integrationsWithInteractions.length > 0;
|
||||||
|
|
||||||
|
if (integrationIds.length === 0) {
|
||||||
|
throw new NoIntegrationSelectedError();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex h="100%" direction="column" gap={0} p="2.5cqmin">
|
<Flex
|
||||||
{options.showToggleAllButtons && (
|
className="dns-hole-controls-stack"
|
||||||
<Flex m="2.5cqmin" gap="2.5cqmin">
|
h="100%"
|
||||||
|
direction="column"
|
||||||
|
p="2.5cqmin"
|
||||||
|
gap="2.5cqmin"
|
||||||
|
style={{ pointerEvents: isEditMode ? "none" : undefined }}
|
||||||
|
>
|
||||||
|
{controlAllButtonsVisible && (
|
||||||
|
<Flex className="dns-hole-controls-buttons" gap="2.5cqmin">
|
||||||
<Tooltip label={t("widget.dnsHoleControls.controls.enableAll")}>
|
<Tooltip label={t("widget.dnsHoleControls.controls.enableAll")}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => disabledIntegrations.forEach((integrationId) => enableDns({ integrationId }))}
|
className="dns-hole-controls-enable-all-button"
|
||||||
disabled={disabledIntegrations.length === 0}
|
onClick={() => integrationsSummaries.disabled.forEach((integrationId) => toggleDns(integrationId))}
|
||||||
|
disabled={integrationsSummaries.disabled.length === 0}
|
||||||
variant="light"
|
variant="light"
|
||||||
color="green"
|
color="green"
|
||||||
fullWidth
|
h="fit-content"
|
||||||
h="2rem"
|
p="1.25cqmin"
|
||||||
|
bd={0}
|
||||||
|
radius="2.5cqmin"
|
||||||
|
flex={1}
|
||||||
>
|
>
|
||||||
<IconPlayerPlay size={20} />
|
<IconPlayerPlay
|
||||||
|
className="dns-hole-controls-enable-all-icon"
|
||||||
|
style={{ height: "7.5cqmin", width: "7.5cqmin" }}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip label={t("widget.dnsHoleControls.controls.setTimer")}>
|
<Tooltip label={t("widget.dnsHoleControls.controls.setTimer")}>
|
||||||
<Button
|
<Button
|
||||||
|
className="dns-hole-controls-timer-all-button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedIntegrationIds(enabledIntegrations);
|
setSelectedIntegrationIds(integrationsSummaries.enabled);
|
||||||
open();
|
open();
|
||||||
}}
|
}}
|
||||||
disabled={enabledIntegrations.length === 0}
|
disabled={integrationsSummaries.enabled.length === 0}
|
||||||
variant="light"
|
variant="light"
|
||||||
color="yellow"
|
color="yellow"
|
||||||
fullWidth
|
h="fit-content"
|
||||||
h="2rem"
|
p="1.25cqmin"
|
||||||
|
bd={0}
|
||||||
|
radius="2.5cqmin"
|
||||||
|
flex={1}
|
||||||
>
|
>
|
||||||
<IconClockPause size={20} />
|
<IconClockPause
|
||||||
|
className="dns-hole-controls-timer-all-icon"
|
||||||
|
style={{ height: "7.5cqmin", width: "7.5cqmin" }}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip label={t("widget.dnsHoleControls.controls.disableAll")}>
|
<Tooltip label={t("widget.dnsHoleControls.controls.disableAll")}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => enabledIntegrations.forEach((integrationId) => disableDns({ integrationId, duration: 0 }))}
|
className="dns-hole-controls-disable-all-button"
|
||||||
disabled={enabledIntegrations.length === 0}
|
onClick={() => integrationsSummaries.enabled.forEach((integrationId) => toggleDns(integrationId))}
|
||||||
|
disabled={integrationsSummaries.enabled.length === 0}
|
||||||
variant="light"
|
variant="light"
|
||||||
color="red"
|
color="red"
|
||||||
fullWidth
|
h="fit-content"
|
||||||
h="2rem"
|
p="1.25cqmin"
|
||||||
|
bd={0}
|
||||||
|
radius="2.5cqmin"
|
||||||
|
flex={1}
|
||||||
>
|
>
|
||||||
<IconPlayerStop size={20} />
|
<IconPlayerStop
|
||||||
|
className="dns-hole-controls-disable-all-icon"
|
||||||
|
style={{ height: "7.5cqmin", width: "7.5cqmin" }}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Stack m="2.5cqmin" gap="2.5cqmin" flex={1} justify={options.showToggleAllButtons ? "flex-end" : "space-evenly"}>
|
<ScrollArea className="dns-hole-controls-integration-list-scroll-area flexed-scroll-area">
|
||||||
{data.map((integrationData) => (
|
<Stack
|
||||||
<ControlsCard
|
className="dns-hole-controls-integration-list"
|
||||||
key={integrationData.integrationId}
|
gap="2.5cqmin"
|
||||||
integrationId={integrationData.integrationId}
|
flex={1}
|
||||||
integrationKind={integrationData.integrationKind}
|
justify={controlAllButtonsVisible ? "flex-end" : "space-evenly"}
|
||||||
toggleDns={toggleDns}
|
>
|
||||||
status={status}
|
{summaries.map((summary) => (
|
||||||
setSelectedIntegrationIds={setSelectedIntegrationIds}
|
<ControlsCard
|
||||||
open={open}
|
key={summary.integration.id}
|
||||||
t={t}
|
integrationsWithInteractions={integrationsWithInteractions}
|
||||||
/>
|
toggleDns={toggleDns}
|
||||||
))}
|
data={summary}
|
||||||
</Stack>
|
setSelectedIntegrationIds={setSelectedIntegrationIds}
|
||||||
|
open={open}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
<TimerModal
|
<TimerModal
|
||||||
opened={opened}
|
opened={opened}
|
||||||
@@ -147,52 +235,109 @@ export default function DnsHoleControlsWidget({ options, integrationIds }: Widge
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ControlsCardProps {
|
interface ControlsCardProps {
|
||||||
integrationId: string;
|
integrationsWithInteractions: string[];
|
||||||
integrationKind: string;
|
|
||||||
toggleDns: (integrationId: string) => void;
|
toggleDns: (integrationId: string) => void;
|
||||||
status: { integrationId: string; enabled: boolean }[];
|
data: RouterOutputs["widget"]["dnsHole"]["summary"][number];
|
||||||
setSelectedIntegrationIds: (integrationId: string[]) => void;
|
setSelectedIntegrationIds: (integrationId: string[]) => void;
|
||||||
open: () => void;
|
open: () => void;
|
||||||
t: TranslationFunction;
|
t: TranslationFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ControlsCard: React.FC<ControlsCardProps> = ({
|
const ControlsCard: React.FC<ControlsCardProps> = ({
|
||||||
integrationId,
|
integrationsWithInteractions,
|
||||||
integrationKind,
|
|
||||||
toggleDns,
|
toggleDns,
|
||||||
status,
|
data,
|
||||||
setSelectedIntegrationIds,
|
setSelectedIntegrationIds,
|
||||||
open,
|
open,
|
||||||
t,
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
const integrationStatus = status.find((item) => item.integrationId === integrationId);
|
// Independently determine connection status, current state and permissions
|
||||||
const isEnabled = integrationStatus?.enabled ?? false;
|
const isConnected = data.summary !== null && Math.abs(dayjs(data.timestamp).diff()) < 30000;
|
||||||
const integrationDef = integrationKind === "piHole" ? integrationDefs.piHole : integrationDefs.adGuardHome;
|
const isEnabled = data.summary?.status ? data.summary.status === "enabled" : undefined;
|
||||||
|
const isInteractPermitted = integrationsWithInteractions.includes(data.integration.id);
|
||||||
|
// Use all factors to infer the state of the action buttons
|
||||||
|
const controlEnabled = isInteractPermitted && isEnabled !== undefined && isConnected;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={integrationId} withBorder p="2.5cqmin" radius="2.5cqmin">
|
<Card
|
||||||
<Flex justify="space-between" align="center" direction="row" m="2.5cqmin">
|
className={`dns-hole-controls-integration-item-outer-shell dns-hole-controls-integration-item-${data.integration.id} dns-hole-controls-integration-item-${data.integration.name}`}
|
||||||
<Image src={integrationDef.iconUrl} width="50cqmin" height="50cqmin" fit="contain" />
|
key={data.integration.id}
|
||||||
<Flex direction="column">
|
withBorder
|
||||||
<Text>{integrationDef.name}</Text>
|
p="2.5cqmin"
|
||||||
<Flex direction="row" gap="2cqmin">
|
radius="2.5cqmin"
|
||||||
<UnstyledButton onClick={() => toggleDns(integrationId)}>
|
>
|
||||||
<Badge variant="dot" color={dnsLightStatus(isEnabled)}>
|
<Flex className="dns-hole-controls-item-container" gap="4cqmin" align="center" direction="row">
|
||||||
{t(`widget.dnsHoleControls.controls.${isEnabled ? "enabled" : "disabled"}`)}
|
<Image
|
||||||
|
className="dns-hole-controls-item-icon"
|
||||||
|
src={integrationDefs[data.integration.kind].iconUrl}
|
||||||
|
w="20cqmin"
|
||||||
|
h="20cqmin"
|
||||||
|
fit="contain"
|
||||||
|
style={{ filter: !isConnected ? "grayscale(100%)" : undefined }}
|
||||||
|
/>
|
||||||
|
<Flex className="dns-hole-controls-item-data-stack" direction="column" gap="1.5cqmin">
|
||||||
|
<Text className="dns-hole-controls-item-integration-name" fz="7cqmin">
|
||||||
|
{data.integration.name}
|
||||||
|
</Text>
|
||||||
|
<Flex className="dns-hole-controls-item-controls" direction="row" gap="1.5cqmin">
|
||||||
|
<UnstyledButton
|
||||||
|
className="dns-hole-controls-item-toggle-button"
|
||||||
|
disabled={!controlEnabled}
|
||||||
|
display="contents"
|
||||||
|
style={{ cursor: controlEnabled ? "pointer" : "default" }}
|
||||||
|
onClick={() => toggleDns(data.integration.id)}
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
className={`dns-hole-controls-item-toggle-button-styling${controlEnabled ? " hoverable-component clickable-component" : ""}`}
|
||||||
|
bd="0.1cqmin solid var(--border-color)"
|
||||||
|
px="2.5cqmin"
|
||||||
|
h="7.5cqmin"
|
||||||
|
fz="4.5cqmin"
|
||||||
|
lts="0.1cqmin"
|
||||||
|
color="var(--background-color)"
|
||||||
|
c="var(--mantine-color-text)"
|
||||||
|
styles={{ section: { marginInlineEnd: "2.5cqmin" } }}
|
||||||
|
leftSection={
|
||||||
|
isConnected && (
|
||||||
|
<IconCircleFilled
|
||||||
|
className="dns-hole-controls-item-status-icon"
|
||||||
|
color={dnsLightStatus(isEnabled)}
|
||||||
|
style={{ height: "3.5cqmin", width: "3.5cqmin" }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
`widget.dnsHoleControls.controls.${
|
||||||
|
!isConnected
|
||||||
|
? "disconnected"
|
||||||
|
: typeof isEnabled === "undefined"
|
||||||
|
? "processing"
|
||||||
|
: isEnabled
|
||||||
|
? "enabled"
|
||||||
|
: "disabled"
|
||||||
|
}`,
|
||||||
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
disabled={!isEnabled}
|
className="dns-hole-controls-item-timer-button"
|
||||||
size={20}
|
display={isInteractPermitted ? undefined : "none"}
|
||||||
radius="xl"
|
disabled={!controlEnabled || !isEnabled}
|
||||||
top="2.67px"
|
color="yellow"
|
||||||
variant="default"
|
size="fit-content"
|
||||||
|
radius="999px 999px 0px 999px"
|
||||||
|
bd={0}
|
||||||
|
variant="subtle"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedIntegrationIds([integrationId]);
|
setSelectedIntegrationIds([data.integration.id]);
|
||||||
open();
|
open();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconClockPause size={20} color="red" />
|
<IconClockPause
|
||||||
|
className="dns-hole-controls-item-timer-icon"
|
||||||
|
style={{ height: "7.5cqmin", width: "7.5cqmin" }}
|
||||||
|
/>
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
|||||||
import { createWidgetDefinition } from "../../definition";
|
import { createWidgetDefinition } from "../../definition";
|
||||||
import { optionsBuilder } from "../../options";
|
import { optionsBuilder } from "../../options";
|
||||||
|
|
||||||
export const { definition, componentLoader, serverDataLoader } = createWidgetDefinition("dnsHoleControls", {
|
export const widgetKind = "dnsHoleControls";
|
||||||
|
|
||||||
|
export const { definition, componentLoader, serverDataLoader } = createWidgetDefinition(widgetKind, {
|
||||||
icon: IconDeviceGamepad,
|
icon: IconDeviceGamepad,
|
||||||
options: optionsBuilder.from((factory) => ({
|
options: optionsBuilder.from((factory) => ({
|
||||||
showToggleAllButtons: factory.switch({
|
showToggleAllButtons: factory.switch({
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
|
|
||||||
|
import { widgetKind } from ".";
|
||||||
import type { WidgetProps } from "../../definition";
|
import type { WidgetProps } from "../../definition";
|
||||||
|
|
||||||
export default async function getServerDataAsync({ integrationIds }: WidgetProps<"dnsHoleControls">) {
|
export default async function getServerDataAsync({ integrationIds }: WidgetProps<typeof widgetKind>) {
|
||||||
if (integrationIds.length === 0) {
|
if (integrationIds.length === 0) {
|
||||||
return {
|
return {
|
||||||
initialData: [],
|
initialData: [],
|
||||||
@@ -13,6 +14,7 @@ export default async function getServerDataAsync({ integrationIds }: WidgetProps
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const currentDns = await api.widget.dnsHole.summary({
|
const currentDns = await api.widget.dnsHole.summary({
|
||||||
|
widgetKind,
|
||||||
integrationIds,
|
integrationIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import type { BoxProps } from "@mantine/core";
|
import type { BoxProps } from "@mantine/core";
|
||||||
import { Box, Card, Flex, Text } from "@mantine/core";
|
import { Avatar, AvatarGroup, Box, Card, Flex, Stack, Text, Tooltip } from "@mantine/core";
|
||||||
import { useElementSize } from "@mantine/hooks";
|
import { useElementSize } from "@mantine/hooks";
|
||||||
import { IconBarrierBlock, IconPercentage, IconSearch, IconWorldWww } from "@tabler/icons-react";
|
import { IconBarrierBlock, IconPercentage, IconSearch, IconWorldWww } from "@tabler/icons-react";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { formatNumber } from "@homarr/common";
|
import { formatNumber } from "@homarr/common";
|
||||||
|
import { integrationDefs } from "@homarr/definitions";
|
||||||
|
import type { DnsHoleSummary } from "@homarr/integrations/types";
|
||||||
import type { stringOrTranslation, TranslationFunction } from "@homarr/translation";
|
import type { stringOrTranslation, TranslationFunction } from "@homarr/translation";
|
||||||
import { translateIfNecessary } from "@homarr/translation";
|
import { translateIfNecessary } from "@homarr/translation";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import type { TablerIcon } from "@homarr/ui";
|
import type { TablerIcon } from "@homarr/ui";
|
||||||
|
|
||||||
|
import { widgetKind } from ".";
|
||||||
import type { WidgetComponentProps, WidgetProps } from "../../definition";
|
import type { WidgetComponentProps, WidgetProps } from "../../definition";
|
||||||
import { NoIntegrationSelectedError } from "../../errors";
|
import { NoIntegrationSelectedError } from "../../errors";
|
||||||
|
|
||||||
@@ -20,20 +24,65 @@ export default function DnsHoleSummaryWidget({
|
|||||||
options,
|
options,
|
||||||
integrationIds,
|
integrationIds,
|
||||||
serverData,
|
serverData,
|
||||||
}: WidgetComponentProps<"dnsHoleSummary">) {
|
}: WidgetComponentProps<typeof widgetKind>) {
|
||||||
const integrationId = integrationIds.at(0);
|
const [summaries, setSummaries] = useState(serverData?.initialData ?? []);
|
||||||
|
|
||||||
if (!integrationId) {
|
const t = useI18n();
|
||||||
|
|
||||||
|
clientApi.widget.dnsHole.subscribeToSummary.useSubscription(
|
||||||
|
{
|
||||||
|
widgetKind,
|
||||||
|
integrationIds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onData: (data) => {
|
||||||
|
setSummaries((prevSummaries) =>
|
||||||
|
prevSummaries.map((summary) => (summary.integration.id === data.integration.id ? data : summary)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = useMemo(
|
||||||
|
() =>
|
||||||
|
summaries
|
||||||
|
.filter(
|
||||||
|
(
|
||||||
|
pair,
|
||||||
|
): pair is {
|
||||||
|
integration: typeof pair.integration;
|
||||||
|
timestamp: typeof pair.timestamp;
|
||||||
|
summary: DnsHoleSummary;
|
||||||
|
} => pair.summary !== null && Math.abs(dayjs(pair.timestamp).diff()) < 30000,
|
||||||
|
)
|
||||||
|
.flatMap(({ summary }) => summary),
|
||||||
|
[summaries, serverData],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (integrationIds.length === 0) {
|
||||||
throw new NoIntegrationSelectedError();
|
throw new NoIntegrationSelectedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = useMemo(() => (serverData?.initialData ?? []).flatMap((summary) => summary.summary), [serverData]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box h="100%" {...boxPropsByLayout(options.layout)}>
|
<Box h="100%" {...boxPropsByLayout(options.layout)} p="2cqmin">
|
||||||
{stats.map((item, index) => (
|
{data.length > 0 ? (
|
||||||
<StatCard key={index} item={item} usePiHoleColors={options.usePiHoleColors} data={data} />
|
stats.map((item) => (
|
||||||
))}
|
<StatCard key={item.color} item={item} usePiHoleColors={options.usePiHoleColors} data={data} t={t} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Stack h="100%" w="100%" justify="center" align="center" gap="2.5cqmin" p="2.5cqmin">
|
||||||
|
<AvatarGroup spacing="10cqmin">
|
||||||
|
{summaries.map(({ integration }) => (
|
||||||
|
<Tooltip key={integration.id} label={integration.name}>
|
||||||
|
<Avatar h="35cqmin" w="35cqmin" src={integrationDefs[integration.kind].iconUrl} />
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</AvatarGroup>
|
||||||
|
<Text fz="10cqmin" ta="center">
|
||||||
|
{t("widget.dnsHoleSummary.error.integrationsDisconnected")}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -86,26 +135,26 @@ const stats = [
|
|||||||
|
|
||||||
interface StatItem {
|
interface StatItem {
|
||||||
icon: TablerIcon;
|
icon: TablerIcon;
|
||||||
value: (x: RouterOutputs["widget"]["dnsHole"]["summary"][number]["summary"][], t: TranslationFunction) => string;
|
value: (x: DnsHoleSummary[], t: TranslationFunction) => string;
|
||||||
label: stringOrTranslation;
|
label: stringOrTranslation;
|
||||||
color: string;
|
color: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StatCardProps {
|
interface StatCardProps {
|
||||||
item: StatItem;
|
item: StatItem;
|
||||||
data: RouterOutputs["widget"]["dnsHole"]["summary"][number]["summary"][];
|
data: DnsHoleSummary[];
|
||||||
usePiHoleColors: boolean;
|
usePiHoleColors: boolean;
|
||||||
|
t: TranslationFunction;
|
||||||
}
|
}
|
||||||
const StatCard = ({ item, data, usePiHoleColors }: StatCardProps) => {
|
const StatCard = ({ item, data, usePiHoleColors, t }: StatCardProps) => {
|
||||||
const { ref, height, width } = useElementSize();
|
const { ref, height, width } = useElementSize();
|
||||||
const isLong = width > height + 20;
|
const isLong = width > height + 20;
|
||||||
const t = useI18n();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="summary-card"
|
className="summary-card"
|
||||||
m="2.5cqmin"
|
m="2cqmin"
|
||||||
p="2.5cqmin"
|
p="2.5cqmin"
|
||||||
bg={usePiHoleColors ? item.color : "rgba(96, 96, 96, 0.1)"}
|
bg={usePiHoleColors ? item.color : "rgba(96, 96, 96, 0.1)"}
|
||||||
style={{
|
style={{
|
||||||
@@ -122,7 +171,7 @@ const StatCard = ({ item, data, usePiHoleColors }: StatCardProps) => {
|
|||||||
direction={isLong ? "row" : "column"}
|
direction={isLong ? "row" : "column"}
|
||||||
style={{ containerType: "size" }}
|
style={{ containerType: "size" }}
|
||||||
>
|
>
|
||||||
<item.icon className="summary-card-icon" size="50cqmin" style={{ margin: "2cqmin" }} />
|
<item.icon className="summary-card-icon" size="40cqmin" style={{ margin: "2.5cqmin" }} />
|
||||||
<Flex
|
<Flex
|
||||||
className="summary-card-texts"
|
className="summary-card-texts"
|
||||||
justify="center"
|
justify="center"
|
||||||
@@ -134,11 +183,18 @@ const StatCard = ({ item, data, usePiHoleColors }: StatCardProps) => {
|
|||||||
h="100%"
|
h="100%"
|
||||||
gap="1cqmin"
|
gap="1cqmin"
|
||||||
>
|
>
|
||||||
<Text className="summary-card-value" ta="center" size="25cqmin" fw="bold">
|
<Text
|
||||||
|
key={item.value(data, t)}
|
||||||
|
className="summary-card-value text-flash"
|
||||||
|
ta="center"
|
||||||
|
size="20cqmin"
|
||||||
|
fw="bold"
|
||||||
|
style={{ "--glow-size": "2.5cqmin" }}
|
||||||
|
>
|
||||||
{item.value(data, t)}
|
{item.value(data, t)}
|
||||||
</Text>
|
</Text>
|
||||||
{item.label && (
|
{item.label && (
|
||||||
<Text className="summary-card-label" ta="center" size="17.5cqmin">
|
<Text className="summary-card-label" ta="center" size="15cqmin">
|
||||||
{translateIfNecessary(t, item.label)}
|
{translateIfNecessary(t, item.label)}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
|||||||
import { createWidgetDefinition } from "../../definition";
|
import { createWidgetDefinition } from "../../definition";
|
||||||
import { optionsBuilder } from "../../options";
|
import { optionsBuilder } from "../../options";
|
||||||
|
|
||||||
export const { definition, componentLoader, serverDataLoader } = createWidgetDefinition("dnsHoleSummary", {
|
export const widgetKind = "dnsHoleSummary";
|
||||||
|
|
||||||
|
export const { definition, componentLoader, serverDataLoader } = createWidgetDefinition(widgetKind, {
|
||||||
icon: IconAd,
|
icon: IconAd,
|
||||||
options: optionsBuilder.from((factory) => ({
|
options: optionsBuilder.from((factory) => ({
|
||||||
usePiHoleColors: factory.switch({
|
usePiHoleColors: factory.switch({
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
|
|
||||||
|
import { widgetKind } from ".";
|
||||||
import type { WidgetProps } from "../../definition";
|
import type { WidgetProps } from "../../definition";
|
||||||
|
|
||||||
export default async function getServerDataAsync({ integrationIds }: WidgetProps<"dnsHoleSummary">) {
|
export default async function getServerDataAsync({ integrationIds }: WidgetProps<typeof widgetKind>) {
|
||||||
if (integrationIds.length === 0) {
|
if (integrationIds.length === 0) {
|
||||||
return {
|
return {
|
||||||
initialData: [],
|
initialData: [],
|
||||||
@@ -13,6 +14,7 @@ export default async function getServerDataAsync({ integrationIds }: WidgetProps
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const currentDns = await api.widget.dnsHole.summary({
|
const currentDns = await api.widget.dnsHole.summary({
|
||||||
|
widgetKind,
|
||||||
integrationIds,
|
integrationIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ import { MantineReactTable, useMantineReactTable } from "mantine-react-table";
|
|||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useIntegrationsWithInteractAccess } from "@homarr/auth/client";
|
import { useIntegrationsWithInteractAccess } from "@homarr/auth/client";
|
||||||
import { humanFileSize } from "@homarr/common";
|
import { humanFileSize } from "@homarr/common";
|
||||||
|
import type { Modify } from "@homarr/common/types";
|
||||||
import type { Integration } from "@homarr/db/schema/sqlite";
|
import type { Integration } from "@homarr/db/schema/sqlite";
|
||||||
|
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||||
import { getIconUrl, getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIconUrl, getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
import type {
|
import type {
|
||||||
DownloadClientJobsAndStatus,
|
DownloadClientJobsAndStatus,
|
||||||
@@ -97,7 +99,7 @@ export default function DownloadClientsWidget({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [currentItems, currentItemsHandlers] = useListState<{
|
const [currentItems, currentItemsHandlers] = useListState<{
|
||||||
integration: Integration;
|
integration: Modify<Integration, { kind: IntegrationKindByCategory<"downloadClient"> }>;
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
data: DownloadClientJobsAndStatus | null;
|
data: DownloadClientJobsAndStatus | null;
|
||||||
}>(
|
}>(
|
||||||
@@ -173,8 +175,13 @@ export default function DownloadClientsWidget({
|
|||||||
.filter(({ integration }) => integrationIds.includes(integration.id))
|
.filter(({ integration }) => integrationIds.includes(integration.id))
|
||||||
//Removing any integration with no data associated
|
//Removing any integration with no data associated
|
||||||
.filter(
|
.filter(
|
||||||
(pair): pair is { integration: Integration; timestamp: Date; data: DownloadClientJobsAndStatus } =>
|
(
|
||||||
pair.data != null,
|
pair,
|
||||||
|
): pair is {
|
||||||
|
integration: typeof pair.integration;
|
||||||
|
timestamp: typeof pair.timestamp;
|
||||||
|
data: DownloadClientJobsAndStatus;
|
||||||
|
} => pair.data != null,
|
||||||
)
|
)
|
||||||
//Construct normalized items list
|
//Construct normalized items list
|
||||||
.flatMap((pair) =>
|
.flatMap((pair) =>
|
||||||
|
|||||||
19
packages/widgets/src/errors/no-data-integration.tsx
Normal file
19
packages/widgets/src/errors/no-data-integration.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { IconDatabaseOff } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
import type { TranslationFunction } from "@homarr/translation";
|
||||||
|
|
||||||
|
import { ErrorBoundaryError } from "./base";
|
||||||
|
|
||||||
|
export class NoIntegrationDataError extends ErrorBoundaryError {
|
||||||
|
constructor() {
|
||||||
|
super("No integration data available");
|
||||||
|
}
|
||||||
|
|
||||||
|
public getErrorBoundaryData() {
|
||||||
|
return {
|
||||||
|
icon: IconDatabaseOff,
|
||||||
|
message: (t: TranslationFunction) => t("widget.common.error.noData"),
|
||||||
|
showLogsLink: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,5 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
import { ActionIcon, Anchor, Avatar, Badge, Card, Group, Image, ScrollArea, Stack, Text, Tooltip } from "@mantine/core";
|
||||||
ActionIcon,
|
|
||||||
Anchor,
|
|
||||||
Avatar,
|
|
||||||
Badge,
|
|
||||||
Card,
|
|
||||||
Center,
|
|
||||||
Group,
|
|
||||||
Image,
|
|
||||||
ScrollArea,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
Tooltip,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { IconThumbDown, IconThumbUp } from "@tabler/icons-react";
|
import { IconThumbDown, IconThumbUp } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
@@ -21,6 +8,8 @@ import type { ScopedTranslationFunction } from "@homarr/translation";
|
|||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../../definition";
|
import type { WidgetComponentProps } from "../../definition";
|
||||||
|
import { NoIntegrationSelectedError } from "../../errors";
|
||||||
|
import { NoIntegrationDataError } from "../../errors/no-data-integration";
|
||||||
|
|
||||||
export default function MediaServerWidget({
|
export default function MediaServerWidget({
|
||||||
integrationIds,
|
integrationIds,
|
||||||
@@ -30,7 +19,6 @@ export default function MediaServerWidget({
|
|||||||
itemId,
|
itemId,
|
||||||
}: WidgetComponentProps<"mediaRequests-requestList">) {
|
}: WidgetComponentProps<"mediaRequests-requestList">) {
|
||||||
const t = useScopedI18n("widget.mediaRequests-requestList");
|
const t = useScopedI18n("widget.mediaRequests-requestList");
|
||||||
const tCommon = useScopedI18n("common");
|
|
||||||
const isQueryEnabled = Boolean(itemId);
|
const isQueryEnabled = Boolean(itemId);
|
||||||
const { data: mediaRequests, isError: _isError } = clientApi.widget.mediaRequests.getLatestRequests.useQuery(
|
const { data: mediaRequests, isError: _isError } = clientApi.widget.mediaRequests.getLatestRequests.useQuery(
|
||||||
{
|
{
|
||||||
@@ -67,9 +55,9 @@ export default function MediaServerWidget({
|
|||||||
|
|
||||||
const { mutate: mutateRequestAnswer } = clientApi.widget.mediaRequests.answerRequest.useMutation();
|
const { mutate: mutateRequestAnswer } = clientApi.widget.mediaRequests.answerRequest.useMutation();
|
||||||
|
|
||||||
if (integrationIds.length === 0) return <Center h="100%">{tCommon("errors.noIntegration")}</Center>;
|
if (integrationIds.length === 0) throw new NoIntegrationSelectedError();
|
||||||
|
|
||||||
if (sortedMediaRequests.length === 0) return <Center h="100%">{tCommon("errors.noData")}</Center>;
|
if (sortedMediaRequests.length === 0) throw new NoIntegrationDataError();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { ActionIcon, Avatar, Card, Center, Grid, Group, Space, Stack, Text, Tooltip } from "@mantine/core";
|
import { ActionIcon, Avatar, Card, Grid, Group, Space, Stack, Text, Tooltip } from "@mantine/core";
|
||||||
import { useElementSize } from "@mantine/hooks";
|
import { useElementSize } from "@mantine/hooks";
|
||||||
import type { Icon } from "@tabler/icons-react";
|
import type { Icon } from "@tabler/icons-react";
|
||||||
import {
|
import {
|
||||||
@@ -16,10 +16,12 @@ import {
|
|||||||
import combineClasses from "clsx";
|
import combineClasses from "clsx";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
import type { RequestStats } from "@homarr/integrations/types";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import type { RequestStats } from "../../../../integrations/src/interfaces/media-requests/media-request";
|
|
||||||
import type { WidgetComponentProps } from "../../definition";
|
import type { WidgetComponentProps } from "../../definition";
|
||||||
|
import { NoIntegrationSelectedError } from "../../errors";
|
||||||
|
import { NoIntegrationDataError } from "../../errors/no-data-integration";
|
||||||
import classes from "./component.module.css";
|
import classes from "./component.module.css";
|
||||||
|
|
||||||
export default function MediaServerWidget({
|
export default function MediaServerWidget({
|
||||||
@@ -64,19 +66,9 @@ export default function MediaServerWidget({
|
|||||||
[baseData],
|
[baseData],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (integrationIds.length === 0)
|
if (integrationIds.length === 0) throw new NoIntegrationSelectedError();
|
||||||
return (
|
|
||||||
<Center ref={ref} h="100%">
|
|
||||||
{tCommon("errors.noIntegration")}
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (users.length === 0 || stats.length === 0)
|
if (users.length === 0 || stats.length === 0) throw new NoIntegrationDataError();
|
||||||
return (
|
|
||||||
<Center ref={ref} h="100%">
|
|
||||||
{tCommon("errors.noData")}
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
|
|
||||||
//Add processing and available
|
//Add processing and available
|
||||||
const data = [
|
const data = [
|
||||||
|
|||||||
@@ -34,3 +34,42 @@
|
|||||||
min-height: var(--sortButtonSize);
|
min-height: var(--sortButtonSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*Make background of component different on hover, depending on base var*/
|
||||||
|
.hoverable-component {
|
||||||
|
&:hover {
|
||||||
|
background-color: rgb(from var(--background-color) calc(r + 10) calc(g + 10) calc(b + 10) / var(--opacity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Make background of component different on click, depending on base var, inverse of hover*/
|
||||||
|
.clickable-component {
|
||||||
|
&:active {
|
||||||
|
background-color: rgb(from var(--background-color) calc(r - 10) calc(g - 10) calc(b - 10) / var(--opacity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*FadingGlowing effect for text that updates, add className and put the updating value as key*/
|
||||||
|
@keyframes glow {
|
||||||
|
from {
|
||||||
|
text-shadow: 0 0 var(--glow-size) var(--mantine-color-text);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-flash {
|
||||||
|
animation: glow 1s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*To apply to any ScrollArea that we want to flex. Same weird workaround as before*/
|
||||||
|
.flexed-scroll-area {
|
||||||
|
height: 100%;
|
||||||
|
.mantine-ScrollArea-viewport {
|
||||||
|
& div[style="min-width: 100%; display: table;"] {
|
||||||
|
display: flex !important;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
1354
pnpm-lock.yaml
generated
1354
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,10 @@
|
|||||||
# Run migrations
|
# Run migrations
|
||||||
node ./db/migrations/$DB_DIALECT/migrate.cjs ./db/migrations/$DB_DIALECT
|
if [ $DB_MIGRATIONS_DISABLED = "true" ]; then
|
||||||
|
echo "DB migrations are disabled, skipping"
|
||||||
|
else
|
||||||
|
echo "Running DB migrations"
|
||||||
|
node ./db/migrations/$DB_DIALECT/migrate.cjs ./db/migrations/$DB_DIALECT
|
||||||
|
fi
|
||||||
|
|
||||||
# Start nginx proxy
|
# Start nginx proxy
|
||||||
# 1. Replace the HOSTNAME in the nginx template file
|
# 1. Replace the HOSTNAME in the nginx template file
|
||||||
|
|||||||
@@ -21,14 +21,14 @@
|
|||||||
"eslint-config-turbo": "^2.1.2",
|
"eslint-config-turbo": "^2.1.2",
|
||||||
"eslint-plugin-import": "^2.30.0",
|
"eslint-plugin-import": "^2.30.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.0",
|
"eslint-plugin-jsx-a11y": "^6.10.0",
|
||||||
"eslint-plugin-react": "^7.36.1",
|
"eslint-plugin-react": "^7.37.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"typescript-eslint": "^8.6.0"
|
"typescript-eslint": "^8.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.11.0",
|
"eslint": "^9.11.1",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config"
|
"prettier": "@homarr/prettier-config"
|
||||||
|
|||||||
Reference in New Issue
Block a user