mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-26 16:30:57 +01:00
feat(downloads): add option to limit amount of items (#3205)
This commit is contained in:
@@ -19,10 +19,11 @@ const createDownloadClientIntegrationMiddleware = (action: IntegrationAction) =>
|
||||
export const downloadsRouter = createTRPCRouter({
|
||||
getJobsAndStatuses: publicProcedure
|
||||
.concat(createDownloadClientIntegrationMiddleware("query"))
|
||||
.query(async ({ ctx }) => {
|
||||
.input(z.object({ limitPerIntegration: z.number().default(50) }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
return await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
const innerHandler = downloadClientRequestHandler.handler(integration, {});
|
||||
const innerHandler = downloadClientRequestHandler.handler(integration, { limit: input.limitPerIntegration });
|
||||
|
||||
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
|
||||
@@ -40,7 +41,8 @@ export const downloadsRouter = createTRPCRouter({
|
||||
}),
|
||||
subscribeToJobsAndStatuses: publicProcedure
|
||||
.concat(createDownloadClientIntegrationMiddleware("query"))
|
||||
.subscription(({ ctx }) => {
|
||||
.input(z.object({ limitPerIntegration: z.number().default(50) }))
|
||||
.subscription(({ ctx, input }) => {
|
||||
return observable<{
|
||||
integration: Modify<Integration, { kind: IntegrationKindByCategory<"downloadClient"> }>;
|
||||
data: DownloadClientJobsAndStatus;
|
||||
@@ -48,7 +50,9 @@ export const downloadsRouter = createTRPCRouter({
|
||||
const unsubscribes: (() => void)[] = [];
|
||||
for (const integrationWithSecrets of ctx.integrations) {
|
||||
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
|
||||
const innerHandler = downloadClientRequestHandler.handler(integrationWithSecrets, {});
|
||||
const innerHandler = downloadClientRequestHandler.handler(integrationWithSecrets, {
|
||||
limit: input.limitPerIntegration,
|
||||
});
|
||||
const unsubscribe = innerHandler.subscribe((data) => {
|
||||
emit.next({
|
||||
integration,
|
||||
|
||||
@@ -8,7 +8,9 @@ export const downloadsJob = createCronJob("downloads", EVERY_5_SECONDS).withCall
|
||||
createRequestIntegrationJobHandler(downloadClientRequestHandler.handler, {
|
||||
widgetKinds: ["downloads"],
|
||||
getInput: {
|
||||
downloads: () => ({}),
|
||||
downloads: (options) => ({
|
||||
limit: options.limitPerIntegration,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { DownloadClientItem } from "../../interfaces/downloads/download-cli
|
||||
import type { Aria2Download, Aria2GetClient } from "./aria2-types";
|
||||
|
||||
export class Aria2Integration extends DownloadClientIntegration {
|
||||
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
|
||||
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
|
||||
const client = this.getClient();
|
||||
const keys: (keyof Aria2Download)[] = [
|
||||
"bittorrent",
|
||||
@@ -27,12 +27,12 @@ export class Aria2Integration extends DownloadClientIntegration {
|
||||
];
|
||||
const [activeDownloads, waitingDownloads, stoppedDownloads, globalStats] = await Promise.all([
|
||||
client.tellActive(),
|
||||
client.tellWaiting(0, 1000, keys),
|
||||
client.tellStopped(0, 1000, keys),
|
||||
client.tellWaiting(0, input.limit, keys),
|
||||
client.tellStopped(0, input.limit, keys),
|
||||
client.getGlobalStat(),
|
||||
]);
|
||||
|
||||
const downloads = [...activeDownloads, ...waitingDownloads, ...stoppedDownloads];
|
||||
const downloads = [...activeDownloads, ...waitingDownloads, ...stoppedDownloads].slice(0, input.limit);
|
||||
const allPaused = downloads.every((download) => download.status === "paused");
|
||||
|
||||
return {
|
||||
|
||||
@@ -29,9 +29,10 @@ export class DelugeIntegration extends DownloadClientIntegration {
|
||||
};
|
||||
}
|
||||
|
||||
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
|
||||
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
|
||||
const type = "torrent";
|
||||
const client = await this.getClientAsync();
|
||||
// Currently there is no way to limit the number of returned torrents
|
||||
const {
|
||||
stats: { download_rate, upload_rate },
|
||||
torrents: rawTorrents,
|
||||
@@ -49,27 +50,29 @@ export class DelugeIntegration extends DownloadClientIntegration {
|
||||
},
|
||||
types: [type],
|
||||
};
|
||||
const items = torrents.map((torrent): DownloadClientItem => {
|
||||
const state = DelugeIntegration.getTorrentState(torrent.state);
|
||||
return {
|
||||
type,
|
||||
id: torrent.id,
|
||||
index: torrent.queue,
|
||||
name: torrent.name,
|
||||
size: torrent.total_wanted,
|
||||
sent: torrent.total_uploaded,
|
||||
downSpeed: torrent.progress !== 100 ? torrent.download_payload_rate : undefined,
|
||||
upSpeed: torrent.upload_payload_rate,
|
||||
time:
|
||||
torrent.progress === 100
|
||||
? Math.min((torrent.completed_time - dayjs().unix()) * 1000, -1)
|
||||
: Math.max(torrent.eta * 1000, 0),
|
||||
added: torrent.time_added * 1000,
|
||||
state,
|
||||
progress: torrent.progress / 100,
|
||||
category: torrent.label,
|
||||
};
|
||||
});
|
||||
const items = torrents
|
||||
.map((torrent): DownloadClientItem => {
|
||||
const state = DelugeIntegration.getTorrentState(torrent.state);
|
||||
return {
|
||||
type,
|
||||
id: torrent.id,
|
||||
index: torrent.queue,
|
||||
name: torrent.name,
|
||||
size: torrent.total_wanted,
|
||||
sent: torrent.total_uploaded,
|
||||
downSpeed: torrent.progress !== 100 ? torrent.download_payload_rate : undefined,
|
||||
upSpeed: torrent.upload_payload_rate,
|
||||
time:
|
||||
torrent.progress === 100
|
||||
? Math.min((torrent.completed_time - dayjs().unix()) * 1000, -1)
|
||||
: Math.max(torrent.eta * 1000, 0),
|
||||
added: torrent.time_added * 1000,
|
||||
state,
|
||||
progress: torrent.progress / 100,
|
||||
category: torrent.label,
|
||||
};
|
||||
})
|
||||
.slice(0, input.limit);
|
||||
return { status, items };
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export class NzbGetIntegration extends DownloadClientIntegration {
|
||||
};
|
||||
}
|
||||
|
||||
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
|
||||
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
|
||||
const type = "usenet";
|
||||
const queue = await this.nzbGetApiCallAsync("listgroups");
|
||||
const history = await this.nzbGetApiCallAsync("history");
|
||||
@@ -65,7 +65,8 @@ export class NzbGetIntegration extends DownloadClientIntegration {
|
||||
category: file.Category,
|
||||
};
|
||||
}),
|
||||
);
|
||||
)
|
||||
.slice(0, input.limit);
|
||||
return { status, items };
|
||||
}
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ export class QBitTorrentIntegration extends DownloadClientIntegration {
|
||||
};
|
||||
}
|
||||
|
||||
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
|
||||
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
|
||||
const type = "torrent";
|
||||
const client = await this.getClientAsync();
|
||||
const torrents = await client.listTorrents();
|
||||
const torrents = await client.listTorrents({ limit: input.limit });
|
||||
const rates = torrents.reduce(
|
||||
({ down, up }, { dlspeed, upspeed }) => ({ down: down + dlspeed, up: up + upspeed }),
|
||||
{ down: 0, up: 0 },
|
||||
|
||||
@@ -22,10 +22,14 @@ export class SabnzbdIntegration extends DownloadClientIntegration {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
|
||||
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
|
||||
const type = "usenet";
|
||||
const { queue } = await queueSchema.parseAsync(await this.sabNzbApiCallAsync("queue"));
|
||||
const { history } = await historySchema.parseAsync(await this.sabNzbApiCallAsync("history"));
|
||||
const { queue } = await queueSchema.parseAsync(
|
||||
await this.sabNzbApiCallAsync("queue", { limit: input.limit.toString() }),
|
||||
);
|
||||
const { history } = await historySchema.parseAsync(
|
||||
await this.sabNzbApiCallAsync("history", { limit: input.limit.toString() }),
|
||||
);
|
||||
const status: DownloadClientStatus = {
|
||||
paused: queue.paused,
|
||||
rates: { down: Math.floor(Number(queue.kbpersec) * 1024) }, //Actually rounded kiBps ()
|
||||
@@ -73,7 +77,8 @@ export class SabnzbdIntegration extends DownloadClientIntegration {
|
||||
category: slot.category,
|
||||
};
|
||||
}),
|
||||
);
|
||||
)
|
||||
.slice(0, input.limit);
|
||||
return { status, items };
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,10 @@ export class TransmissionIntegration extends DownloadClientIntegration {
|
||||
};
|
||||
}
|
||||
|
||||
public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
|
||||
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
|
||||
const type = "torrent";
|
||||
const client = await this.getClientAsync();
|
||||
// Currently there is no way to limit the number of returned torrents
|
||||
const { torrents } = (await client.listTorrents()).arguments;
|
||||
const rates = torrents.reduce(
|
||||
({ down, up }, { rateDownload, rateUpload }) => ({ down: down + rateDownload, up: up + rateUpload }),
|
||||
@@ -34,27 +35,29 @@ export class TransmissionIntegration extends DownloadClientIntegration {
|
||||
const paused =
|
||||
torrents.find(({ status }) => TransmissionIntegration.getTorrentState(status) !== "paused") === undefined;
|
||||
const status: DownloadClientStatus = { paused, rates, types: [type] };
|
||||
const items = torrents.map((torrent): DownloadClientItem => {
|
||||
const state = TransmissionIntegration.getTorrentState(torrent.status);
|
||||
return {
|
||||
type,
|
||||
id: torrent.hashString,
|
||||
index: torrent.queuePosition,
|
||||
name: torrent.name,
|
||||
size: torrent.totalSize,
|
||||
sent: torrent.uploadedEver,
|
||||
downSpeed: torrent.percentDone !== 1 ? torrent.rateDownload : undefined,
|
||||
upSpeed: torrent.rateUpload,
|
||||
time:
|
||||
torrent.percentDone === 1
|
||||
? Math.min(torrent.doneDate * 1000 - dayjs().valueOf(), -1)
|
||||
: Math.max(torrent.eta * 1000, 0),
|
||||
added: torrent.addedDate * 1000,
|
||||
state,
|
||||
progress: torrent.percentDone,
|
||||
category: torrent.labels,
|
||||
};
|
||||
});
|
||||
const items = torrents
|
||||
.map((torrent): DownloadClientItem => {
|
||||
const state = TransmissionIntegration.getTorrentState(torrent.status);
|
||||
return {
|
||||
type,
|
||||
id: torrent.hashString,
|
||||
index: torrent.queuePosition,
|
||||
name: torrent.name,
|
||||
size: torrent.totalSize,
|
||||
sent: torrent.uploadedEver,
|
||||
downSpeed: torrent.percentDone !== 1 ? torrent.rateDownload : undefined,
|
||||
upSpeed: torrent.rateUpload,
|
||||
time:
|
||||
torrent.percentDone === 1
|
||||
? Math.min(torrent.doneDate * 1000 - dayjs().valueOf(), -1)
|
||||
: Math.max(torrent.eta * 1000, 0),
|
||||
added: torrent.addedDate * 1000,
|
||||
state,
|
||||
progress: torrent.percentDone,
|
||||
category: torrent.labels,
|
||||
};
|
||||
})
|
||||
.slice(0, input.limit);
|
||||
return { status, items };
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { DownloadClientItem } from "./download-client-items";
|
||||
|
||||
export abstract class DownloadClientIntegration extends Integration {
|
||||
/** Get download client's status and list of all of it's items */
|
||||
public abstract getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus>;
|
||||
public abstract getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus>;
|
||||
/** Pauses the client or all of it's items */
|
||||
public abstract pauseQueueAsync(): Promise<void>;
|
||||
/** Pause a single item using it's ID */
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("Aria2 integration", () => {
|
||||
|
||||
// Acts
|
||||
const actAsync = async () => await aria2Integration.pauseQueueAsync();
|
||||
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync();
|
||||
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).resolves.not.toThrow();
|
||||
@@ -62,7 +62,7 @@ describe("Aria2 integration", () => {
|
||||
const aria2Integration = createAria2Intergration(startedContainer, API_KEY);
|
||||
|
||||
// Act
|
||||
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync();
|
||||
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||
|
||||
// Assert
|
||||
await expect(getAsync()).resolves.not.toThrow();
|
||||
@@ -81,7 +81,7 @@ describe("Aria2 integration", () => {
|
||||
await aria2AddItemAsync(startedContainer, API_KEY, aria2Integration);
|
||||
|
||||
// Act
|
||||
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync();
|
||||
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||
|
||||
// Assert
|
||||
await expect(getAsync()).resolves.not.toThrow();
|
||||
@@ -104,7 +104,7 @@ describe("Aria2 integration", () => {
|
||||
await expect(actAsync()).resolves.not.toThrow();
|
||||
// NzbGet is slow and we wait for a second before querying the items. Test was flaky without this.
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
const result = await aria2Integration.getClientJobsAndStatusAsync();
|
||||
const result = await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||
expect(result.items).toHaveLength(0);
|
||||
|
||||
// Cleanup
|
||||
@@ -153,7 +153,7 @@ const aria2AddItemAsync = async (container: StartedTestContainer, apiKey: string
|
||||
|
||||
const {
|
||||
items: [item],
|
||||
} = await integration.getClientJobsAndStatusAsync();
|
||||
} = await integration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||
|
||||
if (!item) {
|
||||
throw new Error("No item found");
|
||||
|
||||
@@ -69,7 +69,7 @@ describe("Nzbget integration", () => {
|
||||
|
||||
// Acts
|
||||
const actAsync = async () => await nzbGetIntegration.pauseQueueAsync();
|
||||
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync();
|
||||
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).resolves.not.toThrow();
|
||||
@@ -87,7 +87,7 @@ describe("Nzbget integration", () => {
|
||||
|
||||
// Acts
|
||||
const actAsync = async () => await nzbGetIntegration.resumeQueueAsync();
|
||||
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync();
|
||||
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).resolves.not.toThrow();
|
||||
@@ -105,7 +105,7 @@ describe("Nzbget integration", () => {
|
||||
const nzbGetIntegration = createNzbGetIntegration(startedContainer, username, password);
|
||||
|
||||
// Act
|
||||
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync();
|
||||
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||
|
||||
// Assert
|
||||
await expect(getAsync()).resolves.not.toThrow();
|
||||
@@ -124,7 +124,7 @@ describe("Nzbget integration", () => {
|
||||
await nzbGetAddItemAsync(startedContainer, username, password, nzbGetIntegration);
|
||||
|
||||
// Act
|
||||
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync();
|
||||
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||
|
||||
// Assert
|
||||
await expect(getAsync()).resolves.not.toThrow();
|
||||
@@ -147,7 +147,7 @@ describe("Nzbget integration", () => {
|
||||
await expect(actAsync()).resolves.not.toThrow();
|
||||
// NzbGet is slow and we wait for a second before querying the items. Test was flaky without this.
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
const result = await nzbGetIntegration.getClientJobsAndStatusAsync();
|
||||
const result = await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||
expect(result.items).toHaveLength(0);
|
||||
|
||||
// Cleanup
|
||||
@@ -209,7 +209,7 @@ const nzbGetAddItemAsync = async (
|
||||
|
||||
const {
|
||||
items: [item],
|
||||
} = await integration.getClientJobsAndStatusAsync();
|
||||
} = await integration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||
|
||||
if (!item) {
|
||||
throw new Error("No item found");
|
||||
|
||||
@@ -67,7 +67,7 @@ describe("Sabnzbd integration", () => {
|
||||
|
||||
// Acts
|
||||
const actAsync = async () => await sabnzbdIntegration.pauseQueueAsync();
|
||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
|
||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).resolves.not.toThrow();
|
||||
@@ -85,7 +85,7 @@ describe("Sabnzbd integration", () => {
|
||||
|
||||
// Acts
|
||||
const actAsync = async () => await sabnzbdIntegration.resumeQueueAsync();
|
||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
|
||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).resolves.not.toThrow();
|
||||
@@ -103,7 +103,7 @@ describe("Sabnzbd integration", () => {
|
||||
const sabnzbdIntegration = createSabnzbdIntegration(startedContainer, DEFAULT_API_KEY);
|
||||
|
||||
// Act
|
||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
|
||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||
|
||||
// Assert
|
||||
await expect(getAsync()).resolves.not.toThrow();
|
||||
@@ -122,7 +122,7 @@ describe("Sabnzbd integration", () => {
|
||||
await sabNzbdAddItemAsync(startedContainer, DEFAULT_API_KEY, sabnzbdIntegration);
|
||||
|
||||
// Act
|
||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
|
||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||
|
||||
// Assert
|
||||
await expect(getAsync()).resolves.not.toThrow();
|
||||
@@ -140,7 +140,7 @@ describe("Sabnzbd integration", () => {
|
||||
|
||||
// Act
|
||||
const actAsync = async () => await sabnzbdIntegration.pauseItemAsync(item);
|
||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
|
||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||
|
||||
// Assert
|
||||
await expect(getAsync()).resolves.toMatchObject({ items: [{ ...item, state: "downloading" }] });
|
||||
@@ -160,7 +160,7 @@ describe("Sabnzbd integration", () => {
|
||||
|
||||
// Act
|
||||
const actAsync = async () => await sabnzbdIntegration.resumeItemAsync(item);
|
||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
|
||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||
|
||||
// Assert
|
||||
await expect(getAsync()).resolves.toMatchObject({ items: [{ ...item, state: "paused" }] });
|
||||
@@ -180,7 +180,7 @@ describe("Sabnzbd integration", () => {
|
||||
// Act - fromDisk already doesn't work for sabnzbd, so only test deletion itself.
|
||||
const actAsync = async () =>
|
||||
await sabnzbdIntegration.deleteItemAsync({ ...item, progress: 0 } as DownloadClientItem, false);
|
||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
|
||||
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).resolves.not.toThrow();
|
||||
@@ -242,7 +242,7 @@ const sabNzbdAddItemAsync = async (
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const {
|
||||
items: [item],
|
||||
} = await integration.getClientJobsAndStatusAsync();
|
||||
} = await integration.getClientJobsAndStatusAsync({ limit: 99 });
|
||||
if (item) return item;
|
||||
}
|
||||
// Throws if it can't find the item
|
||||
|
||||
@@ -77,6 +77,7 @@ const optionMapping: OptionMapping = {
|
||||
descendingDefaultSort: () => false,
|
||||
showCompletedUsenet: () => true,
|
||||
showCompletedHttp: () => true,
|
||||
limitPerIntegration: () => undefined,
|
||||
},
|
||||
weather: {
|
||||
forecastDayCount: (oldOptions) => oldOptions.forecastDays,
|
||||
|
||||
@@ -9,11 +9,11 @@ import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-
|
||||
export const downloadClientRequestHandler = createCachedIntegrationRequestHandler<
|
||||
DownloadClientJobsAndStatus,
|
||||
IntegrationKindByCategory<"downloadClient">,
|
||||
Record<string, never>
|
||||
{ limit: number }
|
||||
>({
|
||||
async requestAsync(integration, _input) {
|
||||
async requestAsync(integration, input) {
|
||||
const integrationInstance = await createIntegrationAsync(integration);
|
||||
return await integrationInstance.getClientJobsAndStatusAsync();
|
||||
return await integrationInstance.getClientJobsAndStatusAsync(input);
|
||||
},
|
||||
cacheDuration: dayjs.duration(5, "seconds"),
|
||||
queryKey: "downloadClientJobStatus",
|
||||
|
||||
@@ -1953,6 +1953,10 @@
|
||||
},
|
||||
"applyFilterToRatio": {
|
||||
"label": "Use filter to calculate Ratio"
|
||||
},
|
||||
"limitPerIntegration": {
|
||||
"label": "Limit items per integration",
|
||||
"description": "This will limit the number of items shown per integration, not globally"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
|
||||
@@ -87,6 +87,7 @@ export default function DownloadClientsWidget({
|
||||
const [currentItems] = clientApi.widget.downloads.getJobsAndStatuses.useSuspenseQuery(
|
||||
{
|
||||
integrationIds,
|
||||
limitPerIntegration: options.limitPerIntegration,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
@@ -126,6 +127,7 @@ export default function DownloadClientsWidget({
|
||||
clientApi.widget.downloads.subscribeToJobsAndStatuses.useSubscription(
|
||||
{
|
||||
integrationIds,
|
||||
limitPerIntegration: options.limitPerIntegration,
|
||||
},
|
||||
{
|
||||
onData: (data) => {
|
||||
|
||||
@@ -82,6 +82,11 @@ export const { definition, componentLoader } = createWidgetDefinition("downloads
|
||||
applyFilterToRatio: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
limitPerIntegration: factory.number({
|
||||
defaultValue: 50,
|
||||
validate: z.number().min(1),
|
||||
withDescription: true,
|
||||
}),
|
||||
}),
|
||||
{
|
||||
defaultSort: {
|
||||
|
||||
@@ -43,7 +43,7 @@ interface SelectInput<TOptions extends readonly SelectOption[]>
|
||||
searchable?: boolean;
|
||||
}
|
||||
|
||||
interface NumberInput extends CommonInput<number | ""> {
|
||||
interface NumberInput extends CommonInput<number> {
|
||||
validate: z.ZodNumber;
|
||||
step?: number;
|
||||
}
|
||||
@@ -87,7 +87,7 @@ const optionsFactory = {
|
||||
}),
|
||||
number: (input: NumberInput) => ({
|
||||
type: "number" as const,
|
||||
defaultValue: input.defaultValue ?? ("" as const),
|
||||
defaultValue: input.defaultValue ?? 0,
|
||||
step: input.step,
|
||||
withDescription: input.withDescription ?? false,
|
||||
validate: input.validate,
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function RssFeed({ options }: WidgetComponentProps<"rssFeed">) {
|
||||
dir={languageDir}
|
||||
c="dimmed"
|
||||
size="sm"
|
||||
lineClamp={options.textLinesClamp as number}
|
||||
lineClamp={options.textLinesClamp}
|
||||
dangerouslySetInnerHTML={{ __html: feedEntry.description }}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user