Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
168a951ad9 |
No files matched your search
Vendored
-5
@@ -14,11 +14,6 @@
|
|||||||
"gridstack",
|
"gridstack",
|
||||||
"homarr",
|
"homarr",
|
||||||
"jellyfin",
|
"jellyfin",
|
||||||
"llen",
|
|
||||||
"lpop",
|
|
||||||
"lpush",
|
|
||||||
"lrange",
|
|
||||||
"ltrim",
|
|
||||||
"mantine",
|
"mantine",
|
||||||
"manuel-rw",
|
"manuel-rw",
|
||||||
"Meierschlumpf",
|
"Meierschlumpf",
|
||||||
|
|||||||
@@ -45,9 +45,9 @@
|
|||||||
"@million/lint": "1.0.13",
|
"@million/lint": "1.0.13",
|
||||||
"@t3-oss/env-nextjs": "^0.11.1",
|
"@t3-oss/env-nextjs": "^0.11.1",
|
||||||
"@tabler/icons-react": "^3.24.0",
|
"@tabler/icons-react": "^3.24.0",
|
||||||
"@tanstack/react-query": "^5.62.3",
|
"@tanstack/react-query": "^5.62.2",
|
||||||
"@tanstack/react-query-devtools": "^5.62.3",
|
"@tanstack/react-query-devtools": "^5.62.2",
|
||||||
"@tanstack/react-query-next-experimental": "5.62.3",
|
"@tanstack/react-query-next-experimental": "5.62.2",
|
||||||
"@trpc/client": "next",
|
"@trpc/client": "next",
|
||||||
"@trpc/next": "next",
|
"@trpc/next": "next",
|
||||||
"@trpc/react-query": "next",
|
"@trpc/react-query": "next",
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
"react-error-boundary": "^4.1.2",
|
"react-error-boundary": "^4.1.2",
|
||||||
"react-simple-code-editor": "^0.14.1",
|
"react-simple-code-editor": "^0.14.1",
|
||||||
"sass": "^1.82.0",
|
"sass": "^1.82.0",
|
||||||
"superjson": "2.2.2",
|
"superjson": "2.2.1",
|
||||||
"swagger-ui-react": "^5.18.2",
|
"swagger-ui-react": "^5.18.2",
|
||||||
"use-deep-compare-effect": "^1.8.1"
|
"use-deep-compare-effect": "^1.8.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ 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";
|
||||||
import { Notifications } from "@homarr/notifications";
|
import { Notifications } from "@homarr/notifications";
|
||||||
import { SpotlightProvider } from "@homarr/spotlight";
|
|
||||||
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
|
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
|
||||||
import { getI18nMessages } from "@homarr/translation/server";
|
import { getI18nMessages } from "@homarr/translation/server";
|
||||||
|
|
||||||
@@ -83,7 +82,6 @@ export default async function Layout(props: { children: React.ReactNode; params:
|
|||||||
(innerProps) => <NextIntlClientProvider {...innerProps} messages={i18nMessages} />,
|
(innerProps) => <NextIntlClientProvider {...innerProps} messages={i18nMessages} />,
|
||||||
(innerProps) => <CustomMantineProvider {...innerProps} defaultColorScheme={colorScheme} />,
|
(innerProps) => <CustomMantineProvider {...innerProps} defaultColorScheme={colorScheme} />,
|
||||||
(innerProps) => <ModalProvider {...innerProps} />,
|
(innerProps) => <ModalProvider {...innerProps} />,
|
||||||
(innerProps) => <SpotlightProvider {...innerProps} />,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
import { Indicator, UnstyledButton } from "@mantine/core";
|
import { UnstyledButton } from "@mantine/core";
|
||||||
|
|
||||||
import { api } from "@homarr/api/server";
|
|
||||||
import { auth } from "@homarr/auth/next";
|
|
||||||
|
|
||||||
import { CurrentUserAvatar } from "~/components/user-avatar";
|
import { CurrentUserAvatar } from "~/components/user-avatar";
|
||||||
import { UserAvatarMenu } from "~/components/user-avatar-menu";
|
import { UserAvatarMenu } from "~/components/user-avatar-menu";
|
||||||
|
|
||||||
export const UserButton = async () => {
|
export const UserButton = () => {
|
||||||
const data = await api.updateChecker.getAvailableUpdates();
|
|
||||||
const session = await auth();
|
|
||||||
const isAdmin = session?.user.permissions.includes("admin");
|
|
||||||
return (
|
return (
|
||||||
<UserAvatarMenu availableUpdates={isAdmin ? data : undefined}>
|
<UserAvatarMenu>
|
||||||
<UnstyledButton>
|
<UnstyledButton>
|
||||||
<Indicator disabled={!data || data.length === 0 || !isAdmin} size={15} processing withBorder>
|
<CurrentUserAvatar size="md" />
|
||||||
<CurrentUserAvatar size="md" />
|
|
||||||
</Indicator>
|
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
</UserAvatarMenu>
|
</UserAvatarMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { useRouter } from "next/navigation";
|
|||||||
import { Center, Menu, Stack, Text, useMantineColorScheme } from "@mantine/core";
|
import { Center, Menu, Stack, Text, useMantineColorScheme } from "@mantine/core";
|
||||||
import { useHotkeys, useTimeout } from "@mantine/hooks";
|
import { useHotkeys, useTimeout } from "@mantine/hooks";
|
||||||
import {
|
import {
|
||||||
IconBellRinging,
|
|
||||||
IconCheck,
|
IconCheck,
|
||||||
IconHome,
|
IconHome,
|
||||||
IconLogin,
|
IconLogin,
|
||||||
@@ -24,17 +23,14 @@ import { useScopedI18n } from "@homarr/translation/client";
|
|||||||
|
|
||||||
import "flag-icons/css/flag-icons.min.css";
|
import "flag-icons/css/flag-icons.min.css";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
|
||||||
|
|
||||||
import { useAuthContext } from "~/app/[locale]/_client-providers/session";
|
import { useAuthContext } from "~/app/[locale]/_client-providers/session";
|
||||||
import { CurrentLanguageCombobox } from "./language/current-language-combobox";
|
import { CurrentLanguageCombobox } from "./language/current-language-combobox";
|
||||||
|
|
||||||
interface UserAvatarMenuProps {
|
interface UserAvatarMenuProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
availableUpdates: RouterOutputs["updateChecker"]["getAvailableUpdates"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserAvatarMenu = ({ children, availableUpdates }: UserAvatarMenuProps) => {
|
export const UserAvatarMenu = ({ children }: UserAvatarMenuProps) => {
|
||||||
const t = useScopedI18n("common.userAvatar.menu");
|
const t = useScopedI18n("common.userAvatar.menu");
|
||||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||||
useHotkeys([["mod+J", toggleColorScheme]]);
|
useHotkeys([["mod+J", toggleColorScheme]]);
|
||||||
@@ -68,21 +64,6 @@ export const UserAvatarMenu = ({ children, availableUpdates }: UserAvatarMenuPro
|
|||||||
// We use keepMounted so we can add event listeners to prevent navigating away without saving the board
|
// We use keepMounted so we can add event listeners to prevent navigating away without saving the board
|
||||||
<Menu width={300} withArrow withinPortal keepMounted>
|
<Menu width={300} withArrow withinPortal keepMounted>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
{availableUpdates && availableUpdates.length > 0 && availableUpdates[0] && (
|
|
||||||
<>
|
|
||||||
<Menu.Item
|
|
||||||
component={"a"}
|
|
||||||
href={availableUpdates[0].url}
|
|
||||||
target="_blank"
|
|
||||||
leftSection={<IconBellRinging size="1rem" />}
|
|
||||||
>
|
|
||||||
<Text fw="bold" size="sm">
|
|
||||||
{t("updateAvailable", { countUpdates: availableUpdates.length, tag: availableUpdates[0].tag_name })}
|
|
||||||
</Text>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Divider />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Menu.Item onClick={toggleColorScheme} leftSection={<ColorSchemeIcon size="1rem" />}>
|
<Menu.Item onClick={toggleColorScheme} leftSection={<ColorSchemeIcon size="1rem" />}>
|
||||||
{colorSchemeText}
|
{colorSchemeText}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
"@homarr/widgets": "workspace:^0.1.0",
|
"@homarr/widgets": "workspace:^0.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"superjson": "2.2.2",
|
"superjson": "2.2.1",
|
||||||
"undici": "7.1.0"
|
"undici": "7.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+3
-5
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homarr",
|
"name": "homarr",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env CI=true turbo build",
|
"build": "cross-env CI=true turbo build",
|
||||||
"clean": "git clean -xdf node_modules",
|
"clean": "git clean -xdf node_modules",
|
||||||
@@ -40,17 +39,16 @@
|
|||||||
"testcontainers": "^10.16.0",
|
"testcontainers": "^10.16.0",
|
||||||
"turbo": "^2.3.3",
|
"turbo": "^2.3.3",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.3",
|
||||||
"vitest": "^2.1.8"
|
"vitest": "^2.1.8"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.15.0",
|
"packageManager": "pnpm@9.14.4",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.12.0"
|
"node": ">=22.12.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"pretty-print-error": "patches/pretty-print-error.patch"
|
"pretty-print-error": "patches/pretty-print-error.patch"
|
||||||
},
|
}
|
||||||
"allowNonAppliedPatches": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@umami/node": "^0.4.0",
|
"@umami/node": "^0.4.0",
|
||||||
"superjson": "2.2.2"
|
"superjson": "2.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
"dockerode": "^4.0.2",
|
"dockerode": "^4.0.2",
|
||||||
"next": "^14.2.20",
|
"next": "^14.2.20",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"superjson": "2.2.2",
|
"superjson": "2.2.1",
|
||||||
"trpc-to-openapi": "^2.1.0"
|
"trpc-to-openapi": "^2.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { logRouter } from "./router/log";
|
|||||||
import { mediaRouter } from "./router/medias/media-router";
|
import { mediaRouter } from "./router/medias/media-router";
|
||||||
import { searchEngineRouter } from "./router/search-engine/search-engine-router";
|
import { searchEngineRouter } from "./router/search-engine/search-engine-router";
|
||||||
import { serverSettingsRouter } from "./router/serverSettings";
|
import { serverSettingsRouter } from "./router/serverSettings";
|
||||||
import { updateCheckerRouter } from "./router/update-checker";
|
|
||||||
import { userRouter } from "./router/user";
|
import { userRouter } from "./router/user";
|
||||||
import { widgetRouter } from "./router/widgets";
|
import { widgetRouter } from "./router/widgets";
|
||||||
import { createTRPCRouter } from "./trpc";
|
import { createTRPCRouter } from "./trpc";
|
||||||
@@ -36,7 +35,6 @@ export const appRouter = createTRPCRouter({
|
|||||||
cronJobs: cronJobsRouter,
|
cronJobs: cronJobsRouter,
|
||||||
apiKeys: apiKeysRouter,
|
apiKeys: apiKeysRouter,
|
||||||
media: mediaRouter,
|
media: mediaRouter,
|
||||||
updateChecker: updateCheckerRouter,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
|
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
|
||||||
|
|
||||||
export const updateCheckerRouter = createTRPCRouter({
|
|
||||||
getAvailableUpdates: protectedProcedure.query(async () => {
|
|
||||||
const handler = updateCheckerRequestHandler.handler({});
|
|
||||||
const data = await handler.getCachedOrUpdatedDataAsync({});
|
|
||||||
return data.data.availableUpdates;
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
|
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
|
||||||
import type { HealthMonitoring } from "@homarr/integrations";
|
import type { HealthMonitoring } from "@homarr/integrations";
|
||||||
import { systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";
|
import { systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";
|
||||||
|
|
||||||
@@ -9,7 +8,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
|
|||||||
|
|
||||||
export const healthMonitoringRouter = createTRPCRouter({
|
export const healthMonitoringRouter = createTRPCRouter({
|
||||||
getHealthStatus: publicProcedure
|
getHealthStatus: publicProcedure
|
||||||
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("healthMonitoring")))
|
.unstable_concat(createManyIntegrationMiddleware("query", "openmediavault"))
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
ctx.integrations.map(async (integration) => {
|
ctx.integrations.map(async (integration) => {
|
||||||
@@ -27,7 +26,7 @@ export const healthMonitoringRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
subscribeHealthStatus: publicProcedure
|
subscribeHealthStatus: publicProcedure
|
||||||
.unstable_concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("healthMonitoring")))
|
.unstable_concat(createManyIntegrationMiddleware("query", "openmediavault"))
|
||||||
.subscription(({ ctx }) => {
|
.subscription(({ ctx }) => {
|
||||||
return observable<{ integrationId: string; healthInfo: HealthMonitoring; timestamp: Date }>((emit) => {
|
return observable<{ integrationId: string; healthInfo: HealthMonitoring; timestamp: Date }>((emit) => {
|
||||||
const unsubscribes: (() => void)[] = [];
|
const unsubscribes: (() => void)[] = [];
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"next": "^14.2.20",
|
"next": "^14.2.20",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"tldts": "^6.1.66"
|
"tldts": "^6.1.65"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { parseAppHrefWithVariables } from "./base";
|
import { parseAppHrefWithVariables } from "./base";
|
||||||
|
|
||||||
export const parseAppHrefWithVariablesClient = <TInput extends string | null>(url: TInput): TInput => {
|
export const parseAppHrefWithVariablesClient = <TInput extends string | null>(url: TInput): TInput => {
|
||||||
if (typeof window === "undefined") return url;
|
|
||||||
return parseAppHrefWithVariables(url, window.location.href);
|
return parseAppHrefWithVariables(url, window.location.href);
|
||||||
};
|
};
|
||||||
@@ -38,9 +38,7 @@
|
|||||||
"@homarr/request-handler": "workspace:^0.1.0",
|
"@homarr/request-handler": "workspace:^0.1.0",
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "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"
|
||||||
"octokit": "^4.0.2",
|
|
||||||
"semver-parser": "^4.1.7"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import { pingJob } from "./jobs/ping";
|
|||||||
import type { RssFeed } from "./jobs/rss-feeds";
|
import type { RssFeed } from "./jobs/rss-feeds";
|
||||||
import { rssFeedsJob } from "./jobs/rss-feeds";
|
import { rssFeedsJob } from "./jobs/rss-feeds";
|
||||||
import { sessionCleanupJob } from "./jobs/session-cleanup";
|
import { sessionCleanupJob } from "./jobs/session-cleanup";
|
||||||
import { updateCheckerJob } from "./jobs/update-checker";
|
|
||||||
import { createCronJobGroup } from "./lib";
|
import { createCronJobGroup } from "./lib";
|
||||||
|
import {notificationsFetcherJob} from "./jobs/notifications-fetcher";
|
||||||
|
|
||||||
export const jobGroup = createCronJobGroup({
|
export const jobGroup = createCronJobGroup({
|
||||||
analytics: analyticsJob,
|
analytics: analyticsJob,
|
||||||
@@ -30,7 +30,7 @@ export const jobGroup = createCronJobGroup({
|
|||||||
indexerManager: indexerManagerJob,
|
indexerManager: indexerManagerJob,
|
||||||
healthMonitoring: healthMonitoringJob,
|
healthMonitoring: healthMonitoringJob,
|
||||||
sessionCleanup: sessionCleanupJob,
|
sessionCleanup: sessionCleanupJob,
|
||||||
updateChecker: updateCheckerJob,
|
"notifications-fetcher": notificationsFetcherJob
|
||||||
});
|
});
|
||||||
|
|
||||||
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
|
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import {createCronJob} from "../lib";
|
||||||
|
import {EVERY_HOUR} from "@homarr/cron-jobs-core/expressions";
|
||||||
|
|
||||||
|
export const notificationsFetcherJob = createCronJob("notifications-fetcher", EVERY_HOUR, {
|
||||||
|
runOnStart: true
|
||||||
|
}).withCallback(async () => {
|
||||||
|
|
||||||
|
});
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { EVERY_HOUR } from "@homarr/cron-jobs-core/expressions";
|
|
||||||
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
|
|
||||||
|
|
||||||
import { createCronJob } from "../lib";
|
|
||||||
|
|
||||||
export const updateCheckerJob = createCronJob("updateChecker", EVERY_HOUR, {
|
|
||||||
runOnStart: true,
|
|
||||||
}).withCallback(async () => {
|
|
||||||
const handler = updateCheckerRequestHandler.handler({});
|
|
||||||
await handler.getCachedOrUpdatedDataAsync({
|
|
||||||
forceUpdate: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -354,6 +354,20 @@ export const icons = sqliteTable("icon", {
|
|||||||
.references(() => iconRepositories.id, { onDelete: "cascade" }),
|
.references(() => iconRepositories.id, { onDelete: "cascade" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const notifications = sqliteTable("notifications", {
|
||||||
|
id: text().notNull().primaryKey(),
|
||||||
|
createdAt: int({ mode: "timestamp" }).notNull(),
|
||||||
|
dismissAfterOneAcknowledgement: int({ mode: "boolean" }).default(false).notNull(),
|
||||||
|
content: text().notNull()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const notificationsAcknowledgements = sqliteTable("notificationAcknowledgements", {
|
||||||
|
id: text().notNull().primaryKey(),
|
||||||
|
acknowledgedAt: int({ mode: "timestamp" }).notNull(),
|
||||||
|
userId: text().notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
notificationId: text().notNull().references(() => notifications.id, { onDelete: "cascade" }),
|
||||||
|
});
|
||||||
|
|
||||||
export const iconRepositories = sqliteTable("iconRepository", {
|
export const iconRepositories = sqliteTable("iconRepository", {
|
||||||
id: text().notNull().primaryKey(),
|
id: text().notNull().primaryKey(),
|
||||||
slug: text().notNull(),
|
slug: text().notNull(),
|
||||||
|
|||||||
@@ -144,13 +144,6 @@ export const integrationDefs = {
|
|||||||
category: ["healthMonitoring"],
|
category: ["healthMonitoring"],
|
||||||
supportsSearch: false,
|
supportsSearch: false,
|
||||||
},
|
},
|
||||||
dashDot: {
|
|
||||||
name: "Dash.",
|
|
||||||
secretKinds: [[]],
|
|
||||||
category: ["healthMonitoring"],
|
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/dashdot.png",
|
|
||||||
supportsSearch: false,
|
|
||||||
},
|
|
||||||
} as const satisfies Record<string, integrationDefinition>;
|
} as const satisfies Record<string, integrationDefinition>;
|
||||||
|
|
||||||
export const integrationKinds = objectKeys(integrationDefs) as AtLeastOneOf<IntegrationKind>;
|
export const integrationKinds = objectKeys(integrationDefs) as AtLeastOneOf<IntegrationKind>;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type { Integration as DbIntegration } from "@homarr/db/schema/sqlite";
|
|||||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||||
|
|
||||||
import { AdGuardHomeIntegration } from "../adguard-home/adguard-home-integration";
|
import { AdGuardHomeIntegration } from "../adguard-home/adguard-home-integration";
|
||||||
import { DashDotIntegration } from "../dashdot/dashdot-integration";
|
|
||||||
import { DelugeIntegration } from "../download-client/deluge/deluge-integration";
|
import { DelugeIntegration } from "../download-client/deluge/deluge-integration";
|
||||||
import { NzbGetIntegration } from "../download-client/nzbget/nzbget-integration";
|
import { NzbGetIntegration } from "../download-client/nzbget/nzbget-integration";
|
||||||
import { QBitTorrentIntegration } from "../download-client/qbittorrent/qbittorrent-integration";
|
import { QBitTorrentIntegration } from "../download-client/qbittorrent/qbittorrent-integration";
|
||||||
@@ -69,5 +68,4 @@ export const integrationCreators = {
|
|||||||
openmediavault: OpenMediaVaultIntegration,
|
openmediavault: OpenMediaVaultIntegration,
|
||||||
lidarr: LidarrIntegration,
|
lidarr: LidarrIntegration,
|
||||||
readarr: ReadarrIntegration,
|
readarr: ReadarrIntegration,
|
||||||
dashDot: DashDotIntegration,
|
|
||||||
} satisfies Record<IntegrationKind, new (integration: IntegrationInput) => Integration>;
|
} satisfies Record<IntegrationKind, new (integration: IntegrationInput) => Integration>;
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import { humanFileSize } from "@homarr/common";
|
|
||||||
|
|
||||||
import "@homarr/redis";
|
|
||||||
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
|
|
||||||
import { z } from "@homarr/validation";
|
|
||||||
|
|
||||||
import { createChannelEventHistory } from "../../../redis/src/lib/channel";
|
|
||||||
import { Integration } from "../base/integration";
|
|
||||||
import type { HealthMonitoring } from "../types";
|
|
||||||
|
|
||||||
export class DashDotIntegration extends Integration {
|
|
||||||
public async testConnectionAsync(): Promise<void> {
|
|
||||||
const response = await fetch(this.url("/info"));
|
|
||||||
await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getSystemInfoAsync(): Promise<HealthMonitoring> {
|
|
||||||
const info = await this.getInfoAsync();
|
|
||||||
const cpuLoad = await this.getCurrentCpuLoadAsync();
|
|
||||||
const memoryLoad = await this.getCurrentMemoryLoadAsync();
|
|
||||||
const storageLoad = await this.getCurrentStorageLoadAsync();
|
|
||||||
|
|
||||||
const channel = this.getChannel();
|
|
||||||
const history = await channel.getSliceUntilTimeAsync(dayjs().subtract(15, "minutes").toDate());
|
|
||||||
|
|
||||||
return {
|
|
||||||
cpuUtilization: cpuLoad.sumLoad,
|
|
||||||
memUsed: `${memoryLoad.loadInBytes}`,
|
|
||||||
memAvailable: `${info.maxAvailableMemoryBytes - memoryLoad.loadInBytes}`,
|
|
||||||
fileSystem: info.storage.map((storage, index) => ({
|
|
||||||
deviceName: `Storage ${index + 1}: (${storage.disks.map((disk) => disk.device).join(", ")})`,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
used: humanFileSize(storageLoad[index]!),
|
|
||||||
available: `${storage.size}`,
|
|
||||||
percentage: storageLoad[index] ? (storageLoad[index] / storage.size) * 100 : 0,
|
|
||||||
})),
|
|
||||||
cpuModelName: info.cpuModel === "" ? `Unknown Model (${info.cpuBrand})` : `${info.cpuModel} (${info.cpuBrand})`,
|
|
||||||
cpuTemp: cpuLoad.averageTemperature,
|
|
||||||
availablePkgUpdates: 0,
|
|
||||||
rebootRequired: false,
|
|
||||||
smart: [],
|
|
||||||
uptime: info.uptime,
|
|
||||||
version: `${info.operatingSystemVersion}`,
|
|
||||||
loadAverage: {
|
|
||||||
"1min": Math.round(this.getAverageOfCpu(history[0])),
|
|
||||||
"5min": Math.round(this.getAverageOfCpuFlat(history.slice(0, 4))),
|
|
||||||
"15min": Math.round(this.getAverageOfCpuFlat(history.slice(0, 14))),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getInfoAsync() {
|
|
||||||
const infoResponse = await fetch(this.url("/info"));
|
|
||||||
const serverInfo = await internalServerInfoApi.parseAsync(await infoResponse.json());
|
|
||||||
return {
|
|
||||||
maxAvailableMemoryBytes: serverInfo.ram.size,
|
|
||||||
storage: serverInfo.storage,
|
|
||||||
cpuBrand: serverInfo.cpu.brand,
|
|
||||||
cpuModel: serverInfo.cpu.model,
|
|
||||||
operatingSystemVersion: `${serverInfo.os.distro} ${serverInfo.os.release} (${serverInfo.os.kernel})`,
|
|
||||||
uptime: serverInfo.os.uptime,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getCurrentCpuLoadAsync() {
|
|
||||||
const channel = this.getChannel();
|
|
||||||
const cpu = await fetch(this.url("/load/cpu"));
|
|
||||||
const data = await cpuLoadPerCoreApiList.parseAsync(await cpu.json());
|
|
||||||
await channel.pushAsync(data);
|
|
||||||
return {
|
|
||||||
sumLoad: this.getAverageOfCpu(data),
|
|
||||||
averageTemperature: data.reduce((acc, current) => acc + current.temp, 0) / data.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private getAverageOfCpuFlat(cpuLoad: z.infer<typeof cpuLoadPerCoreApiList>[]) {
|
|
||||||
const averages = cpuLoad.map((load) => this.getAverageOfCpu(load));
|
|
||||||
return averages.reduce((acc, current) => acc + current, 0) / averages.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getAverageOfCpu(cpuLoad?: z.infer<typeof cpuLoadPerCoreApiList>) {
|
|
||||||
if (!cpuLoad) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return cpuLoad.reduce((acc, current) => acc + current.load, 0) / cpuLoad.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getCurrentStorageLoadAsync() {
|
|
||||||
const storageLoad = await fetch(this.url("/load/storage"));
|
|
||||||
return (await storageLoad.json()) as number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getCurrentMemoryLoadAsync() {
|
|
||||||
const memoryLoad = await fetch(this.url("/load/ram"));
|
|
||||||
const data = await memoryLoadApi.parseAsync(await memoryLoad.json());
|
|
||||||
return {
|
|
||||||
loadInBytes: data.load,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private getChannel() {
|
|
||||||
return createChannelEventHistory<z.infer<typeof cpuLoadPerCoreApiList>>(
|
|
||||||
`integration:${this.integration.id}:history:cpu`,
|
|
||||||
100,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cpuLoadPerCoreApi = z.object({
|
|
||||||
load: z.number().min(0),
|
|
||||||
temp: z.number().min(0),
|
|
||||||
});
|
|
||||||
|
|
||||||
const memoryLoadApi = z.object({
|
|
||||||
load: z.number().min(0),
|
|
||||||
});
|
|
||||||
|
|
||||||
const internalServerInfoApi = z.object({
|
|
||||||
os: z.object({
|
|
||||||
distro: z.string(),
|
|
||||||
kernel: z.string(),
|
|
||||||
release: z.string(),
|
|
||||||
uptime: z.number().min(0),
|
|
||||||
}),
|
|
||||||
cpu: z.object({
|
|
||||||
brand: z.string(),
|
|
||||||
model: z.string(),
|
|
||||||
}),
|
|
||||||
ram: z.object({
|
|
||||||
size: z.number().min(0),
|
|
||||||
}),
|
|
||||||
storage: z.array(
|
|
||||||
z.object({
|
|
||||||
size: z.number().min(0),
|
|
||||||
disks: z.array(
|
|
||||||
z.object({
|
|
||||||
device: z.string(),
|
|
||||||
brand: z.string(),
|
|
||||||
type: z.string(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
const cpuLoadPerCoreApiList = z.array(cpuLoadPerCoreApi);
|
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ioredis": "5.4.1",
|
"ioredis": "5.4.1",
|
||||||
"superjson": "2.2.2",
|
"superjson": "2.2.1",
|
||||||
"winston": "3.17.0"
|
"winston": "3.17.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@homarr/old-schema": "workspace:^0.1.0",
|
"@homarr/old-schema": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"superjson": "2.2.2"
|
"superjson": "2.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
"@homarr/definitions": "workspace:^",
|
"@homarr/definitions": "workspace:^",
|
||||||
"@homarr/log": "workspace:^",
|
"@homarr/log": "workspace:^",
|
||||||
"ioredis": "5.4.1",
|
"ioredis": "5.4.1",
|
||||||
"superjson": "2.2.2"
|
"superjson": "2.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export {
|
|||||||
createIntegrationOptionsChannel,
|
createIntegrationOptionsChannel,
|
||||||
createChannelWithLatestAndEvents,
|
createChannelWithLatestAndEvents,
|
||||||
handshakeAsync,
|
handshakeAsync,
|
||||||
createSubPubChannel,
|
|
||||||
} from "./lib/channel";
|
} from "./lib/channel";
|
||||||
|
|
||||||
export const exampleChannel = createSubPubChannel<{ message: string }>("example");
|
export const exampleChannel = createSubPubChannel<{ message: string }>("example");
|
||||||
|
|||||||
@@ -187,67 +187,6 @@ export const createItemChannel = <TData>(itemId: string) => {
|
|||||||
return createChannelWithLatestAndEvents<TData>(`item:${itemId}`);
|
return createChannelWithLatestAndEvents<TData>(`item:${itemId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createChannelEventHistory = <TData>(channelName: string, maxElements = 15) => {
|
|
||||||
const popElementsOverMaxAsync = async () => {
|
|
||||||
const length = await getSetClient.llen(channelName);
|
|
||||||
if (length <= maxElements) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await getSetClient.ltrim(channelName, length - maxElements, length);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe: (callback: (data: TData) => void) => {
|
|
||||||
return ChannelSubscriptionTracker.subscribe(channelName, (message) => {
|
|
||||||
callback(superjson.parse(message));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
publishAndPushAsync: async (data: TData) => {
|
|
||||||
await publisher.publish(channelName, superjson.stringify(data));
|
|
||||||
await getSetClient.lpush(channelName, superjson.stringify({ data, timestamp: new Date() }));
|
|
||||||
await popElementsOverMaxAsync();
|
|
||||||
},
|
|
||||||
pushAsync: async (data: TData) => {
|
|
||||||
await getSetClient.lpush(channelName, superjson.stringify({ data, timestamp: new Date() }));
|
|
||||||
await popElementsOverMaxAsync();
|
|
||||||
},
|
|
||||||
clearAsync: async () => {
|
|
||||||
await getSetClient.del(channelName);
|
|
||||||
},
|
|
||||||
getLastAsync: async () => {
|
|
||||||
const length = await getSetClient.llen(channelName);
|
|
||||||
const data = await getSetClient.lrange(channelName, length - 1, length);
|
|
||||||
if (data.length !== 1) return null;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
return superjson.parse<{ data: TData; timestamp: Date }>(data[0]!);
|
|
||||||
},
|
|
||||||
getSliceAsync: async (startIndex: number, endIndex: number) => {
|
|
||||||
const range = await getSetClient.lrange(channelName, startIndex, endIndex);
|
|
||||||
return range.map((item) => superjson.parse<{ data: TData; timestamp: Date }>(item));
|
|
||||||
},
|
|
||||||
getSliceUntilTimeAsync: async (time: Date) => {
|
|
||||||
const length = await getSetClient.llen(channelName);
|
|
||||||
const items: TData[] = [];
|
|
||||||
const itemsInCollection = await getSetClient.lrange(channelName, 0, length - 1);
|
|
||||||
|
|
||||||
for (let i = 0; i < length - 1; i++) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const deserializedItem = superjson.parse<{ data: TData; timestamp: Date }>(itemsInCollection[i]!);
|
|
||||||
if (deserializedItem.timestamp < time) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
items.push(deserializedItem.data);
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
},
|
|
||||||
getLengthAsync: async () => {
|
|
||||||
return await getSetClient.llen(channelName);
|
|
||||||
},
|
|
||||||
name: channelName,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createChannelWithLatestAndEvents = <TData>(channelName: string) => {
|
export const createChannelWithLatestAndEvents = <TData>(channelName: string) => {
|
||||||
return {
|
return {
|
||||||
subscribe: (callback: (data: TData) => void) => {
|
subscribe: (callback: (data: TData) => void) => {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"pretty-print-error": "^1.1.2",
|
"pretty-print-error": "^1.1.2",
|
||||||
"superjson": "2.2.2"
|
"superjson": "2.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import { Octokit } from "octokit";
|
|
||||||
import { compareSemVer, isValidSemVer } from "semver-parser";
|
|
||||||
|
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
import { createChannelWithLatestAndEvents } from "@homarr/redis";
|
|
||||||
import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler";
|
|
||||||
|
|
||||||
import packageJson from "../../../package.json";
|
|
||||||
|
|
||||||
export const updateCheckerRequestHandler = createCachedRequestHandler({
|
|
||||||
queryKey: "",
|
|
||||||
cacheDuration: dayjs.duration(1, "hour"),
|
|
||||||
async requestAsync(_) {
|
|
||||||
const octokit = new Octokit();
|
|
||||||
const releases = await octokit.rest.repos.listReleases({
|
|
||||||
owner: "homarr-labs",
|
|
||||||
repo: "homarr",
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentVersion = (packageJson as { version: string }).version;
|
|
||||||
const availableReleases = [];
|
|
||||||
|
|
||||||
for (const release of releases.data) {
|
|
||||||
if (!isValidSemVer(release.tag_name)) {
|
|
||||||
logger.warn(`Unable to parse semantic tag '${release.tag_name}'. Update check might not work.`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
availableReleases.push(release);
|
|
||||||
}
|
|
||||||
|
|
||||||
const availableNewerReleases = availableReleases
|
|
||||||
.filter((release) => compareSemVer(release.tag_name, currentVersion) > 0)
|
|
||||||
.sort((releaseA, releaseB) => compareSemVer(releaseB.tag_name, releaseA.tag_name));
|
|
||||||
if (availableReleases.length > 0) {
|
|
||||||
logger.info(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
`Update checker found a new available version: ${availableReleases[0]!.tag_name}. Current version is ${currentVersion}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.debug(`Update checker did not find any available updates. Current version is ${currentVersion}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
availableUpdates: availableNewerReleases.map((release) => ({
|
|
||||||
name: release.name,
|
|
||||||
contentHtml: release.body_html,
|
|
||||||
url: release.html_url,
|
|
||||||
tag_name: release.tag_name,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
createRedisChannel() {
|
|
||||||
return createChannelWithLatestAndEvents<{
|
|
||||||
availableUpdates: { name: string | null; contentHtml?: string; url: string; tag_name: string }[];
|
|
||||||
}>("homarr:update");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -34,10 +34,6 @@ export const SpotlightGroupActions = <TOption extends Record<string, unknown>>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (Array.isArray(options)) {
|
if (Array.isArray(options)) {
|
||||||
if (options.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredOptions = options
|
const filteredOptions = options
|
||||||
.filter((option) => ("filter" in group ? group.filter(query, option) : false))
|
.filter((option) => ("filter" in group ? group.filter(query, option) : false))
|
||||||
.sort((optionA, optionB) => {
|
.sort((optionA, optionB) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useMemo, useRef, useState } from "react";
|
||||||
import { ActionIcon, Center, Group, Kbd } from "@mantine/core";
|
import { ActionIcon, Center, Group, Kbd } from "@mantine/core";
|
||||||
import { Spotlight as MantineSpotlight } from "@mantine/spotlight";
|
import { Spotlight as MantineSpotlight } from "@mantine/spotlight";
|
||||||
import { IconSearch, IconX } from "@tabler/icons-react";
|
import { IconSearch, IconX } from "@tabler/icons-react";
|
||||||
@@ -12,7 +12,6 @@ import { useI18n } from "@homarr/translation/client";
|
|||||||
import type { inferSearchInteractionOptions } from "../lib/interaction";
|
import type { inferSearchInteractionOptions } from "../lib/interaction";
|
||||||
import type { SearchMode } from "../lib/mode";
|
import type { SearchMode } from "../lib/mode";
|
||||||
import { searchModes } from "../modes";
|
import { searchModes } from "../modes";
|
||||||
import { useSpotlightContextResults } from "../modes/home/context";
|
|
||||||
import { selectAction, spotlightStore } from "../spotlight-store";
|
import { selectAction, spotlightStore } from "../spotlight-store";
|
||||||
import { SpotlightChildrenActions } from "./actions/children-actions";
|
import { SpotlightChildrenActions } from "./actions/children-actions";
|
||||||
import { SpotlightActionGroups } from "./actions/groups/action-group";
|
import { SpotlightActionGroups } from "./actions/groups/action-group";
|
||||||
@@ -20,45 +19,24 @@ import { SpotlightActionGroups } from "./actions/groups/action-group";
|
|||||||
type SearchModeKey = keyof TranslationObject["search"]["mode"];
|
type SearchModeKey = keyof TranslationObject["search"]["mode"];
|
||||||
|
|
||||||
export const Spotlight = () => {
|
export const Spotlight = () => {
|
||||||
const items = useSpotlightContextResults();
|
const searchModeState = useState<SearchModeKey>("help");
|
||||||
// We fallback to help if no context results are available
|
|
||||||
const defaultMode = items.length >= 1 ? "home" : "help";
|
|
||||||
const searchModeState = useState<SearchModeKey>(defaultMode);
|
|
||||||
const mode = searchModeState[0];
|
const mode = searchModeState[0];
|
||||||
const activeMode = useMemo(() => searchModes.find((searchMode) => searchMode.modeKey === mode), [mode]);
|
const activeMode = useMemo(() => searchModes.find((searchMode) => searchMode.modeKey === mode), [mode]);
|
||||||
|
|
||||||
/**
|
|
||||||
* The below logic is used to switch to home page if any context results are registered
|
|
||||||
* or to help page if context results are unregistered
|
|
||||||
*/
|
|
||||||
const previousLengthRef = useRef(items.length);
|
|
||||||
useEffect(() => {
|
|
||||||
if (items.length >= 1 && previousLengthRef.current === 0) {
|
|
||||||
searchModeState[1]("home");
|
|
||||||
} else if (items.length === 0 && previousLengthRef.current >= 1) {
|
|
||||||
searchModeState[1]("help");
|
|
||||||
}
|
|
||||||
|
|
||||||
previousLengthRef.current = items.length;
|
|
||||||
}, [items.length, searchModeState]);
|
|
||||||
|
|
||||||
if (!activeMode) {
|
if (!activeMode) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We use the "key" below to prevent the 'Different amounts of hooks' error
|
// We use the "key" below to prevent the 'Different amounts of hooks' error
|
||||||
return (
|
return <SpotlightWithActiveMode key={mode} modeState={searchModeState} activeMode={activeMode} />;
|
||||||
<SpotlightWithActiveMode key={mode} modeState={searchModeState} activeMode={activeMode} defaultMode={defaultMode} />
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface SpotlightWithActiveModeProps {
|
interface SpotlightWithActiveModeProps {
|
||||||
modeState: [SearchModeKey, Dispatch<SetStateAction<SearchModeKey>>];
|
modeState: [SearchModeKey, Dispatch<SetStateAction<SearchModeKey>>];
|
||||||
activeMode: SearchMode;
|
activeMode: SearchMode;
|
||||||
defaultMode: SearchModeKey;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SpotlightWithActiveMode = ({ modeState, activeMode, defaultMode }: SpotlightWithActiveModeProps) => {
|
const SpotlightWithActiveMode = ({ modeState, activeMode }: SpotlightWithActiveModeProps) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [mode, setMode] = modeState;
|
const [mode, setMode] = modeState;
|
||||||
const [childrenOptions, setChildrenOptions] = useState<inferSearchInteractionOptions<"children"> | null>(null);
|
const [childrenOptions, setChildrenOptions] = useState<inferSearchInteractionOptions<"children"> | null>(null);
|
||||||
@@ -72,12 +50,12 @@ const SpotlightWithActiveMode = ({ modeState, activeMode, defaultMode }: Spotlig
|
|||||||
<MantineSpotlight.Root
|
<MantineSpotlight.Root
|
||||||
yOffset={8}
|
yOffset={8}
|
||||||
onSpotlightClose={() => {
|
onSpotlightClose={() => {
|
||||||
setMode(defaultMode);
|
setMode("help");
|
||||||
setChildrenOptions(null);
|
setChildrenOptions(null);
|
||||||
}}
|
}}
|
||||||
query={query}
|
query={query}
|
||||||
onQueryChange={(query) => {
|
onQueryChange={(query) => {
|
||||||
if ((mode !== "help" && mode !== "home") || query.length !== 1) {
|
if (mode !== "help" || query.length !== 1) {
|
||||||
setQuery(query);
|
setQuery(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,13 +73,13 @@ const SpotlightWithActiveMode = ({ modeState, activeMode, defaultMode }: Spotlig
|
|||||||
<MantineSpotlight.Search
|
<MantineSpotlight.Search
|
||||||
placeholder={`${t("search.placeholder")}...`}
|
placeholder={`${t("search.placeholder")}...`}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
leftSectionWidth={activeMode.modeKey !== defaultMode ? 80 : 48}
|
leftSectionWidth={activeMode.modeKey !== "help" ? 80 : 48}
|
||||||
leftSection={
|
leftSection={
|
||||||
<Group align="center" wrap="nowrap" gap="xs" w="100%" h="100%">
|
<Group align="center" wrap="nowrap" gap="xs" w="100%" h="100%">
|
||||||
<Center w={48} h="100%">
|
<Center w={48} h="100%">
|
||||||
<IconSearch stroke={1.5} />
|
<IconSearch stroke={1.5} />
|
||||||
</Center>
|
</Center>
|
||||||
{activeMode.modeKey !== defaultMode ? <Kbd size="sm">{activeMode.character}</Kbd> : null}
|
{activeMode.modeKey !== "help" ? <Kbd size="sm">{activeMode.character}</Kbd> : null}
|
||||||
</Group>
|
</Group>
|
||||||
}
|
}
|
||||||
styles={{
|
styles={{
|
||||||
@@ -110,10 +88,10 @@ const SpotlightWithActiveMode = ({ modeState, activeMode, defaultMode }: Spotlig
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
rightSection={
|
rightSection={
|
||||||
mode === defaultMode ? undefined : (
|
mode === "help" ? undefined : (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMode(defaultMode);
|
setMode("help");
|
||||||
setChildrenOptions(null);
|
setChildrenOptions(null);
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
@@ -125,8 +103,8 @@ const SpotlightWithActiveMode = ({ modeState, activeMode, defaultMode }: Spotlig
|
|||||||
}
|
}
|
||||||
value={query}
|
value={query}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (query.length === 0 && mode !== defaultMode && event.key === "Backspace") {
|
if (query.length === 0 && mode !== "help" && event.key === "Backspace") {
|
||||||
setMode(defaultMode);
|
setMode("help");
|
||||||
setChildrenOptions(null);
|
setChildrenOptions(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -4,10 +4,5 @@ import { spotlightActions } from "./spotlight-store";
|
|||||||
|
|
||||||
export { Spotlight } from "./components/spotlight";
|
export { Spotlight } from "./components/spotlight";
|
||||||
export { openSpotlight };
|
export { openSpotlight };
|
||||||
export {
|
|
||||||
SpotlightProvider,
|
|
||||||
useRegisterSpotlightContextResults,
|
|
||||||
useRegisterSpotlightContextActions,
|
|
||||||
} from "./modes/home/context";
|
|
||||||
|
|
||||||
const openSpotlight = spotlightActions.open;
|
const openSpotlight = spotlightActions.open;
|
||||||
@@ -4,7 +4,7 @@ import type { SearchGroup } from "./group";
|
|||||||
|
|
||||||
export type SearchMode = {
|
export type SearchMode = {
|
||||||
modeKey: keyof TranslationObject["search"]["mode"];
|
modeKey: keyof TranslationObject["search"]["mode"];
|
||||||
character: string | undefined;
|
character: string;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
groups: SearchGroup[];
|
groups: SearchGroup[];
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import { Group, Text } from "@mantine/core";
|
|
||||||
|
|
||||||
import { createGroup } from "../../lib/group";
|
|
||||||
import type { ContextSpecificItem } from "../home/context";
|
|
||||||
import { useSpotlightContextActions } from "../home/context";
|
|
||||||
|
|
||||||
export const contextSpecificActionsSearchGroups = createGroup<ContextSpecificItem>({
|
|
||||||
title: (t) => t("search.mode.command.group.localCommand.title"),
|
|
||||||
keyPath: "id",
|
|
||||||
Component(option) {
|
|
||||||
const icon =
|
|
||||||
typeof option.icon !== "string" ? (
|
|
||||||
<option.icon size={24} />
|
|
||||||
) : (
|
|
||||||
<img width={24} height={24} src={option.icon} alt={option.name} />
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Group w="100%" wrap="nowrap" align="center" px="md" py="xs">
|
|
||||||
{icon}
|
|
||||||
<Text>{option.name}</Text>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
useInteraction(option) {
|
|
||||||
return option.interaction();
|
|
||||||
},
|
|
||||||
filter(query, option) {
|
|
||||||
return option.name.toLowerCase().includes(query.toLowerCase());
|
|
||||||
},
|
|
||||||
useOptions() {
|
|
||||||
return useSpotlightContextActions();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
import { Group, Text, useMantineColorScheme } from "@mantine/core";
|
|
||||||
import type { TablerIcon } from "@tabler/icons-react";
|
|
||||||
import {
|
|
||||||
IconBox,
|
|
||||||
IconCategoryPlus,
|
|
||||||
IconFileImport,
|
|
||||||
IconLanguage,
|
|
||||||
IconMailForward,
|
|
||||||
IconMoon,
|
|
||||||
IconPlug,
|
|
||||||
IconSun,
|
|
||||||
IconUserPlus,
|
|
||||||
IconUsersGroup,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
|
|
||||||
import { useSession } from "@homarr/auth/client";
|
|
||||||
import { useModalAction } from "@homarr/modals";
|
|
||||||
import { AddBoardModal, AddGroupModal, ImportBoardModal, InviteCreateModal } from "@homarr/modals-collection";
|
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
|
||||||
|
|
||||||
import { createGroup } from "../../lib/group";
|
|
||||||
import type { inferSearchInteractionDefinition, SearchInteraction } from "../../lib/interaction";
|
|
||||||
import { interaction } from "../../lib/interaction";
|
|
||||||
import { languageChildrenOptions } from "./children/language";
|
|
||||||
import { newIntegrationChildrenOptions } from "./children/new-integration";
|
|
||||||
|
|
||||||
// This has to be type so it can be interpreted as Record<string, unknown>.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
||||||
type Command<TSearchInteraction extends SearchInteraction = SearchInteraction> = {
|
|
||||||
commandKey: string;
|
|
||||||
icon: TablerIcon;
|
|
||||||
name: string;
|
|
||||||
useInteraction: (
|
|
||||||
_c: Command<TSearchInteraction>,
|
|
||||||
query: string,
|
|
||||||
) => inferSearchInteractionDefinition<TSearchInteraction>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const globalCommandGroup = createGroup<Command>({
|
|
||||||
keyPath: "commandKey",
|
|
||||||
title: "Global commands",
|
|
||||||
useInteraction: (option, query) => option.useInteraction(option, query),
|
|
||||||
Component: ({ icon: Icon, name }) => (
|
|
||||||
<Group px="md" py="sm">
|
|
||||||
<Icon stroke={1.5} />
|
|
||||||
<Text>{name}</Text>
|
|
||||||
</Group>
|
|
||||||
),
|
|
||||||
filter(query, option) {
|
|
||||||
return option.name.toLowerCase().includes(query.toLowerCase());
|
|
||||||
},
|
|
||||||
useOptions() {
|
|
||||||
const tOption = useScopedI18n("search.mode.command.group.globalCommand.option");
|
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
const { data: session } = useSession();
|
|
||||||
|
|
||||||
const commands: (Command & { hidden?: boolean })[] = [
|
|
||||||
{
|
|
||||||
commandKey: "colorScheme",
|
|
||||||
icon: colorScheme === "dark" ? IconSun : IconMoon,
|
|
||||||
name: tOption(`colorScheme.${colorScheme === "dark" ? "light" : "dark"}`),
|
|
||||||
useInteraction: () => {
|
|
||||||
const { toggleColorScheme } = useMantineColorScheme();
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "javaScript",
|
|
||||||
onSelect: toggleColorScheme,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "language",
|
|
||||||
icon: IconLanguage,
|
|
||||||
name: tOption("language.label"),
|
|
||||||
useInteraction: interaction.children(languageChildrenOptions),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "newBoard",
|
|
||||||
icon: IconCategoryPlus,
|
|
||||||
name: tOption("newBoard.label"),
|
|
||||||
useInteraction() {
|
|
||||||
const { openModal } = useModalAction(AddBoardModal);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "javaScript",
|
|
||||||
onSelect() {
|
|
||||||
openModal(undefined);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
hidden: !session?.user.permissions.includes("board-create"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "importBoard",
|
|
||||||
icon: IconFileImport,
|
|
||||||
name: tOption("importBoard.label"),
|
|
||||||
useInteraction() {
|
|
||||||
const { openModal } = useModalAction(ImportBoardModal);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "javaScript",
|
|
||||||
onSelect() {
|
|
||||||
openModal(undefined);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
hidden: !session?.user.permissions.includes("board-create"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "newApp",
|
|
||||||
icon: IconBox,
|
|
||||||
name: tOption("newApp.label"),
|
|
||||||
useInteraction: interaction.link(() => ({ href: "/manage/apps/new" })),
|
|
||||||
hidden: !session?.user.permissions.includes("app-create"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "newIntegration",
|
|
||||||
icon: IconPlug,
|
|
||||||
name: tOption("newIntegration.label"),
|
|
||||||
useInteraction: interaction.children(newIntegrationChildrenOptions),
|
|
||||||
hidden: !session?.user.permissions.includes("integration-create"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "newUser",
|
|
||||||
icon: IconUserPlus,
|
|
||||||
name: tOption("newUser.label"),
|
|
||||||
useInteraction: interaction.link(() => ({ href: "/manage/users/new" })),
|
|
||||||
hidden: !session?.user.permissions.includes("admin"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "newInvite",
|
|
||||||
icon: IconMailForward,
|
|
||||||
name: tOption("newInvite.label"),
|
|
||||||
useInteraction() {
|
|
||||||
const { openModal } = useModalAction(InviteCreateModal);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "javaScript",
|
|
||||||
onSelect() {
|
|
||||||
openModal(undefined);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
hidden: !session?.user.permissions.includes("admin"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "newGroup",
|
|
||||||
icon: IconUsersGroup,
|
|
||||||
name: tOption("newGroup.label"),
|
|
||||||
useInteraction() {
|
|
||||||
const { openModal } = useModalAction(AddGroupModal);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "javaScript",
|
|
||||||
onSelect() {
|
|
||||||
openModal(undefined);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
hidden: !session?.user.permissions.includes("admin"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return commands.filter((command) => !command.hidden);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,9 +1,173 @@
|
|||||||
|
import { Group, Text, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import {
|
||||||
|
IconBox,
|
||||||
|
IconCategoryPlus,
|
||||||
|
IconFileImport,
|
||||||
|
IconLanguage,
|
||||||
|
IconMailForward,
|
||||||
|
IconMoon,
|
||||||
|
IconPlug,
|
||||||
|
IconSun,
|
||||||
|
IconUserPlus,
|
||||||
|
IconUsersGroup,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
|
import { useSession } from "@homarr/auth/client";
|
||||||
|
import { useModalAction } from "@homarr/modals";
|
||||||
|
import { AddBoardModal, AddGroupModal, ImportBoardModal, InviteCreateModal } from "@homarr/modals-collection";
|
||||||
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
|
import type { TablerIcon } from "@homarr/ui";
|
||||||
|
|
||||||
|
import { createGroup } from "../../lib/group";
|
||||||
|
import type { inferSearchInteractionDefinition, SearchInteraction } from "../../lib/interaction";
|
||||||
|
import { interaction } from "../../lib/interaction";
|
||||||
import type { SearchMode } from "../../lib/mode";
|
import type { SearchMode } from "../../lib/mode";
|
||||||
import { contextSpecificActionsSearchGroups } from "./context-specific-group";
|
import { languageChildrenOptions } from "./children/language";
|
||||||
import { globalCommandGroup } from "./global-group";
|
import { newIntegrationChildrenOptions } from "./children/new-integration";
|
||||||
|
|
||||||
|
// This has to be type so it can be interpreted as Record<string, unknown>.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||||
|
type Command<TSearchInteraction extends SearchInteraction = SearchInteraction> = {
|
||||||
|
commandKey: string;
|
||||||
|
icon: TablerIcon;
|
||||||
|
name: string;
|
||||||
|
useInteraction: (
|
||||||
|
_c: Command<TSearchInteraction>,
|
||||||
|
query: string,
|
||||||
|
) => inferSearchInteractionDefinition<TSearchInteraction>;
|
||||||
|
};
|
||||||
|
|
||||||
export const commandMode = {
|
export const commandMode = {
|
||||||
modeKey: "command",
|
modeKey: "command",
|
||||||
character: ">",
|
character: ">",
|
||||||
groups: [contextSpecificActionsSearchGroups, globalCommandGroup],
|
groups: [
|
||||||
|
createGroup<Command>({
|
||||||
|
keyPath: "commandKey",
|
||||||
|
title: "Global commands",
|
||||||
|
useInteraction: (option, query) => option.useInteraction(option, query),
|
||||||
|
Component: ({ icon: Icon, name }) => (
|
||||||
|
<Group px="md" py="sm">
|
||||||
|
<Icon stroke={1.5} />
|
||||||
|
<Text>{name}</Text>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
filter(query, option) {
|
||||||
|
return option.name.toLowerCase().includes(query.toLowerCase());
|
||||||
|
},
|
||||||
|
useOptions() {
|
||||||
|
const tOption = useScopedI18n("search.mode.command.group.globalCommand.option");
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
|
const commands: (Command & { hidden?: boolean })[] = [
|
||||||
|
{
|
||||||
|
commandKey: "colorScheme",
|
||||||
|
icon: colorScheme === "dark" ? IconSun : IconMoon,
|
||||||
|
name: tOption(`colorScheme.${colorScheme === "dark" ? "light" : "dark"}`),
|
||||||
|
useInteraction: () => {
|
||||||
|
const { toggleColorScheme } = useMantineColorScheme();
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "javaScript",
|
||||||
|
onSelect: toggleColorScheme,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "language",
|
||||||
|
icon: IconLanguage,
|
||||||
|
name: tOption("language.label"),
|
||||||
|
useInteraction: interaction.children(languageChildrenOptions),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "newBoard",
|
||||||
|
icon: IconCategoryPlus,
|
||||||
|
name: tOption("newBoard.label"),
|
||||||
|
useInteraction() {
|
||||||
|
const { openModal } = useModalAction(AddBoardModal);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "javaScript",
|
||||||
|
onSelect() {
|
||||||
|
openModal(undefined);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hidden: !session?.user.permissions.includes("board-create"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "importBoard",
|
||||||
|
icon: IconFileImport,
|
||||||
|
name: tOption("importBoard.label"),
|
||||||
|
useInteraction() {
|
||||||
|
const { openModal } = useModalAction(ImportBoardModal);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "javaScript",
|
||||||
|
onSelect() {
|
||||||
|
openModal(undefined);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hidden: !session?.user.permissions.includes("board-create"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "newApp",
|
||||||
|
icon: IconBox,
|
||||||
|
name: tOption("newApp.label"),
|
||||||
|
useInteraction: interaction.link(() => ({ href: "/manage/apps/new" })),
|
||||||
|
hidden: !session?.user.permissions.includes("app-create"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "newIntegration",
|
||||||
|
icon: IconPlug,
|
||||||
|
name: tOption("newIntegration.label"),
|
||||||
|
useInteraction: interaction.children(newIntegrationChildrenOptions),
|
||||||
|
hidden: !session?.user.permissions.includes("integration-create"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "newUser",
|
||||||
|
icon: IconUserPlus,
|
||||||
|
name: tOption("newUser.label"),
|
||||||
|
useInteraction: interaction.link(() => ({ href: "/manage/users/new" })),
|
||||||
|
hidden: !session?.user.permissions.includes("admin"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "newInvite",
|
||||||
|
icon: IconMailForward,
|
||||||
|
name: tOption("newInvite.label"),
|
||||||
|
useInteraction() {
|
||||||
|
const { openModal } = useModalAction(InviteCreateModal);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "javaScript",
|
||||||
|
onSelect() {
|
||||||
|
openModal(undefined);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hidden: !session?.user.permissions.includes("admin"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "newGroup",
|
||||||
|
icon: IconUsersGroup,
|
||||||
|
name: tOption("newGroup.label"),
|
||||||
|
useInteraction() {
|
||||||
|
const { openModal } = useModalAction(AddGroupModal);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "javaScript",
|
||||||
|
onSelect() {
|
||||||
|
openModal(undefined);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hidden: !session?.user.permissions.includes("admin"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return commands.filter((command) => !command.hidden);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
} satisfies SearchMode;
|
} satisfies SearchMode;
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { Group, Text } from "@mantine/core";
|
|
||||||
|
|
||||||
import { createGroup } from "../../lib/group";
|
|
||||||
import type { ContextSpecificItem } from "./context";
|
|
||||||
import { useSpotlightContextResults } from "./context";
|
|
||||||
|
|
||||||
export const contextSpecificSearchGroups = createGroup<ContextSpecificItem>({
|
|
||||||
title: (t) => t("search.mode.home.group.local.title"),
|
|
||||||
keyPath: "id",
|
|
||||||
Component(option) {
|
|
||||||
const icon =
|
|
||||||
typeof option.icon !== "string" ? (
|
|
||||||
<option.icon size={24} />
|
|
||||||
) : (
|
|
||||||
<img width={24} height={24} src={option.icon} alt={option.name} />
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Group w="100%" wrap="nowrap" align="center" px="md" py="xs">
|
|
||||||
{icon}
|
|
||||||
<Text>{option.name}</Text>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
useInteraction(option) {
|
|
||||||
return option.interaction();
|
|
||||||
},
|
|
||||||
filter(query, option) {
|
|
||||||
return option.name.toLowerCase().includes(query.toLowerCase());
|
|
||||||
},
|
|
||||||
useOptions() {
|
|
||||||
return useSpotlightContextResults();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
import type { DependencyList, PropsWithChildren } from "react";
|
|
||||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
||||||
|
|
||||||
import type { TablerIcon } from "@homarr/ui";
|
|
||||||
|
|
||||||
import type { inferSearchInteractionDefinition, SearchInteraction } from "../../lib/interaction";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
||||||
export type ContextSpecificItem = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
icon: TablerIcon | string;
|
|
||||||
interaction: () => inferSearchInteractionDefinition<SearchInteraction>;
|
|
||||||
disabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface SpotlightContextProps {
|
|
||||||
items: ContextSpecificItem[];
|
|
||||||
registerItems: (key: string, results: ContextSpecificItem[]) => void;
|
|
||||||
unregisterItems: (key: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const createSpotlightContext = (displayName: string) => {
|
|
||||||
const SpotlightContext = createContext<SpotlightContextProps | null>(null);
|
|
||||||
SpotlightContext.displayName = displayName;
|
|
||||||
|
|
||||||
const Provider = ({ children }: PropsWithChildren) => {
|
|
||||||
const [itemsMap, setItemsMap] = useState<Map<string, { items: ContextSpecificItem[]; count: number }>>(new Map());
|
|
||||||
|
|
||||||
const registerItems = useCallback((key: string, newItems: ContextSpecificItem[]) => {
|
|
||||||
setItemsMap((prevItems) => {
|
|
||||||
const newItemsMap = new Map(prevItems);
|
|
||||||
newItemsMap.set(key, { items: newItems, count: (newItemsMap.get(key)?.count ?? 0) + 1 });
|
|
||||||
return newItemsMap;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const unregisterItems = useCallback((key: string) => {
|
|
||||||
setItemsMap((prevItems) => {
|
|
||||||
const registrationCount = prevItems.get(key)?.count ?? 0;
|
|
||||||
|
|
||||||
if (registrationCount <= 1) {
|
|
||||||
const newItemsMap = new Map(prevItems);
|
|
||||||
newItemsMap.delete(key);
|
|
||||||
return newItemsMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newItemsMap = new Map(prevItems);
|
|
||||||
newItemsMap.set(key, { items: newItemsMap.get(key)?.items ?? [], count: registrationCount - 1 });
|
|
||||||
|
|
||||||
return prevItems;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const items = useMemo(() => Array.from(itemsMap.values()).flatMap(({ items }) => items), [itemsMap]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SpotlightContext.Provider value={{ items, registerItems, unregisterItems }}>
|
|
||||||
{children}
|
|
||||||
</SpotlightContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useSpotlightContextItems = () => {
|
|
||||||
const context = useContext(SpotlightContext);
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(`useSpotlightContextItems must be used within SpotlightContext[displayName=${displayName}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.items;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useRegisterSpotlightContextItems = (
|
|
||||||
key: string,
|
|
||||||
items: ContextSpecificItem[],
|
|
||||||
dependencyArray: DependencyList,
|
|
||||||
) => {
|
|
||||||
const context = useContext(SpotlightContext);
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(
|
|
||||||
`useRegisterSpotlightContextItems must be used within SpotlightContext[displayName=${displayName}]`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
context.registerItems(
|
|
||||||
key,
|
|
||||||
items.filter((item) => !item.disabled),
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
context.unregisterItems(key);
|
|
||||||
};
|
|
||||||
// We ignore the results
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [...dependencyArray, key]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return [SpotlightContext, Provider, useSpotlightContextItems, useRegisterSpotlightContextItems] as const;
|
|
||||||
};
|
|
||||||
|
|
||||||
const [_ResultContext, ResultProvider, useSpotlightContextResults, useRegisterSpotlightContextResults] =
|
|
||||||
createSpotlightContext("SpotlightContextSpecificResults");
|
|
||||||
const [_ActionContext, ActionProvider, useSpotlightContextActions, useRegisterSpotlightContextActions] =
|
|
||||||
createSpotlightContext("SpotlightContextSpecificActions");
|
|
||||||
|
|
||||||
export {
|
|
||||||
useRegisterSpotlightContextActions,
|
|
||||||
useRegisterSpotlightContextResults,
|
|
||||||
useSpotlightContextActions,
|
|
||||||
useSpotlightContextResults,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SpotlightProvider = ({ children }: PropsWithChildren) => {
|
|
||||||
return (
|
|
||||||
<ResultProvider>
|
|
||||||
<ActionProvider>{children}</ActionProvider>
|
|
||||||
</ResultProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import type { SearchMode } from "../../lib/mode";
|
|
||||||
import { contextSpecificSearchGroups } from "./context-specific-group";
|
|
||||||
|
|
||||||
export const homeMode = {
|
|
||||||
character: undefined,
|
|
||||||
modeKey: "home",
|
|
||||||
groups: [contextSpecificSearchGroups],
|
|
||||||
} satisfies SearchMode;
|
|
||||||
@@ -11,11 +11,10 @@ import type { SearchMode } from "../lib/mode";
|
|||||||
import { appIntegrationBoardMode } from "./app-integration-board";
|
import { appIntegrationBoardMode } from "./app-integration-board";
|
||||||
import { commandMode } from "./command";
|
import { commandMode } from "./command";
|
||||||
import { externalMode } from "./external";
|
import { externalMode } from "./external";
|
||||||
import { homeMode } from "./home";
|
|
||||||
import { pageMode } from "./page";
|
import { pageMode } from "./page";
|
||||||
import { userGroupMode } from "./user-group";
|
import { userGroupMode } from "./user-group";
|
||||||
|
|
||||||
const searchModesForHelp = [userGroupMode, appIntegrationBoardMode, externalMode, commandMode, pageMode] as const;
|
const searchModesWithoutHelp = [userGroupMode, appIntegrationBoardMode, externalMode, commandMode, pageMode] as const;
|
||||||
|
|
||||||
const helpMode = {
|
const helpMode = {
|
||||||
modeKey: "help",
|
modeKey: "help",
|
||||||
@@ -83,4 +82,4 @@ const helpMode = {
|
|||||||
},
|
},
|
||||||
} satisfies SearchMode;
|
} satisfies SearchMode;
|
||||||
|
|
||||||
export const searchModes = [...searchModesForHelp, helpMode, homeMode] as const;
|
export const searchModes = [...searchModesWithoutHelp, helpMode] as const;
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"deepmerge": "4.3.1",
|
"deepmerge": "4.3.1",
|
||||||
"mantine-react-table": "2.0.0-beta.7",
|
"mantine-react-table": "2.0.0-beta.7",
|
||||||
"next": "^14.2.20",
|
"next": "^14.2.20",
|
||||||
"next-intl": "3.26.0",
|
"next-intl": "3.25.3",
|
||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -733,8 +733,7 @@
|
|||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"homeBoard": "Your home board",
|
"homeBoard": "Your home board",
|
||||||
"loggedOut": "Logged out",
|
"loggedOut": "Logged out"
|
||||||
"updateAvailable": "{countUpdates} updates available: {tag}"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dangerZone": "Danger zone",
|
"dangerZone": "Danger zone",
|
||||||
@@ -1160,9 +1159,6 @@
|
|||||||
"automationId": {
|
"automationId": {
|
||||||
"label": "Automation ID"
|
"label": "Automation ID"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"spotlightAction": {
|
|
||||||
"run": "Run {name}"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
@@ -2155,8 +2151,8 @@
|
|||||||
"sessionCleanup": {
|
"sessionCleanup": {
|
||||||
"label": "Session Cleanup"
|
"label": "Session Cleanup"
|
||||||
},
|
},
|
||||||
"updateChecker": {
|
"notifications-fetcher": {
|
||||||
"label": "Update checker"
|
"label": "Notifications fetcher"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2457,9 +2453,6 @@
|
|||||||
"command": {
|
"command": {
|
||||||
"help": "Activate command mode",
|
"help": "Activate command mode",
|
||||||
"group": {
|
"group": {
|
||||||
"localCommand": {
|
|
||||||
"title": "Local commands"
|
|
||||||
},
|
|
||||||
"globalCommand": {
|
"globalCommand": {
|
||||||
"title": "Global commands",
|
"title": "Global commands",
|
||||||
"option": {
|
"option": {
|
||||||
@@ -2569,13 +2562,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
|
||||||
"group": {
|
|
||||||
"local": {
|
|
||||||
"title": "Local results"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"page": {
|
"page": {
|
||||||
"help": "Search for pages",
|
"help": "Search for pages",
|
||||||
"group": {
|
"group": {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@extractus/feed-extractor": "^7.1.3",
|
"@extractus/feed-extractor": "^7.1.3",
|
||||||
"@homarr/api": "workspace:^0.1.0",
|
"@homarr/api": "workspace:^0.1.0",
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
"mantine-react-table": "2.0.0-beta.7",
|
"mantine-react-table": "2.0.0-beta.7",
|
||||||
"next": "^14.2.20",
|
"next": "^14.2.20",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"video.js": "^8.21.0"
|
"video.js": "^8.20.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import combineClasses from "clsx";
|
|||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { parseAppHrefWithVariablesClient } from "@homarr/common/client";
|
import { parseAppHrefWithVariablesClient } from "@homarr/common/client";
|
||||||
import { useRegisterSpotlightContextResults } from "@homarr/spotlight";
|
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../definition";
|
import type { WidgetComponentProps } from "../definition";
|
||||||
@@ -29,24 +28,6 @@ export default function AppWidget({ options, isEditMode }: WidgetComponentProps<
|
|||||||
retry: false,
|
retry: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
useRegisterSpotlightContextResults(
|
|
||||||
`app-${app.id}`,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
id: app.id,
|
|
||||||
name: app.name,
|
|
||||||
icon: app.iconUrl,
|
|
||||||
interaction() {
|
|
||||||
return {
|
|
||||||
type: "link",
|
|
||||||
href: parseAppHrefWithVariablesClient(app.href ?? ""),
|
|
||||||
newTab: options.openInNewTab,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[app, options.openInNewTab],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLink
|
<AppLink
|
||||||
|
|||||||
@@ -4,36 +4,17 @@ import { Anchor, Box, Card, Divider, Flex, Group, Stack, Text, Title, UnstyledBu
|
|||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { parseAppHrefWithVariablesClient } from "@homarr/common/client";
|
|
||||||
import { useRegisterSpotlightContextResults } from "@homarr/spotlight";
|
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../definition";
|
import type { WidgetComponentProps } from "../definition";
|
||||||
import classes from "./bookmark.module.css";
|
import classes from "./bookmark.module.css";
|
||||||
|
|
||||||
export default function BookmarksWidget({ options, width, height, itemId }: WidgetComponentProps<"bookmarks">) {
|
export default function BookmarksWidget({ options, width, height }: WidgetComponentProps<"bookmarks">) {
|
||||||
const [data] = clientApi.app.byIds.useSuspenseQuery(options.items, {
|
const [data] = clientApi.app.byIds.useSuspenseQuery(options.items, {
|
||||||
select(data) {
|
select(data) {
|
||||||
return data.sort((appA, appB) => options.items.indexOf(appA.id) - options.items.indexOf(appB.id));
|
return data.sort((appA, appB) => options.items.indexOf(appA.id) - options.items.indexOf(appB.id));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useRegisterSpotlightContextResults(
|
|
||||||
`bookmark-${itemId}`,
|
|
||||||
data.map((app) => ({
|
|
||||||
id: app.id,
|
|
||||||
name: app.name,
|
|
||||||
icon: app.iconUrl,
|
|
||||||
interaction() {
|
|
||||||
return {
|
|
||||||
type: "link",
|
|
||||||
href: parseAppHrefWithVariablesClient(app.href ?? ""),
|
|
||||||
newTab: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
[data],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack h="100%" gap="sm" p="sm">
|
<Stack h="100%" gap="sm" p="sm">
|
||||||
<Title order={4} px="0.25rem">
|
<Title order={4} px="0.25rem">
|
||||||
|
|||||||
@@ -169,15 +169,15 @@ export default function HealthMonitoringWidget({ options, integrationIds }: Widg
|
|||||||
</List.Item>
|
</List.Item>
|
||||||
<List m="0.5cqmin" withPadding center spacing="0.5cqmin" icon={<IconCpu size="1cqmin" />}>
|
<List m="0.5cqmin" withPadding center spacing="0.5cqmin" icon={<IconCpu size="1cqmin" />}>
|
||||||
<List.Item className="health-monitoring-information-load-average-1min">
|
<List.Item className="health-monitoring-information-load-average-1min">
|
||||||
{t("widget.healthMonitoring.popover.minute")} {healthInfo.loadAverage["1min"]}%
|
{t("widget.healthMonitoring.popover.minute")} {healthInfo.loadAverage["1min"]}
|
||||||
</List.Item>
|
</List.Item>
|
||||||
<List.Item className="health-monitoring-information-load-average-5min">
|
<List.Item className="health-monitoring-information-load-average-5min">
|
||||||
{t("widget.healthMonitoring.popover.minutes", { count: 5 })}{" "}
|
{t("widget.healthMonitoring.popover.minutes", { count: 5 })}{" "}
|
||||||
{healthInfo.loadAverage["5min"]}%
|
{healthInfo.loadAverage["5min"]}
|
||||||
</List.Item>
|
</List.Item>
|
||||||
<List.Item className="health-monitoring-information-load-average-15min">
|
<List.Item className="health-monitoring-information-load-average-15min">
|
||||||
{t("widget.healthMonitoring.popover.minutes", { count: 15 })}{" "}
|
{t("widget.healthMonitoring.popover.minutes", { count: 15 })}{" "}
|
||||||
{healthInfo.loadAverage["15min"]}%
|
{healthInfo.loadAverage["15min"]}
|
||||||
</List.Item>
|
</List.Item>
|
||||||
</List>
|
</List>
|
||||||
</List>
|
</List>
|
||||||
@@ -363,7 +363,7 @@ const CpuTempRing = ({ fahrenheit, cpuTemp }: { fahrenheit: boolean; cpuTemp: nu
|
|||||||
label={
|
label={
|
||||||
<Center style={{ flexDirection: "column" }}>
|
<Center style={{ flexDirection: "column" }}>
|
||||||
<Text className="health-monitoring-cpu-temp-value" size="3cqmin">
|
<Text className="health-monitoring-cpu-temp-value" size="3cqmin">
|
||||||
{fahrenheit ? `${(cpuTemp * 1.8 + 32).toFixed(1)}°F` : `${cpuTemp.toFixed(1)}°C`}
|
{fahrenheit ? `${(cpuTemp * 1.8 + 32).toFixed(1)}°F` : `${cpuTemp}°C`}
|
||||||
</Text>
|
</Text>
|
||||||
<IconCpu className="health-monitoring-cpu-temp-icon" size="7cqmin" />
|
<IconCpu className="health-monitoring-cpu-temp-icon" size="7cqmin" />
|
||||||
</Center>
|
</Center>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { IconHeartRateMonitor, IconServerOff } from "@tabler/icons-react";
|
import { IconHeartRateMonitor, IconServerOff } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
|
||||||
|
|
||||||
import { createWidgetDefinition } from "../definition";
|
import { createWidgetDefinition } from "../definition";
|
||||||
import { optionsBuilder } from "../options";
|
import { optionsBuilder } from "../options";
|
||||||
|
|
||||||
@@ -21,7 +19,7 @@ export const { definition, componentLoader } = createWidgetDefinition("healthMon
|
|||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
supportedIntegrations: getIntegrationKindsByCategory("healthMonitoring"),
|
supportedIntegrations: ["openmediavault"],
|
||||||
errors: {
|
errors: {
|
||||||
INTERNAL_SERVER_ERROR: {
|
INTERNAL_SERVER_ERROR: {
|
||||||
icon: IconServerOff,
|
icon: IconServerOff,
|
||||||
|
|||||||
@@ -2,10 +2,8 @@
|
|||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { Center, Stack, Text, UnstyledButton } from "@mantine/core";
|
import { Center, Stack, Text, UnstyledButton } from "@mantine/core";
|
||||||
import { IconBinaryTree } from "@tabler/icons-react";
|
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useRegisterSpotlightContextActions } from "@homarr/spotlight";
|
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../../definition";
|
import type { WidgetComponentProps } from "../../definition";
|
||||||
import { NoIntegrationSelectedError } from "../../errors";
|
import { NoIntegrationSelectedError } from "../../errors";
|
||||||
@@ -62,27 +60,6 @@ const InnerComponent = ({ options, integrationId, isEditMode }: InnerComponentPr
|
|||||||
});
|
});
|
||||||
}, [integrationId, isEditMode, mutate, options.clickable, options.entityId]);
|
}, [integrationId, isEditMode, mutate, options.clickable, options.entityId]);
|
||||||
|
|
||||||
useRegisterSpotlightContextActions(
|
|
||||||
`smartHome-entityState-${options.entityId}`,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
id: options.entityId,
|
|
||||||
name: options.displayName,
|
|
||||||
icon: IconBinaryTree,
|
|
||||||
interaction() {
|
|
||||||
return {
|
|
||||||
type: "javaScript",
|
|
||||||
onSelect() {
|
|
||||||
handleClick();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
disabled: !options.clickable,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[handleClick, options.clickable, options.displayName, options.entityId],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import { useDisclosure, useTimeout } from "@mantine/hooks";
|
|||||||
import { IconAutomation, IconCheck } from "@tabler/icons-react";
|
import { IconAutomation, IconCheck } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useRegisterSpotlightContextActions } from "@homarr/spotlight";
|
|
||||||
import { useI18n } from "@homarr/translation/client";
|
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../../definition";
|
import type { WidgetComponentProps } from "../../definition";
|
||||||
|
|
||||||
@@ -36,29 +34,6 @@ export default function SmartHomeTriggerAutomationWidget({
|
|||||||
integrationId: integrationIds[0] ?? "",
|
integrationId: integrationIds[0] ?? "",
|
||||||
});
|
});
|
||||||
}, [integrationIds, isEditMode, mutateAsync, options.automationId]);
|
}, [integrationIds, isEditMode, mutateAsync, options.automationId]);
|
||||||
|
|
||||||
const t = useI18n();
|
|
||||||
useRegisterSpotlightContextActions(
|
|
||||||
`smartHome-automation-${options.automationId}`,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
id: options.automationId,
|
|
||||||
name: t("widget.smartHome-executeAutomation.spotlightAction.run", { name: options.displayName }),
|
|
||||||
icon: IconAutomation,
|
|
||||||
interaction() {
|
|
||||||
return {
|
|
||||||
type: "javaScript",
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
|
||||||
async onSelect() {
|
|
||||||
await handleClick();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[handleClick, options.automationId, options.displayName],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UnstyledButton onClick={handleClick} style={{ cursor: !isEditMode ? "pointer" : "initial" }} w="100%" h="100%">
|
<UnstyledButton onClick={handleClick} style={{ cursor: !isEditMode ? "pointer" : "initial" }} w="100%" h="100%">
|
||||||
{isShowSuccess && (
|
{isShowSuccess && (
|
||||||
|
|||||||
Generated
+142
-444
File diff suppressed because it is too large.
Load diff
@@ -23,7 +23,7 @@
|
|||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"eslint-plugin-react": "^7.37.2",
|
"eslint-plugin-react": "^7.37.2",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"typescript-eslint": "^8.17.0"
|
"typescript-eslint": "^8.17.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
Reference in new issue
Block a user