Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: downloads widget #844

Merged
merged 49 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
8340e82
feat: usenet downloads widget
manuel-rw Jul 21, 2024
b65f3ae
feat: add nzbget package
manuel-rw Jul 21, 2024
e133977
feat: initial commit
Jul 31, 2024
d90f9f0
Merge branch 'dev' into feature/usenet-downloads
SeDemal Jul 31, 2024
6700fce
Merge branch 'feature/usenet-downloads' of https://github.com/homarr-…
Jul 31, 2024
fb8b15f
fix: javacript errors and mistakes
Jul 31, 2024
56f2b44
chore: misc
Jul 31, 2024
134728a
feat: NzbGet API
Aug 1, 2024
3ed7adc
feat: add default sorting and fix Icon sizing warnings
Aug 1, 2024
b284f1f
fix: with low opacity header not readable
Meierschlumpf Aug 2, 2024
f845d08
Merge branch 'dev' into feat/download-clients-integration-and-widget
Meierschlumpf Aug 2, 2024
0a99a77
fix: fixed table header, console warnings and improve subscription
Aug 2, 2024
a816322
Merge branch 'feat/download-clients-integration-and-widget' of https:…
Aug 3, 2024
e1f861b
fix: merged incorrectly
Aug 3, 2024
d17bd84
Merge branch 'dev' into feature/usenet-downloads
SeDemal Aug 8, 2024
b3c1471
Merge branch 'feature/usenet-downloads' into feat/download-clients-in…
SeDemal Aug 8, 2024
3c7f5ce
fix: remove erroneously re-added "usenet-downloads" mentions and files
Aug 8, 2024
61b42ae
chore: cleaning and format things
Aug 9, 2024
e4c7926
feat: standardize sizes and group them in baseStyle
Aug 9, 2024
b2a576c
Merge branch 'dev' into feature/usenet-downloads
SeDemal Aug 9, 2024
84a72f4
Merge branch 'feature/usenet-downloads' into feat/download-clients-in…
SeDemal Aug 9, 2024
c86c176
feat: Add option hide depending on integration selection
SeDemal Aug 9, 2024
67d7186
refactor: address comments
SeDemal Aug 13, 2024
d524484
feat: Get integrations by category + various fixes
SeDemal Aug 14, 2024
1fb3d28
fix: broken lock file
SeDemal Aug 14, 2024
12605b4
fix: last comments and small refactors
SeDemal Aug 14, 2024
1ee663f
fix: adapt humanFileSize uses
SeDemal Aug 14, 2024
6f3c6b0
refactor: better typing of IntegrationKindByCategory
SeDemal Aug 15, 2024
653d2e4
fix: createXIntegrationMiddleware to accept Arrays
SeDemal Aug 15, 2024
9ea0dc8
refactor: revert change of createXIntegrationMiddleware and normalize…
SeDemal Aug 15, 2024
94ee417
fix: lint errors
SeDemal Aug 20, 2024
99320ad
refactor: extract typing
SeDemal Aug 25, 2024
1b3be40
feat: add hooks for getting all integrations with certain access
Meierschlumpf Aug 25, 2024
da3cac1
Merge branch 'feat/download-clients-integration-and-widget' of https:…
Meierschlumpf Aug 25, 2024
86250a4
feat: Add tests
SeDemal Sep 4, 2024
287b61e
feat: hide controls depending on permissions
SeDemal Sep 4, 2024
47ef25a
chore: further use of AtLeastOneOf
SeDemal Sep 4, 2024
3e32288
Merge pull request #895 from homarr-labs/feat/download-clients-integr…
Meierschlumpf Sep 6, 2024
ab94cdc
Merge https://github.com/homarr-labs/homarr into feature/usenet-downl…
SeDemal Sep 6, 2024
b0c8592
fix: format and lint
SeDemal Sep 7, 2024
b059edf
chore: make encryption server side
SeDemal Sep 7, 2024
df7e129
fix: use path join for container files copy source
SeDemal Sep 7, 2024
f482060
fix: torrent-file update dependency
SeDemal Sep 10, 2024
4ee9f0a
Merge branch 'dev' of https://github.com/homarr-labs/homarr into feat…
SeDemal Sep 10, 2024
5d8d0ae
feat: Applying new helpers everywhere
SeDemal Sep 10, 2024
8d42ac4
Merge branch 'dev' of https://github.com/homarr-labs/homarr into feat…
SeDemal Sep 10, 2024
9fe9d77
fix: tests
SeDemal Sep 10, 2024
a3e17db
fix: packages
SeDemal Sep 10, 2024
a37baa0
fix: comments
SeDemal Sep 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,7 @@ yarn-error.log*
apps/tasks/tasks.cjs
apps/websocket/wssServer.cjs
apps/nextjs/.million/


#personal backgrounds
apps/nextjs/public/images/background.png
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@
"js/ts.implicitProjectConfig.experimentalDecorators": true,
"prettier.configPath": "./tooling/prettier/index.mjs",
"cSpell.words": [
"ajnart",
"cqmin",
"gridstack",
"homarr",
"jellyfin",
"mantine",
"manuel-rw",
"Meierschlumpf",
"overseerr",
"Sabnzbd",
"SeDemal",
"Sonarr",
"superjson",
"tabler",
Expand Down
15 changes: 13 additions & 2 deletions apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { TRPCError } from "@trpc/server";
// Placed here because gridstack styles are used for board content
import "~/styles/gridstack.scss";

import { IntegrationProvider } from "@homarr/auth/client";
import { auth } from "@homarr/auth/next";
import { getIntegrationsWithPermissionsAsync } from "@homarr/auth/server";
import { getI18n } from "@homarr/translation/server";

import { createMetaTitle } from "~/metadata";
Expand All @@ -27,8 +30,16 @@ export const createBoardContentPage = <TParams extends Record<string, unknown>>(
getInitialBoardAsync: getInitialBoard,
isBoardContentPage: true,
}),
page: () => {
return <ClientBoard />;
// eslint-disable-next-line no-restricted-syntax
page: async () => {
const session = await auth();
const integrations = await getIntegrationsWithPermissionsAsync(session);

return (
<IntegrationProvider integrations={integrations}>
<ClientBoard />
</IntegrationProvider>
);
},
generateMetadataAsync: async ({ params }: { params: TParams }): Promise<Metadata> => {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ interface NewIntegrationPageProps {
}

export default async function IntegrationsNewPage({ searchParams }: NewIntegrationPageProps) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const result = z.enum([integrationKinds[0]!, ...integrationKinds.slice(1)]).safeParse(searchParams.kind);
const result = z.enum(integrationKinds).safeParse(searchParams.kind);
if (!result.success) {
notFound();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from "react";
import type { MantineSpacing } from "@mantine/core";
import { Group, Stack, Switch, Text, UnstyledButton } from "@mantine/core";

import type { Modify } from "@homarr/common/types";
import type { UseFormReturnType } from "@homarr/form";

export const SwitchSetting = <TFormValue extends Record<string, boolean>>({
Expand All @@ -13,9 +14,12 @@ export const SwitchSetting = <TFormValue extends Record<string, boolean>>({
formKey,
disabled,
}: {
form: Omit<UseFormReturnType<TFormValue, () => TFormValue>, "setFieldValue"> & {
setFieldValue: (key: keyof TFormValue, value: (previous: boolean) => boolean) => void;
};
form: Modify<
UseFormReturnType<TFormValue, () => TFormValue>,
{
setFieldValue: (key: keyof TFormValue, value: (previous: boolean) => boolean) => void;
}
>;
formKey: keyof TFormValue;
ms?: MantineSpacing;
title: string;
Expand Down
4 changes: 4 additions & 0 deletions apps/nextjs/src/app/[locale]/widgets/[kind]/_content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie
});
}, [dimensions, openPreviewDimensionsModal]);

const updateOptions = ({ newOptions }: { newOptions: Record<string, unknown> }) =>
setState({ ...state, options: { ...state.options, newOptions } });

return (
<>
<Card withBorder w={dimensions.width} h={dimensions.height} p={dimensions.height >= 96 ? undefined : 4}>
Expand All @@ -105,6 +108,7 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie
isEditMode={editMode}
boardId={undefined}
itemId={undefined}
setOptions={updateOptions}
/>
</ErrorBoundary>
)}
Expand Down
12 changes: 8 additions & 4 deletions apps/nextjs/src/components/board/items/item-actions.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback } from "react";

import type { Modify } from "@homarr/common/types";
import { createId } from "@homarr/db/client";
import type { WidgetKind } from "@homarr/definitions";
import type { BoardItemAdvancedOptions } from "@homarr/validation";
Expand Down Expand Up @@ -71,9 +72,12 @@ export const useItemActions = () => {
advancedOptions: {
customCssClasses: [],
},
} satisfies Omit<Item, "kind" | "yOffset" | "xOffset"> & {
kind: WidgetKind;
};
} satisfies Modify<
Omit<Item, "yOffset" | "xOffset">,
{
kind: WidgetKind;
}
>;

return {
...previous,
Expand Down Expand Up @@ -105,7 +109,7 @@ export const useItemActions = () => {
id: createId(),
yOffset: undefined,
xOffset: undefined,
} satisfies Omit<Item, "yOffset" | "xOffset"> & { yOffset?: number; xOffset?: number };
} satisfies Modify<Item, { yOffset?: number; xOffset?: number }>;

return {
...previous,
Expand Down
5 changes: 5 additions & 0 deletions apps/nextjs/src/components/board/items/item-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { WidgetError } from "@homarr/widgets/errors";
import type { Item } from "~/app/[locale]/boards/_types";
import { useEditMode, useRequiredBoard } from "~/app/[locale]/boards/(content)/_context";
import classes from "../sections/item.module.css";
import { useItemActions } from "./item-actions";
import { BoardItemMenu } from "./item-menu";

interface BoardItemContentProps {
Expand Down Expand Up @@ -56,6 +57,9 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
const Comp = loadWidgetDynamic(item.kind);
const options = reduceWidgetOptionsWithDefaultValues(item.kind, item.options);
const newItem = { ...item, options };
const { updateItemOptions } = useItemActions();
const updateOptions = ({ newOptions }: { newOptions: Record<string, unknown> }) =>
updateItemOptions({ itemId: item.id, newOptions });

if (!serverData?.isReady) return null;

Expand All @@ -80,6 +84,7 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
isEditMode={isEditMode}
boardId={board.id}
itemId={item.id}
setOptions={updateOptions}
{...dimensions}
/>
</ErrorBoundary>
Expand Down
10 changes: 6 additions & 4 deletions apps/nextjs/src/components/board/sections/item.module.css
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
.itemCard {
@mixin dark {
background-color: rgba(46, 46, 46, var(--opacity));
border-color: rgba(66, 66, 66, var(--opacity));
--background-color: rgb(from var(--mantine-color-dark-6) r g b / var(--opacity));
--border-color: rgb(from var(--mantine-color-dark-4) r g b / var(--opacity));
}
@mixin light {
background-color: rgba(255, 255, 255, var(--opacity));
border-color: rgba(222, 226, 230, var(--opacity));
--background-color: rgb(from var(--mantine-color-white) r g b / var(--opacity));
--border-color: rgb(from var(--mantine-color-gray-3) r g b / var(--opacity));
}
background-color: var(--background-color);
border-color: var(--border-color);
}
11 changes: 6 additions & 5 deletions packages/api/src/middlewares/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { TRPCError } from "@trpc/server";
import type { Session } from "@homarr/auth";
import { hasQueryAccessToIntegrationsAsync } from "@homarr/auth/server";
import { constructIntegrationPermissions } from "@homarr/auth/shared";
import { decryptSecret } from "@homarr/common";
import { decryptSecret } from "@homarr/common/server";
import type { AtLeastOneOf } from "@homarr/common/types";
import type { Database } from "@homarr/db";
import { and, eq, inArray } from "@homarr/db";
import { integrations } from "@homarr/db/schema/sqlite";
Expand All @@ -12,7 +13,7 @@ import { z } from "@homarr/validation";

import { publicProcedure } from "../trpc";

type IntegrationAction = "query" | "interact";
export type IntegrationAction = "query" | "interact";

/**
* Creates a middleware that provides the integration in the context that is of the specified kinds
Expand All @@ -25,7 +26,7 @@ type IntegrationAction = "query" | "interact";
*/
export const createOneIntegrationMiddleware = <TKind extends IntegrationKind>(
action: IntegrationAction,
...kinds: [TKind, ...TKind[]] // Ensure at least one kind is provided
...kinds: AtLeastOneOf<TKind> // Ensure at least one kind is provided
) => {
return publicProcedure.input(z.object({ integrationId: z.string() })).use(async ({ input, ctx, next }) => {
const integration = await ctx.db.query.integrations.findFirst({
Expand Down Expand Up @@ -95,7 +96,7 @@ export const createOneIntegrationMiddleware = <TKind extends IntegrationKind>(
*/
export const createManyIntegrationMiddleware = <TKind extends IntegrationKind>(
action: IntegrationAction,
...kinds: [TKind, ...TKind[]] // Ensure at least one kind is provided
...kinds: AtLeastOneOf<TKind> // Ensure at least one kind is provided
) => {
return publicProcedure
.input(z.object({ integrationIds: z.array(z.string()).min(1) }))
Expand Down Expand Up @@ -161,7 +162,7 @@ export const createManyIntegrationMiddleware = <TKind extends IntegrationKind>(
*/
export const createManyIntegrationOfOneItemMiddleware = <TKind extends IntegrationKind>(
action: IntegrationAction,
...kinds: [TKind, ...TKind[]] // Ensure at least one kind is provided
...kinds: AtLeastOneOf<TKind> // Ensure at least one kind is provided
) => {
return publicProcedure
.input(z.object({ integrationIds: z.array(z.string()).min(1), itemId: z.string() }))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TRPCError } from "@trpc/server";

import { decryptSecret, encryptSecret } from "@homarr/common";
import { decryptSecret, encryptSecret } from "@homarr/common/server";
import type { Database } from "@homarr/db";
import { and, createId, eq, inArray } from "@homarr/db";
import {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { decryptSecret } from "@homarr/common";
import { decryptSecret } from "@homarr/common/server";
import type { Integration } from "@homarr/db/schema/sqlite";
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
import { getAllSecretKindOptions } from "@homarr/definitions";
import { integrationCreatorByKind, IntegrationTestConnectionError } from "@homarr/integrations";
import { integrationCreator, IntegrationTestConnectionError } from "@homarr/integrations";

type FormIntegration = Integration & {
secrets: {
Expand Down Expand Up @@ -37,23 +37,25 @@ export const testConnectionAsync = async (
const sourcedSecrets = [...formSecrets, ...decryptedDbSecrets];
const secretKinds = getSecretKindOption(integration.kind, sourcedSecrets);

const filteredSecrets = secretKinds.map((kind) => {
const secrets = sourcedSecrets.filter((secret) => secret.kind === kind);
// Will never be undefined because of the check before
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (secrets.length === 1) return secrets[0]!;
const decryptedSecrets = secretKinds
.map((kind) => {
const secrets = sourcedSecrets.filter((secret) => secret.kind === kind);
// Will never be undefined because of the check before
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (secrets.length === 1) return secrets[0]!;

// There will always be a matching secret because of the getSecretKindOption function
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return secrets.find((secret) => secret.source === "form") ?? secrets[0]!;
});
// There will always be a matching secret because of the getSecretKindOption function
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return secrets.find((secret) => secret.source === "form") ?? secrets[0]!;
})
.map(({ source: _, ...secret }) => secret);

const { secrets: _, ...baseIntegration } = integration;

// @ts-expect-error - For now we expect an error here as not all integerations have been implemented
const integrationInstance = integrationCreatorByKind(integration.kind, {
id: integration.id,
name: integration.name,
url: integration.url,
decryptedSecrets: filteredSecrets,
// @ts-expect-error - For now we expect an error here as not all integrations have been implemented
const integrationInstance = integrationCreator({
...baseIntegration,
decryptedSecrets,
});

await integrationInstance.testConnectionAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { describe, expect, test, vi } from "vitest";

import type { Session } from "@homarr/auth";
import { encryptSecret } from "@homarr/common";
import { encryptSecret } from "@homarr/common/server";
import { createId } from "@homarr/db";
import { integrations, integrationSecrets } from "@homarr/db/schema/sqlite";
import { createDb } from "@homarr/db/test";
Expand Down
Loading