chore(release): automatic release v1.1.0
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import "@homarr/auth/env";
|
||||
import "@homarr/db/env";
|
||||
import "@homarr/common/env";
|
||||
import "@homarr/docker/env";
|
||||
|
||||
import type { NextConfig } from "next";
|
||||
import MillionLint from "@million/lint";
|
||||
|
||||
@@ -23,8 +23,10 @@
|
||||
"@homarr/cron-job-status": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/definitions": "workspace:^0.1.0",
|
||||
"@homarr/docker": "workspace:^0.1.0",
|
||||
"@homarr/form": "workspace:^0.1.0",
|
||||
"@homarr/gridstack": "^1.11.3",
|
||||
"@homarr/gridstack": "^1.12.0",
|
||||
"@homarr/icons": "workspace:^0.1.0",
|
||||
"@homarr/integrations": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^",
|
||||
"@homarr/modals": "workspace:^0.1.0",
|
||||
@@ -39,18 +41,18 @@
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@homarr/widgets": "workspace:^0.1.0",
|
||||
"@mantine/colors-generator": "^7.16.0",
|
||||
"@mantine/core": "^7.16.0",
|
||||
"@mantine/dropzone": "^7.16.0",
|
||||
"@mantine/hooks": "^7.16.0",
|
||||
"@mantine/modals": "^7.16.0",
|
||||
"@mantine/tiptap": "^7.16.0",
|
||||
"@mantine/colors-generator": "^7.16.1",
|
||||
"@mantine/core": "^7.16.1",
|
||||
"@mantine/dropzone": "^7.16.1",
|
||||
"@mantine/hooks": "^7.16.1",
|
||||
"@mantine/modals": "^7.16.1",
|
||||
"@mantine/tiptap": "^7.16.1",
|
||||
"@million/lint": "1.0.14",
|
||||
"@t3-oss/env-nextjs": "^0.11.1",
|
||||
"@tabler/icons-react": "^3.28.1",
|
||||
"@tanstack/react-query": "^5.64.1",
|
||||
"@tanstack/react-query-devtools": "^5.64.1",
|
||||
"@tanstack/react-query-next-experimental": "5.64.1",
|
||||
"@tabler/icons-react": "^3.29.0",
|
||||
"@tanstack/react-query": "^5.64.2",
|
||||
"@tanstack/react-query-devtools": "^5.64.2",
|
||||
"@tanstack/react-query-next-experimental": "5.64.2",
|
||||
"@trpc/client": "next",
|
||||
"@trpc/next": "next",
|
||||
"@trpc/react-query": "next",
|
||||
@@ -64,9 +66,9 @@
|
||||
"dotenv": "^16.4.7",
|
||||
"flag-icons": "^7.3.2",
|
||||
"glob": "^11.0.1",
|
||||
"jotai": "^2.11.0",
|
||||
"jotai": "^2.11.1",
|
||||
"mantine-react-table": "2.0.0-beta.8",
|
||||
"next": "15.1.5",
|
||||
"next": "15.1.6",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "19.0.0",
|
||||
|
||||
@@ -94,6 +94,7 @@ export function TRPCReactProvider(props: PropsWithChildren) {
|
||||
false: unstable_httpBatchStreamLink({
|
||||
transformer: superjson,
|
||||
url: getTrpcUrl(),
|
||||
maxURLLength: 2083, // Suggested by tRPC: https://trpc.io/docs/client/links/httpBatchLink#setting-a-maximum-url-length
|
||||
headers: createHeadersCallbackForSource("nextjs-react (json)"),
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import type { MouseEvent } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Group, Menu } from "@mantine/core";
|
||||
import { useHotkeys } from "@mantine/hooks";
|
||||
@@ -9,9 +10,11 @@ import {
|
||||
IconBox,
|
||||
IconBoxAlignTop,
|
||||
IconChevronDown,
|
||||
IconLayoutBoard,
|
||||
IconPencil,
|
||||
IconPencilOff,
|
||||
IconPlus,
|
||||
IconReplace,
|
||||
IconResize,
|
||||
IconSettings,
|
||||
} from "@tabler/icons-react";
|
||||
@@ -49,6 +52,8 @@ export const BoardContentHeaderActions = () => {
|
||||
<HeaderButton href={`/boards/${board.name}/settings`}>
|
||||
<IconSettings stroke={1.5} />
|
||||
</HeaderButton>
|
||||
|
||||
<SelectBoardsMenu />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -151,6 +156,32 @@ const EditModeMenu = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const SelectBoardsMenu = () => {
|
||||
const { data: boards = [] } = clientApi.board.getAllBoards.useQuery();
|
||||
|
||||
return (
|
||||
<Menu position="bottom-end" withArrow>
|
||||
<Menu.Target>
|
||||
<HeaderButton w="auto" px={4}>
|
||||
<IconReplace stroke={1.5} />
|
||||
</HeaderButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown style={{ transform: "translate(-7px, 0)" }}>
|
||||
{boards.map((board) => (
|
||||
<Menu.Item
|
||||
key={board.id}
|
||||
component={Link}
|
||||
href={`/boards/${board.name}`}
|
||||
leftSection={<IconLayoutBoard size={20} />}
|
||||
>
|
||||
{board.name}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const usePreventLeaveWithDirty = (isDirty: boolean) => {
|
||||
const t = useI18n();
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
@@ -15,7 +15,9 @@ export const IntegrationCreateDropdownContent = () => {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filteredKinds = useMemo(() => {
|
||||
return integrationKinds.filter((kind) => kind.includes(search.toLowerCase()));
|
||||
return integrationKinds.filter((kind) =>
|
||||
getIntegrationName(kind).toLowerCase().includes(search.toLowerCase().trim()),
|
||||
);
|
||||
}, [search]);
|
||||
|
||||
const handleSearch = React.useCallback(
|
||||
@@ -29,6 +31,7 @@ export const IntegrationCreateDropdownContent = () => {
|
||||
leftSection={<IconSearch stroke={1.5} size={20} />}
|
||||
placeholder={t("integration.page.list.search")}
|
||||
value={search}
|
||||
data-autofocus
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
|
||||
|
||||
@@ -102,7 +102,15 @@ export default async function IntegrationsPage(props: IntegrationsPageProps) {
|
||||
|
||||
const IntegrationSelectMenu = ({ children }: PropsWithChildren) => {
|
||||
return (
|
||||
<Menu width={256} trapFocus position="bottom-end" withinPortal shadow="md" keepMounted={false}>
|
||||
<Menu
|
||||
width={256}
|
||||
trapFocus
|
||||
position="bottom-end"
|
||||
withinPortal
|
||||
shadow="md"
|
||||
keepMounted={false}
|
||||
withInitialFocusPlaceholder={false}
|
||||
>
|
||||
{children}
|
||||
<MenuDropdown>
|
||||
<IntegrationCreateDropdownContent />
|
||||
|
||||
@@ -15,6 +15,11 @@ export const CopyMedia = ({ media }: CopyMediaProps) => {
|
||||
|
||||
const url = typeof window !== "undefined" ? `${window.location.origin}/api/user-medias/${media.id}` : "";
|
||||
|
||||
// Clipboard only works on localhost or secure connections (https)
|
||||
if (url.startsWith("http://") && !url.startsWith("http://localhost")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CopyButton value={url}>
|
||||
{({ copy, copied }) => (
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Anchor, Group, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from "@mantine/core";
|
||||
import {
|
||||
ActionIcon,
|
||||
Anchor,
|
||||
Group,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { IconExternalLink } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { humanFileSize } from "@homarr/common";
|
||||
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
|
||||
import { createLocalImageUrl } from "@homarr/icons/local";
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
import { SearchInput, TablePagination, UserAvatar } from "@homarr/ui";
|
||||
import { z } from "@homarr/validation";
|
||||
@@ -91,13 +106,14 @@ interface RowProps {
|
||||
|
||||
const Row = async ({ media }: RowProps) => {
|
||||
const session = await auth();
|
||||
const t = await getI18n();
|
||||
const canDelete = media.creatorId === session?.user.id || session?.user.permissions.includes("media-full-all");
|
||||
|
||||
return (
|
||||
<TableTr>
|
||||
<TableTd w={64}>
|
||||
<Image
|
||||
src={`/api/user-medias/${media.id}`}
|
||||
src={createLocalImageUrl(media.id)}
|
||||
alt={media.name}
|
||||
width={64}
|
||||
height={64}
|
||||
@@ -121,6 +137,17 @@ const Row = async ({ media }: RowProps) => {
|
||||
<TableTd w={64}>
|
||||
<Group wrap="nowrap" gap="xs">
|
||||
<CopyMedia media={media} />
|
||||
<Tooltip label={t("media.action.open.label")} openDelay={500}>
|
||||
<ActionIcon
|
||||
component="a"
|
||||
href={createLocalImageUrl(media.id)}
|
||||
target="_blank"
|
||||
color="gray"
|
||||
variant="subtle"
|
||||
>
|
||||
<IconExternalLink size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{canDelete && <DeleteMedia media={media} />}
|
||||
</Group>
|
||||
</TableTd>
|
||||
|
||||
@@ -59,10 +59,17 @@ export default async function CertificatesPage({ params }: CertificatesPageProps
|
||||
{x509Certificates.map((cert) => (
|
||||
<Card key={cert.x509.fingerprint} withBorder>
|
||||
<Group wrap="nowrap">
|
||||
<IconCertificate color={getMantineColor(iconColor(cert.x509.validToDate), 6)} size={32} stroke={1.5} />
|
||||
<Stack flex={1} gap="xs">
|
||||
<Group justify="space-between">
|
||||
<Text fw={500}>{cert.x509.subject}</Text>
|
||||
<IconCertificate
|
||||
color={getMantineColor(iconColor(cert.x509.validToDate), 6)}
|
||||
style={{ minWidth: 32 }}
|
||||
size={32}
|
||||
stroke={1.5}
|
||||
/>
|
||||
<Stack flex={1} gap="xs" maw="calc(100% - 48px)">
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Text fw={500} lineClamp={1} style={{ wordBreak: "break-all" }}>
|
||||
{cert.x509.subject}
|
||||
</Text>
|
||||
<Text c="gray.6" ta="end" size="sm">
|
||||
{cert.fileName}
|
||||
</Text>
|
||||
|
||||
@@ -16,7 +16,7 @@ import { MantineReactTable } from "mantine-react-table";
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useTimeAgo } from "@homarr/common";
|
||||
import type { DockerContainerState } from "@homarr/definitions";
|
||||
import type { ContainerState } from "@homarr/docker";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { AddDockerAppToHomarr } from "@homarr/modals-collection";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
@@ -246,9 +246,9 @@ const containerStates = {
|
||||
exited: "red",
|
||||
removing: "pink",
|
||||
dead: "dark",
|
||||
} satisfies Record<DockerContainerState, MantineColor>;
|
||||
} satisfies Record<ContainerState, MantineColor>;
|
||||
|
||||
const ContainerStateBadge = ({ state }: { state: DockerContainerState }) => {
|
||||
const ContainerStateBadge = ({ state }: { state: ContainerState }) => {
|
||||
const t = useScopedI18n("docker.field.state.option");
|
||||
|
||||
return (
|
||||
|
||||
@@ -51,7 +51,10 @@ export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => {
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack gap="md">
|
||||
<Switch {...form.getInputProps("pingIconsEnabled")} label={t("user.field.pingIconsEnabled.label")} />
|
||||
<Switch
|
||||
{...form.getInputProps("pingIconsEnabled", { type: "checkbox" })}
|
||||
label={t("user.field.pingIconsEnabled.label")}
|
||||
/>
|
||||
|
||||
<Group justify="end">
|
||||
<Button type="submit" color="teal" loading={isPending}>
|
||||
|
||||
@@ -83,10 +83,12 @@ export default async function EditUserPage(props: Props) {
|
||||
</Box>
|
||||
</Group>
|
||||
|
||||
<Stack mb="lg">
|
||||
<Title order={2}>{tGeneral("item.language")}</Title>
|
||||
<CurrentLanguageCombobox />
|
||||
</Stack>
|
||||
{session?.user.id === user.id && (
|
||||
<Stack mb="lg">
|
||||
<Title order={2}>{tGeneral("item.language")}</Title>
|
||||
<CurrentLanguageCombobox />
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Stack mb="lg">
|
||||
<Title order={2}>{tGeneral("item.board.title")}</Title>
|
||||
|
||||
@@ -30,7 +30,9 @@ const handler = auth(async (req) => {
|
||||
req,
|
||||
createContext: () => createTRPCContext({ session: req.auth, headers: req.headers }),
|
||||
onError({ error, path, type }) {
|
||||
logger.error(`tRPC Error with ${type} on '${path}': (${error.code}) - ${error.message}`);
|
||||
logger.error(
|
||||
`tRPC Error with ${type} on '${path}': (${error.code}) - ${error.message}\n${error.stack}\n${error.cause}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { Modify } from "@homarr/common/types";
|
||||
import { createId } from "@homarr/db/client";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
|
||||
import type { Board, DynamicSection, EmptySection, Item } from "~/app/[locale]/boards/_types";
|
||||
import { getFirstEmptyPosition } from "./empty-position";
|
||||
|
||||
export interface CreateItemInput {
|
||||
kind: WidgetKind;
|
||||
}
|
||||
|
||||
export const createItemCallback =
|
||||
({ kind }: CreateItemInput) =>
|
||||
(previous: Board): Board => {
|
||||
const firstSection = previous.sections
|
||||
.filter((section): section is EmptySection => section.kind === "empty")
|
||||
.sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset)
|
||||
.at(0);
|
||||
|
||||
if (!firstSection) return previous;
|
||||
|
||||
const dynamicSectionsOfFirstSection = previous.sections.filter(
|
||||
(section): section is DynamicSection => section.kind === "dynamic" && section.parentSectionId === firstSection.id,
|
||||
);
|
||||
const elements = [...firstSection.items, ...dynamicSectionsOfFirstSection];
|
||||
const emptyPosition = getFirstEmptyPosition(elements, previous.columnCount);
|
||||
|
||||
if (!emptyPosition) {
|
||||
console.error("Your board is full");
|
||||
return previous;
|
||||
}
|
||||
|
||||
const widget = {
|
||||
id: createId(),
|
||||
kind,
|
||||
options: {},
|
||||
width: 1,
|
||||
height: 1,
|
||||
...emptyPosition,
|
||||
integrationIds: [],
|
||||
advancedOptions: {
|
||||
customCssClasses: [],
|
||||
},
|
||||
} satisfies Modify<
|
||||
Item,
|
||||
{
|
||||
kind: WidgetKind;
|
||||
}
|
||||
>;
|
||||
|
||||
return {
|
||||
...previous,
|
||||
sections: previous.sections.map((section) => {
|
||||
// Return same section if item is not in it
|
||||
if (section.id !== firstSection.id) return section;
|
||||
return {
|
||||
...section,
|
||||
items: section.items.concat(widget),
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import { createId } from "@homarr/db/client";
|
||||
|
||||
import type { Board, DynamicSection, EmptySection } from "~/app/[locale]/boards/_types";
|
||||
import { getFirstEmptyPosition } from "./empty-position";
|
||||
|
||||
export interface DuplicateItemInput {
|
||||
itemId: string;
|
||||
}
|
||||
|
||||
export const duplicateItemCallback =
|
||||
({ itemId }: DuplicateItemInput) =>
|
||||
(previous: Board): Board => {
|
||||
const itemToDuplicate = previous.sections
|
||||
.flatMap((section) => section.items.map((item) => ({ ...item, sectionId: section.id })))
|
||||
.find((item) => item.id === itemId);
|
||||
if (!itemToDuplicate) return previous;
|
||||
|
||||
const currentSection = previous.sections.find((section) => section.id === itemToDuplicate.sectionId);
|
||||
if (!currentSection) return previous;
|
||||
|
||||
const dynamicSectionsOfCurrentSection = previous.sections.filter(
|
||||
(section): section is DynamicSection =>
|
||||
section.kind === "dynamic" && section.parentSectionId === currentSection.id,
|
||||
);
|
||||
const elements = [...currentSection.items, ...dynamicSectionsOfCurrentSection];
|
||||
let sectionId = currentSection.id;
|
||||
let emptyPosition = getFirstEmptyPosition(
|
||||
elements,
|
||||
currentSection.kind === "dynamic" ? currentSection.width : previous.columnCount,
|
||||
currentSection.kind === "dynamic" ? currentSection.height : undefined,
|
||||
{
|
||||
width: itemToDuplicate.width,
|
||||
height: itemToDuplicate.height,
|
||||
},
|
||||
);
|
||||
|
||||
if (!emptyPosition) {
|
||||
const firstSection = previous.sections
|
||||
.filter((section): section is EmptySection => section.kind === "empty")
|
||||
.sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset)
|
||||
.at(0);
|
||||
|
||||
if (!firstSection) return previous;
|
||||
|
||||
const dynamicSectionsOfFirstSection = previous.sections.filter(
|
||||
(section): section is DynamicSection =>
|
||||
section.kind === "dynamic" && section.parentSectionId === firstSection.id,
|
||||
);
|
||||
const elements = [...firstSection.items, ...dynamicSectionsOfFirstSection];
|
||||
emptyPosition = getFirstEmptyPosition(elements, previous.columnCount, undefined, {
|
||||
width: itemToDuplicate.width,
|
||||
height: itemToDuplicate.height,
|
||||
});
|
||||
if (!emptyPosition) {
|
||||
console.error("Your board is full");
|
||||
return previous;
|
||||
}
|
||||
|
||||
sectionId = firstSection.id;
|
||||
}
|
||||
|
||||
const widget = structuredClone(itemToDuplicate);
|
||||
widget.id = createId();
|
||||
widget.xOffset = emptyPosition.xOffset;
|
||||
widget.yOffset = emptyPosition.yOffset;
|
||||
widget.sectionId = sectionId;
|
||||
|
||||
const result = {
|
||||
...previous,
|
||||
sections: previous.sections.map((section) => {
|
||||
// Return same section if item is not in it
|
||||
if (section.id !== sectionId) return section;
|
||||
return {
|
||||
...section,
|
||||
items: section.items.concat(widget),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Item } from "~/app/[locale]/boards/_types";
|
||||
|
||||
export const getFirstEmptyPosition = (
|
||||
elements: Pick<Item, "yOffset" | "xOffset" | "width" | "height">[],
|
||||
columnCount: number,
|
||||
rowCount = 9999,
|
||||
size: { width: number; height: number } = { width: 1, height: 1 },
|
||||
) => {
|
||||
for (let yOffset = 0; yOffset < rowCount + 1 - size.height; yOffset++) {
|
||||
for (let xOffset = 0; xOffset < columnCount; xOffset++) {
|
||||
const isOccupied = elements.some(
|
||||
(element) =>
|
||||
element.yOffset < yOffset + size.height &&
|
||||
element.yOffset + element.height > yOffset &&
|
||||
element.xOffset < xOffset + size.width &&
|
||||
element.xOffset + element.width > xOffset,
|
||||
);
|
||||
|
||||
if (!isOccupied) {
|
||||
return { xOffset, yOffset };
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import type { Board } from "~/app/[locale]/boards/_types";
|
||||
import { createItemCallback } from "../create-item";
|
||||
import * as emptyPosition from "../empty-position";
|
||||
import { createDynamicSection, createEmptySection, createItem } from "./shared";
|
||||
|
||||
describe("item actions create-item", () => {
|
||||
test("should add it to first section", () => {
|
||||
const spy = vi.spyOn(emptyPosition, "getFirstEmptyPosition");
|
||||
spy.mockReturnValue({ xOffset: 5, yOffset: 5 });
|
||||
const input = {
|
||||
sections: [createEmptySection("1", 2), createEmptySection("2", 0), createEmptySection("3", 1)],
|
||||
columnCount: 4,
|
||||
} satisfies Pick<Board, "sections" | "columnCount">;
|
||||
|
||||
const result = createItemCallback({
|
||||
kind: "clock",
|
||||
})(input as unknown as Board);
|
||||
|
||||
const firstSection = result.sections.find((section) => section.id === "2");
|
||||
const item = firstSection?.items.at(0);
|
||||
expect(item).toEqual(expect.objectContaining({ kind: "clock", xOffset: 5, yOffset: 5 }));
|
||||
expect(spy).toHaveBeenCalledWith([], input.columnCount);
|
||||
});
|
||||
test("should correctly pass dynamic section and items to getFirstEmptyPosition", () => {
|
||||
const spy = vi.spyOn(emptyPosition, "getFirstEmptyPosition");
|
||||
spy.mockReturnValue({ xOffset: 5, yOffset: 5 });
|
||||
const firstSection = createEmptySection("2", 0);
|
||||
const expectedItem = createItem({ id: "12", xOffset: 1, yOffset: 2, width: 3, height: 2 });
|
||||
firstSection.items.push(expectedItem);
|
||||
const dynamicSectionInFirst = createDynamicSection({
|
||||
id: "4",
|
||||
parentSectionId: "2",
|
||||
yOffset: 0,
|
||||
xOffset: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
});
|
||||
|
||||
const input = {
|
||||
sections: [
|
||||
createEmptySection("1", 2),
|
||||
firstSection,
|
||||
createEmptySection("3", 1),
|
||||
dynamicSectionInFirst,
|
||||
createDynamicSection({ id: "5", parentSectionId: "3", yOffset: 1 }),
|
||||
],
|
||||
columnCount: 4,
|
||||
} satisfies Pick<Board, "sections" | "columnCount">;
|
||||
|
||||
const result = createItemCallback({
|
||||
kind: "clock",
|
||||
})(input as unknown as Board);
|
||||
|
||||
const firstSectionResult = result.sections.find((section) => section.id === "2");
|
||||
const item = firstSectionResult?.items.find((item) => item.id !== "12");
|
||||
expect(item).toEqual(expect.objectContaining({ kind: "clock", xOffset: 5, yOffset: 5 }));
|
||||
expect(spy).toHaveBeenCalledWith([expectedItem, dynamicSectionInFirst], input.columnCount);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import type { Board } from "~/app/[locale]/boards/_types";
|
||||
import { duplicateItemCallback } from "../duplicate-item";
|
||||
import * as emptyPosition from "../empty-position";
|
||||
import { createEmptySection, createItem } from "./shared";
|
||||
|
||||
describe("item actions duplicate-item", () => {
|
||||
test("should copy it in the same section", () => {
|
||||
const spy = vi.spyOn(emptyPosition, "getFirstEmptyPosition");
|
||||
spy.mockReturnValue({ xOffset: 5, yOffset: 5 });
|
||||
const currentSection = createEmptySection("2", 1);
|
||||
const currentItem = createItem({
|
||||
id: "1",
|
||||
xOffset: 1,
|
||||
yOffset: 3,
|
||||
width: 3,
|
||||
height: 2,
|
||||
kind: "minecraftServerStatus",
|
||||
integrationIds: ["1"],
|
||||
options: { address: "localhost" },
|
||||
advancedOptions: { customCssClasses: ["test"] },
|
||||
});
|
||||
const otherItem = createItem({
|
||||
id: "2",
|
||||
});
|
||||
currentSection.items.push(currentItem, otherItem);
|
||||
const input = {
|
||||
columnCount: 10,
|
||||
sections: [createEmptySection("1", 0), currentSection, createEmptySection("3", 2)],
|
||||
} satisfies Pick<Board, "sections" | "columnCount">;
|
||||
|
||||
const result = duplicateItemCallback({ itemId: currentItem.id })(input as unknown as Board);
|
||||
|
||||
const section = result.sections.find((section) => section.id === "2");
|
||||
expect(section?.items.length).toBe(3);
|
||||
const duplicatedItem = section?.items.find((item) => item.id !== currentItem.id && item.id !== otherItem.id);
|
||||
|
||||
expect(duplicatedItem).toEqual(
|
||||
expect.objectContaining({
|
||||
kind: "minecraftServerStatus",
|
||||
xOffset: 5,
|
||||
yOffset: 5,
|
||||
width: 3,
|
||||
height: 2,
|
||||
integrationIds: ["1"],
|
||||
options: { address: "localhost" },
|
||||
advancedOptions: { customCssClasses: ["test"] },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import type { Item } from "~/app/[locale]/boards/_types";
|
||||
import { getFirstEmptyPosition } from "../empty-position";
|
||||
|
||||
describe("get first empty position", () => {
|
||||
test.each([
|
||||
[[[" ", " ", " ", " "]], [1, 1], 0, 0],
|
||||
[[["a", " ", " ", " "]], [1, 1], 1, 0],
|
||||
[[[" ", "a", " ", " "]], [1, 1], 0, 0],
|
||||
[
|
||||
[
|
||||
["a", "a", " ", " "],
|
||||
["a", "a", " ", " "],
|
||||
],
|
||||
[1, 1],
|
||||
2,
|
||||
0,
|
||||
],
|
||||
[[["a", "a", "a", "a"]], [1, 1], 0, 1],
|
||||
[
|
||||
[
|
||||
["a", "a", "a", "a"],
|
||||
["a", "a", "a", "a"],
|
||||
],
|
||||
[1, 1],
|
||||
0,
|
||||
2,
|
||||
],
|
||||
[
|
||||
[
|
||||
["a", "a", " ", "b", "b"],
|
||||
["a", "a", " ", "b", "b"],
|
||||
],
|
||||
[1, 2],
|
||||
2,
|
||||
0,
|
||||
],
|
||||
[
|
||||
[
|
||||
["a", "a", " ", " ", "b", "b"],
|
||||
["a", "a", " ", " ", "b", "b"],
|
||||
],
|
||||
[2, 2],
|
||||
2,
|
||||
0,
|
||||
],
|
||||
[
|
||||
[
|
||||
["a", "a", " ", "d", "b", "b"],
|
||||
["a", "a", "c", "e", "b", "b"],
|
||||
],
|
||||
[1, 1],
|
||||
2,
|
||||
0,
|
||||
],
|
||||
[
|
||||
[
|
||||
["a", "a", " ", " ", "b", "b"],
|
||||
["a", "a", " ", "e", "b", "b"],
|
||||
],
|
||||
[2, 2],
|
||||
0,
|
||||
2,
|
||||
],
|
||||
])("should return the first empty position", (layout, size, expectedX, expectedY) => {
|
||||
const elements = createElementsFromLayout(layout);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const result = getFirstEmptyPosition(elements, layout[0]!.length, undefined, { width: size[0]!, height: size[1]! });
|
||||
|
||||
expect(result).toEqual({ xOffset: expectedX, yOffset: expectedY });
|
||||
});
|
||||
|
||||
test.each([
|
||||
[[[" ", " "]], [1, 1], 0, 0, 1],
|
||||
[[["a", " "]], [1, 1], 1, 0, 1],
|
||||
[[["a", "a"]], [1, 1], undefined, undefined, 1],
|
||||
[[["a", "a"]], [1, 1], 0, 1, 2],
|
||||
[
|
||||
[
|
||||
["a", "b", " ", " "],
|
||||
["a", "c", " ", "d"],
|
||||
],
|
||||
[2, 2],
|
||||
undefined,
|
||||
undefined,
|
||||
3,
|
||||
],
|
||||
[
|
||||
[
|
||||
["a", "b", " ", " "],
|
||||
["a", "c", " ", "d"],
|
||||
],
|
||||
[2, 2],
|
||||
0,
|
||||
2,
|
||||
4,
|
||||
],
|
||||
[[["a", "b"]], [2, 1], 0, 1, 2],
|
||||
])("should return the first empty position with limited rows", (layout, size, expectedX, expectedY, rowCount) => {
|
||||
const elements = createElementsFromLayout(layout);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const result = getFirstEmptyPosition(elements, layout[0]!.length, rowCount, { width: size[0]!, height: size[1]! });
|
||||
|
||||
expect(result).toEqual(expectedX !== undefined ? { xOffset: expectedX, yOffset: expectedY } : undefined);
|
||||
});
|
||||
});
|
||||
|
||||
const createElementsFromLayout = (layout: string[][]) => {
|
||||
const elements: (Pick<Item, "xOffset" | "yOffset" | "width" | "height"> & { char: string })[] = [];
|
||||
for (let yOffset = 0; yOffset < layout.length; yOffset++) {
|
||||
const row = layout[yOffset];
|
||||
if (!row) continue;
|
||||
for (let xOffset = 0; xOffset < row.length; xOffset++) {
|
||||
const item = row[xOffset];
|
||||
if (item === " " || !item) continue;
|
||||
|
||||
const existing = elements.find((element) => element.char === item);
|
||||
if (existing) {
|
||||
existing.height = yOffset - existing.yOffset + 1;
|
||||
existing.width = xOffset - existing.xOffset + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
elements.push({
|
||||
yOffset,
|
||||
xOffset,
|
||||
width: 1,
|
||||
height: 1,
|
||||
char: item,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { DynamicSection, EmptySection, Item } from "~/app/[locale]/boards/_types";
|
||||
|
||||
export const createEmptySection = (id: string, yOffset: number): EmptySection => ({
|
||||
id,
|
||||
kind: "empty",
|
||||
yOffset,
|
||||
xOffset: 0,
|
||||
items: [],
|
||||
});
|
||||
|
||||
export const createDynamicSection = (section: Omit<Partial<DynamicSection>, "kind">): DynamicSection => ({
|
||||
id: section.id ?? "0",
|
||||
kind: "dynamic",
|
||||
parentSectionId: section.parentSectionId ?? "0",
|
||||
height: section.height ?? 1,
|
||||
width: section.width ?? 1,
|
||||
yOffset: section.yOffset ?? 0,
|
||||
xOffset: section.xOffset ?? 0,
|
||||
items: section.items ?? [],
|
||||
});
|
||||
|
||||
export const createItem = (item: Partial<Item>): Item => ({
|
||||
id: item.id ?? "0",
|
||||
width: item.width ?? 1,
|
||||
height: item.height ?? 1,
|
||||
yOffset: item.yOffset ?? 0,
|
||||
xOffset: item.xOffset ?? 0,
|
||||
kind: item.kind ?? "clock",
|
||||
integrationIds: item.integrationIds ?? [],
|
||||
options: item.options ?? {},
|
||||
advancedOptions: item.advancedOptions ?? { customCssClasses: [] },
|
||||
});
|
||||
@@ -1,12 +1,13 @@
|
||||
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";
|
||||
|
||||
import type { EmptySection, Item } from "~/app/[locale]/boards/_types";
|
||||
import type { Item } from "~/app/[locale]/boards/_types";
|
||||
import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client";
|
||||
import type { CreateItemInput } from "./actions/create-item";
|
||||
import { createItemCallback } from "./actions/create-item";
|
||||
import type { DuplicateItemInput } from "./actions/duplicate-item";
|
||||
import { duplicateItemCallback } from "./actions/duplicate-item";
|
||||
|
||||
interface MoveAndResizeItem {
|
||||
itemId: string;
|
||||
@@ -42,87 +43,19 @@ interface UpdateItemIntegrations {
|
||||
newIntegrations: string[];
|
||||
}
|
||||
|
||||
interface CreateItem {
|
||||
kind: WidgetKind;
|
||||
}
|
||||
|
||||
interface DuplicateItem {
|
||||
itemId: string;
|
||||
}
|
||||
|
||||
export const useItemActions = () => {
|
||||
const { updateBoard } = useUpdateBoard();
|
||||
|
||||
const createItem = useCallback(
|
||||
({ kind }: CreateItem) => {
|
||||
updateBoard((previous) => {
|
||||
const lastSection = previous.sections
|
||||
.filter((section): section is EmptySection => section.kind === "empty")
|
||||
.sort((sectionA, sectionB) => sectionB.yOffset - sectionA.yOffset)[0];
|
||||
|
||||
if (!lastSection) return previous;
|
||||
|
||||
const widget = {
|
||||
id: createId(),
|
||||
kind,
|
||||
options: {},
|
||||
width: 1,
|
||||
height: 1,
|
||||
integrationIds: [],
|
||||
advancedOptions: {
|
||||
customCssClasses: [],
|
||||
},
|
||||
} satisfies Modify<
|
||||
Omit<Item, "yOffset" | "xOffset">,
|
||||
{
|
||||
kind: WidgetKind;
|
||||
}
|
||||
>;
|
||||
|
||||
return {
|
||||
...previous,
|
||||
sections: previous.sections.map((section) => {
|
||||
// Return same section if item is not in it
|
||||
if (section.id !== lastSection.id) return section;
|
||||
return {
|
||||
...section,
|
||||
items: section.items.concat(widget as unknown as Item),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
(input: CreateItemInput) => {
|
||||
updateBoard(createItemCallback(input));
|
||||
},
|
||||
[updateBoard],
|
||||
);
|
||||
|
||||
const duplicateItem = useCallback(
|
||||
({ itemId }: DuplicateItem) => {
|
||||
updateBoard((previous) => {
|
||||
const itemToDuplicate = previous.sections
|
||||
.flatMap((section) => section.items)
|
||||
.find((item) => item.id === itemId);
|
||||
|
||||
if (!itemToDuplicate) return previous;
|
||||
|
||||
const newItem = {
|
||||
...itemToDuplicate,
|
||||
id: createId(),
|
||||
yOffset: undefined,
|
||||
xOffset: undefined,
|
||||
} satisfies Modify<Item, { yOffset?: number; xOffset?: number }>;
|
||||
|
||||
return {
|
||||
...previous,
|
||||
sections: previous.sections.map((section) => {
|
||||
// Return same section if item is not in it
|
||||
if (!section.items.some((item) => item.id === itemId)) return section;
|
||||
return {
|
||||
...section,
|
||||
items: section.items.concat(newItem as unknown as Item),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
({ itemId }: DuplicateItemInput) => {
|
||||
updateBoard(duplicateItemCallback({ itemId }));
|
||||
},
|
||||
[updateBoard],
|
||||
);
|
||||
|
||||
@@ -79,7 +79,12 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
|
||||
>
|
||||
<Throw
|
||||
error={new NoIntegrationSelectedError()}
|
||||
when={widgetSupportsIntegrations && item.integrationIds.length === 0}
|
||||
when={
|
||||
widgetSupportsIntegrations &&
|
||||
item.integrationIds.length === 0 &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
(!("integrationsRequired" in definition) || definition.integrationsRequired !== false)
|
||||
}
|
||||
/>
|
||||
<BoardItemMenu offset={4} item={newItem} />
|
||||
<Comp
|
||||
|
||||
@@ -12,11 +12,7 @@ export const env = createEnv({
|
||||
* Specify your server-side environment variables schema here. This way you can ensure the app isn't
|
||||
* built with invalid env vars.
|
||||
*/
|
||||
server: {
|
||||
// Comma separated list of docker hostnames that can be used to connect to query the docker endpoints (localhost:2375,host.docker.internal:2375, ...)
|
||||
DOCKER_HOSTNAMES: z.string().optional(),
|
||||
DOCKER_PORTS: z.string().optional(),
|
||||
},
|
||||
server: {},
|
||||
/**
|
||||
* Specify your client-side environment variables schema here.
|
||||
* For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`.
|
||||
@@ -30,9 +26,8 @@ export const env = createEnv({
|
||||
runtimeEnv: {
|
||||
PORT: process.env.PORT,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
DOCKER_HOSTNAMES: process.env.DOCKER_HOSTNAMES,
|
||||
DOCKER_PORTS: process.env.DOCKER_PORTS,
|
||||
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
|
||||
},
|
||||
skipValidation: shouldSkipEnvValidation(),
|
||||
emptyStringAsUndefined: true,
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.4.7",
|
||||
"superjson": "2.2.2",
|
||||
"undici": "7.2.3"
|
||||
"undici": "7.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
|
||||
@@ -49,9 +49,9 @@ describe("Onboarding", () => {
|
||||
environment: {
|
||||
AUTH_PROVIDERS: "ldap",
|
||||
AUTH_LDAP_URI: "ldap://host.docker.internal:3890",
|
||||
AUTH_LDAP_BASE: "",
|
||||
AUTH_LDAP_BIND_DN: "",
|
||||
AUTH_LDAP_BIND_PASSWORD: "",
|
||||
AUTH_LDAP_BASE: "not-used",
|
||||
AUTH_LDAP_BIND_DN: "not-used",
|
||||
AUTH_LDAP_BIND_PASSWORD: "not-used",
|
||||
},
|
||||
mounts: {
|
||||
"/appdata": localMountPath,
|
||||
|
||||
12
package.json
12
package.json
@@ -40,27 +40,27 @@
|
||||
"@semantic-release/release-notes-generator": "^14.0.3",
|
||||
"@turbo/gen": "^2.3.3",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/coverage-v8": "^3.0.2",
|
||||
"@vitest/ui": "^3.0.2",
|
||||
"@vitest/coverage-v8": "^3.0.3",
|
||||
"@vitest/ui": "^3.0.3",
|
||||
"conventional-changelog-conventionalcommits": "^8.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"jsdom": "^26.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"semantic-release": "^24.2.1",
|
||||
"testcontainers": "^10.16.0",
|
||||
"testcontainers": "^10.17.1",
|
||||
"turbo": "^2.3.3",
|
||||
"typescript": "^5.7.3",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.0.2"
|
||||
"vitest": "^3.0.3"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.4",
|
||||
"engines": {
|
||||
"node": ">=22.13.0"
|
||||
"node": ">=22.13.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"allowNonAppliedPatches": true,
|
||||
"overrides": {
|
||||
"proxmox-api>undici": "7.2.3"
|
||||
"proxmox-api>undici": "7.3.0"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"pretty-print-error": "patches/pretty-print-error.patch"
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/definitions": "workspace:^0.1.0",
|
||||
"@homarr/docker": "workspace:^0.1.0",
|
||||
"@homarr/icons": "workspace:^0.1.0",
|
||||
"@homarr/integrations": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^",
|
||||
@@ -42,9 +43,8 @@
|
||||
"@trpc/client": "next",
|
||||
"@trpc/react-query": "next",
|
||||
"@trpc/server": "next",
|
||||
"dockerode": "4.0.2",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"next": "15.1.5",
|
||||
"next": "15.1.6",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"superjson": "2.2.2",
|
||||
@@ -54,7 +54,6 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/dockerode": "^3.3.34",
|
||||
"eslint": "^9.18.0",
|
||||
"prettier": "^3.4.2",
|
||||
"typescript": "^5.7.3"
|
||||
|
||||
@@ -98,56 +98,59 @@ export const createManyIntegrationMiddleware = <TKind extends IntegrationKind>(
|
||||
action: IntegrationAction,
|
||||
...kinds: AtLeastOneOf<TKind> // Ensure at least one kind is provided
|
||||
) => {
|
||||
return publicProcedure
|
||||
.input(z.object({ integrationIds: z.array(z.string()).min(1) }))
|
||||
.use(async ({ ctx, input, next }) => {
|
||||
const dbIntegrations = await ctx.db.query.integrations.findMany({
|
||||
where: and(inArray(integrations.id, input.integrationIds), inArray(integrations.kind, kinds)),
|
||||
with: {
|
||||
secrets: true,
|
||||
items: {
|
||||
return publicProcedure.input(z.object({ integrationIds: z.array(z.string()) })).use(async ({ ctx, input, next }) => {
|
||||
const dbIntegrations =
|
||||
input.integrationIds.length >= 1
|
||||
? await ctx.db.query.integrations.findMany({
|
||||
where: and(inArray(integrations.id, input.integrationIds), inArray(integrations.kind, kinds)),
|
||||
with: {
|
||||
item: {
|
||||
secrets: true,
|
||||
items: {
|
||||
with: {
|
||||
section: {
|
||||
columns: {
|
||||
boardId: true,
|
||||
item: {
|
||||
with: {
|
||||
section: {
|
||||
columns: {
|
||||
boardId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
userPermissions: true,
|
||||
groupPermissions: true,
|
||||
},
|
||||
},
|
||||
userPermissions: true,
|
||||
groupPermissions: true,
|
||||
},
|
||||
})
|
||||
: [];
|
||||
|
||||
const offset = input.integrationIds.length - dbIntegrations.length;
|
||||
if (offset !== 0) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: `${offset} of the specified integrations not found or not of kinds ${kinds.join(",")}: ([${input.integrationIds.join(",")}] compared to [${dbIntegrations.map(({ id, kind }) => `${kind}:${id}`).join(",")}])`,
|
||||
});
|
||||
}
|
||||
|
||||
const offset = input.integrationIds.length - dbIntegrations.length;
|
||||
if (offset !== 0) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: `${offset} of the specified integrations not found or not of kinds ${kinds.join(",")}: ([${input.integrationIds.join(",")}] compared to [${dbIntegrations.map(({ id, kind }) => `${kind}:${id}`).join(",")}])`,
|
||||
});
|
||||
}
|
||||
|
||||
if (dbIntegrations.length >= 1) {
|
||||
await throwIfActionIsNotAllowedAsync(action, ctx.db, dbIntegrations, ctx.session);
|
||||
}
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
integrations: dbIntegrations.map(
|
||||
({ secrets, kind, items: _ignore1, groupPermissions: _ignore2, userPermissions: _ignore3, ...rest }) => ({
|
||||
...rest,
|
||||
kind: kind as TKind,
|
||||
decryptedSecrets: secrets.map((secret) => ({
|
||||
...secret,
|
||||
value: decryptSecret(secret.value),
|
||||
})),
|
||||
}),
|
||||
),
|
||||
},
|
||||
});
|
||||
return next({
|
||||
ctx: {
|
||||
integrations: dbIntegrations.map(
|
||||
({ secrets, kind, items: _ignore1, groupPermissions: _ignore2, userPermissions: _ignore3, ...rest }) => ({
|
||||
...rest,
|
||||
kind: kind as TKind,
|
||||
decryptedSecrets: secrets.map((secret) => ({
|
||||
...secret,
|
||||
value: decryptSecret(secret.value),
|
||||
})),
|
||||
}),
|
||||
),
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import type Docker from "dockerode";
|
||||
import type { Container } from "dockerode";
|
||||
|
||||
import { db, like, or } from "@homarr/db";
|
||||
import { icons } from "@homarr/db/schema";
|
||||
import type { DockerContainerState } from "@homarr/definitions";
|
||||
import { DockerSingleton } from "@homarr/docker";
|
||||
import type { Container, ContainerInfo, ContainerState, Docker, Port } from "@homarr/docker";
|
||||
import { logger } from "@homarr/log";
|
||||
import { createCacheChannel } from "@homarr/redis";
|
||||
import { z } from "@homarr/validation";
|
||||
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
|
||||
import { DockerSingleton } from "./docker-singleton";
|
||||
|
||||
const dockerCache = createCacheChannel<{
|
||||
containers: (Docker.ContainerInfo & { instance: string; iconUrl: string | null })[];
|
||||
containers: (ContainerInfo & { instance: string; iconUrl: string | null })[];
|
||||
}>("docker-containers", 5 * 60 * 1000);
|
||||
|
||||
export const dockerRouter = createTRPCRouter({
|
||||
getContainers: permissionRequiredProcedure.requiresPermission("admin").query(async () => {
|
||||
const result = await dockerCache
|
||||
.consumeAsync(async () => {
|
||||
const dockerInstances = DockerSingleton.getInstance();
|
||||
const dockerInstances = DockerSingleton.getInstances();
|
||||
const containers = await Promise.all(
|
||||
// Return all the containers of all the instances into only one item
|
||||
dockerInstances.map(({ instance, host: key }) =>
|
||||
@@ -33,8 +31,7 @@ export const dockerRouter = createTRPCRouter({
|
||||
),
|
||||
).then((containers) => containers.flat());
|
||||
|
||||
const extractImage = (container: Docker.ContainerInfo) =>
|
||||
container.Image.split("/").at(-1)?.split(":").at(0) ?? "";
|
||||
const extractImage = (container: ContainerInfo) => container.Image.split("/").at(-1)?.split(":").at(0) ?? "";
|
||||
const likeQueries = containers.map((container) => like(icons.name, `%${extractImage(container)}%`));
|
||||
const dbIcons =
|
||||
likeQueries.length >= 1
|
||||
@@ -151,7 +148,7 @@ const getContainerOrDefaultAsync = async (instance: Docker, id: string) => {
|
||||
};
|
||||
|
||||
const getContainerOrThrowAsync = async (id: string) => {
|
||||
const dockerInstances = DockerSingleton.getInstance();
|
||||
const dockerInstances = DockerSingleton.getInstances();
|
||||
const containers = await Promise.all(dockerInstances.map(({ instance }) => getContainerOrDefaultAsync(instance, id)));
|
||||
const foundContainer = containers.find((container) => container) ?? null;
|
||||
|
||||
@@ -168,21 +165,21 @@ const getContainerOrThrowAsync = async (id: string) => {
|
||||
interface DockerContainer {
|
||||
name: string;
|
||||
id: string;
|
||||
state: DockerContainerState;
|
||||
state: ContainerState;
|
||||
image: string;
|
||||
ports: Docker.Port[];
|
||||
ports: Port[];
|
||||
iconUrl: string | null;
|
||||
}
|
||||
|
||||
function sanitizeContainers(
|
||||
containers: (Docker.ContainerInfo & { instance: string; iconUrl: string | null })[],
|
||||
containers: (ContainerInfo & { instance: string; iconUrl: string | null })[],
|
||||
): DockerContainer[] {
|
||||
return containers.map((container) => {
|
||||
return {
|
||||
name: container.Names[0]?.split("/")[1] ?? "Unknown",
|
||||
id: container.Id,
|
||||
instance: container.instance,
|
||||
state: container.State as DockerContainerState,
|
||||
state: container.State as ContainerState,
|
||||
image: container.Image,
|
||||
ports: container.Ports,
|
||||
iconUrl: container.iconUrl,
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import Docker from "dockerode";
|
||||
|
||||
interface DockerInstance {
|
||||
host: string;
|
||||
instance: Docker;
|
||||
}
|
||||
|
||||
export class DockerSingleton {
|
||||
private static instances: DockerInstance[];
|
||||
|
||||
private createInstances() {
|
||||
const instances: DockerInstance[] = [];
|
||||
const hostVariable = process.env.DOCKER_HOST;
|
||||
const portVariable = process.env.DOCKER_PORT;
|
||||
if (hostVariable === undefined || portVariable === undefined) {
|
||||
instances.push({ host: "socket", instance: new Docker() });
|
||||
return instances;
|
||||
}
|
||||
const hosts = hostVariable.split(",");
|
||||
const ports = portVariable.split(",");
|
||||
|
||||
if (hosts.length !== ports.length) {
|
||||
throw new Error("The number of hosts and ports must match");
|
||||
}
|
||||
|
||||
hosts.forEach((host, i) => {
|
||||
instances.push({
|
||||
host: `${host}:${ports[i]}`,
|
||||
instance: new Docker({
|
||||
host,
|
||||
port: parseInt(ports[i] ?? "", 10),
|
||||
}),
|
||||
});
|
||||
return instances;
|
||||
});
|
||||
return instances;
|
||||
}
|
||||
|
||||
public static findInstance(key: string): DockerInstance | undefined {
|
||||
return this.instances.find((instance) => instance.host === key);
|
||||
}
|
||||
|
||||
public static getInstance(): DockerInstance[] {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!DockerSingleton.instances) {
|
||||
DockerSingleton.instances = new DockerSingleton().createInstances();
|
||||
}
|
||||
|
||||
return this.instances;
|
||||
}
|
||||
}
|
||||
@@ -404,7 +404,13 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.userId), "view");
|
||||
// Only allow user to select boards they have access to
|
||||
if (input.homeBoardId) {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.homeBoardId), "view");
|
||||
}
|
||||
if (input.mobileHomeBoardId) {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.mobileHomeBoardId), "view");
|
||||
}
|
||||
|
||||
await ctx.db
|
||||
.update(users)
|
||||
|
||||
@@ -1,12 +1,36 @@
|
||||
import type { RssFeed } from "@homarr/cron-jobs";
|
||||
import { createItemChannel } from "@homarr/redis";
|
||||
import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";
|
||||
import { z } from "@homarr/validation";
|
||||
|
||||
import { createOneItemMiddleware } from "../../middlewares/item";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const rssFeedRouter = createTRPCRouter({
|
||||
getFeeds: publicProcedure.unstable_concat(createOneItemMiddleware("rssFeed")).query(async ({ input }) => {
|
||||
const channel = createItemChannel<RssFeed[]>(input.itemId);
|
||||
return await channel.getAsync();
|
||||
}),
|
||||
getFeeds: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
urls: z.array(z.string()),
|
||||
maximumAmountPosts: z.number(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
const rssFeeds = await Promise.all(
|
||||
input.urls.map(async (url) => {
|
||||
const innerHandler = rssFeedsRequestHandler.handler({
|
||||
url,
|
||||
count: input.maximumAmountPosts,
|
||||
});
|
||||
return await innerHandler.getCachedOrUpdatedDataAsync({
|
||||
forceUpdate: false,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return rssFeeds
|
||||
.flatMap((rssFeed) => rssFeed.data.entries)
|
||||
.slice(0, input.maximumAmountPosts)
|
||||
.sort((entryA, entryB) => {
|
||||
return entryA.published && entryB.published
|
||||
? new Date(entryB.published).getTime() - new Date(entryA.published).getTime()
|
||||
: 0;
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -2,9 +2,11 @@ import type { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapte
|
||||
import { cookies } from "next/headers";
|
||||
import NextAuth from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
import { formatError } from "pretty-print-error";
|
||||
|
||||
import { db } from "@homarr/db";
|
||||
import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { createAdapter } from "./adapter";
|
||||
import { createSessionCallback } from "./callbacks";
|
||||
@@ -26,15 +28,16 @@ export const createConfiguration = (
|
||||
const adapter = createAdapter(db, provider);
|
||||
return NextAuth({
|
||||
logger: {
|
||||
error: (code, ...message) => {
|
||||
error: (error) => {
|
||||
// Remove the big error message for failed login attempts
|
||||
// as it is not useful for the user.
|
||||
if (code.name === "CredentialsSignin") {
|
||||
console.warn("The login attempt of a user was not successful.");
|
||||
if (error.name === "CredentialsSignin") {
|
||||
logger.warn("The login attempt of a user was not successful.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(code, ...message);
|
||||
logger.error(formatError(error));
|
||||
logger.error(formatError(error.cause));
|
||||
},
|
||||
},
|
||||
trustHost: true,
|
||||
|
||||
@@ -86,4 +86,5 @@ export const env = createEnv({
|
||||
AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE: process.env.AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE,
|
||||
},
|
||||
skipValidation,
|
||||
emptyStringAsUndefined: true,
|
||||
});
|
||||
|
||||
@@ -34,8 +34,9 @@
|
||||
"bcrypt": "^5.1.1",
|
||||
"cookies": "^0.9.1",
|
||||
"ldapts": "7.3.1",
|
||||
"next": "15.1.5",
|
||||
"next": "15.1.6",
|
||||
"next-auth": "5.0.0-beta.25",
|
||||
"pretty-print-error": "^1.1.2",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"undici": "7.2.3"
|
||||
"undici": "7.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
|
||||
@@ -26,4 +26,5 @@ export const env = createEnv({
|
||||
SECRET_ENCRYPTION_KEY: process.env.SECRET_ENCRYPTION_KEY,
|
||||
},
|
||||
skipValidation: shouldSkipEnvValidation(),
|
||||
emptyStringAsUndefined: true,
|
||||
});
|
||||
|
||||
@@ -29,10 +29,10 @@
|
||||
"dependencies": {
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"next": "15.1.5",
|
||||
"next": "15.1.6",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"undici": "7.2.3",
|
||||
"undici": "7.3.0",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@extractus/feed-extractor": "^7.1.3",
|
||||
"@homarr/analytics": "workspace:^0.1.0",
|
||||
"@homarr/auth": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
|
||||
@@ -11,7 +11,6 @@ import { mediaServerJob } from "./jobs/integrations/media-server";
|
||||
import { mediaTranscodingJob } from "./jobs/integrations/media-transcoding";
|
||||
import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
|
||||
import { pingJob } from "./jobs/ping";
|
||||
import type { RssFeed } from "./jobs/rss-feeds";
|
||||
import { rssFeedsJob } from "./jobs/rss-feeds";
|
||||
import { sessionCleanupJob } from "./jobs/session-cleanup";
|
||||
import { updateCheckerJob } from "./jobs/update-checker";
|
||||
@@ -38,4 +37,3 @@ export const jobGroup = createCronJobGroup({
|
||||
});
|
||||
|
||||
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
|
||||
export type { RssFeed };
|
||||
|
||||
@@ -1,139 +1,36 @@
|
||||
import type { FeedData, FeedEntry } from "@extractus/feed-extractor";
|
||||
import { extract } from "@extractus/feed-extractor";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
import type { Modify } from "@homarr/common/types";
|
||||
import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions";
|
||||
import { db, eq } from "@homarr/db";
|
||||
import { items } from "@homarr/db/schema";
|
||||
import { logger } from "@homarr/log";
|
||||
import { createItemChannel } from "@homarr/redis";
|
||||
import { z } from "@homarr/validation";
|
||||
|
||||
// This import is done that way to avoid circular dependencies.
|
||||
import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";
|
||||
|
||||
import type { WidgetComponentProps } from "../../../widgets";
|
||||
import { createCronJob } from "../lib";
|
||||
|
||||
export const rssFeedsJob = createCronJob("rssFeeds", EVERY_5_MINUTES).withCallback(async () => {
|
||||
const itemsForIntegration = await db.query.items.findMany({
|
||||
const rssItems = await db.query.items.findMany({
|
||||
where: eq(items.kind, "rssFeed"),
|
||||
});
|
||||
|
||||
for (const item of itemsForIntegration) {
|
||||
const options = SuperJSON.parse<WidgetComponentProps<"rssFeed">["options"]>(item.options);
|
||||
const itemOptions = rssItems.map((item) => SuperJSON.parse<WidgetComponentProps<"rssFeed">["options"]>(item.options));
|
||||
|
||||
const feeds = await Promise.all(
|
||||
options.feedUrls.map(async (feedUrl) => ({
|
||||
feedUrl,
|
||||
feed: (await extract(feedUrl, {
|
||||
getExtraEntryFields: (feedEntry) => {
|
||||
const media = attemptGetImageFromEntry(feedUrl, feedEntry);
|
||||
if (!media) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
enclosure: media,
|
||||
};
|
||||
},
|
||||
})) as ExtendedFeedData,
|
||||
})),
|
||||
);
|
||||
|
||||
const channel = createItemChannel<RssFeed[]>(item.id);
|
||||
await channel.publishAndUpdateLastStateAsync(feeds);
|
||||
for (const option of itemOptions) {
|
||||
const maxAmountPosts = typeof option.maximumAmountPosts === "number" ? option.maximumAmountPosts : 100;
|
||||
for (const url of option.feedUrls) {
|
||||
try {
|
||||
const innerHandler = rssFeedsRequestHandler.handler({
|
||||
url,
|
||||
count: maxAmountPosts,
|
||||
});
|
||||
await innerHandler.getCachedOrUpdatedDataAsync({
|
||||
forceUpdate: true,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Failed to update RSS feed", { url, error });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const attemptGetImageFromEntry = (feedUrl: string, entry: object) => {
|
||||
const media = getFirstMediaProperty(entry);
|
||||
if (media !== null) {
|
||||
return media;
|
||||
}
|
||||
return getImageFromStringAsFallback(feedUrl, JSON.stringify(entry));
|
||||
};
|
||||
|
||||
const getImageFromStringAsFallback = (feedUrl: string, content: string) => {
|
||||
const regex = /https?:\/\/\S+?\.(jpg|jpeg|png|gif|bmp|svg|webp|tiff)/i;
|
||||
const result = regex.exec(content);
|
||||
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.debug(
|
||||
`Falling back to regex image search for '${feedUrl}'. Found ${result.length} matches in content: ${content}`,
|
||||
);
|
||||
return result[0];
|
||||
};
|
||||
|
||||
const mediaProperties = [
|
||||
{
|
||||
path: ["enclosure", "@_url"],
|
||||
},
|
||||
{
|
||||
path: ["media:content", "@_url"],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* The RSS and Atom standards are poorly adhered to in most of the web.
|
||||
* We want to show pretty background images on the posts and therefore need to extract
|
||||
* the enclosure (aka. media images). This function uses the dynamic properties defined above
|
||||
* to search through the possible paths and detect valid image URLs.
|
||||
* @param feedObject The object to scan for.
|
||||
* @returns the value of the first path that is found within the object
|
||||
*/
|
||||
const getFirstMediaProperty = (feedObject: object) => {
|
||||
for (const mediaProperty of mediaProperties) {
|
||||
let propertyIndex = 0;
|
||||
let objectAtPath: object = feedObject;
|
||||
while (propertyIndex < mediaProperty.path.length) {
|
||||
const key = mediaProperty.path[propertyIndex];
|
||||
if (key === undefined) {
|
||||
break;
|
||||
}
|
||||
const propertyEntries = Object.entries(objectAtPath);
|
||||
const propertyEntry = propertyEntries.find(([entryKey]) => entryKey === key);
|
||||
if (!propertyEntry) {
|
||||
break;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const [_, propertyEntryValue] = propertyEntry;
|
||||
objectAtPath = propertyEntryValue as object;
|
||||
propertyIndex++;
|
||||
}
|
||||
|
||||
const validationResult = z.string().url().safeParse(objectAtPath);
|
||||
if (!validationResult.success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.debug(`Found an image in the feed entry: ${validationResult.data}`);
|
||||
return validationResult.data;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* We extend the feed with custom properties.
|
||||
* This interface adds properties on top of the default ones.
|
||||
*/
|
||||
interface ExtendedFeedEntry extends FeedEntry {
|
||||
enclosure?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* We extend the feed with custom properties.
|
||||
* This interface omits the default entries with our custom definition.
|
||||
*/
|
||||
type ExtendedFeedData = Modify<
|
||||
FeedData,
|
||||
{
|
||||
entries?: ExtendedFeedEntry;
|
||||
}
|
||||
>;
|
||||
|
||||
export interface RssFeed {
|
||||
feedUrl: string;
|
||||
feed: ExtendedFeedData;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,11 @@ export const env = createEnv({
|
||||
.default(drivers.betterSqlite3),
|
||||
...(urlRequired
|
||||
? {
|
||||
DB_URL: z.string(),
|
||||
DB_URL:
|
||||
// Fallback to the default sqlite file path in production
|
||||
process.env.NODE_ENV === "production" && isDriver("better-sqlite3")
|
||||
? z.string().default("/appdata/db/db.sqlite")
|
||||
: z.string().nonempty(),
|
||||
}
|
||||
: {}),
|
||||
...(hostRequired
|
||||
@@ -58,4 +62,5 @@ export const env = createEnv({
|
||||
DB_PORT: process.env.DB_PORT,
|
||||
},
|
||||
skipValidation: shouldSkipEnvValidation(),
|
||||
emptyStringAsUndefined: true,
|
||||
});
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@t3-oss/env-nextjs": "^0.11.1",
|
||||
"@testcontainers/mysql": "^10.16.0",
|
||||
"better-sqlite3": "^11.8.0",
|
||||
"@testcontainers/mysql": "^10.17.1",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"drizzle-kit": "^0.30.2",
|
||||
"drizzle-orm": "^0.38.4",
|
||||
|
||||
@@ -10,6 +10,7 @@ export type HomarrDocumentationPath =
|
||||
| "/blog/2024/09/23/version-1.0"
|
||||
| "/blog/2024/12/17/open-beta-1.0"
|
||||
| "/blog/2024/12/31/migrate-secret-enryption-key"
|
||||
| "/blog/2025/01/19/migration-guide-1.0"
|
||||
| "/blog/archive"
|
||||
| "/blog/authors"
|
||||
| "/blog/authors/ajnart"
|
||||
@@ -165,6 +166,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/getting-started/installation/home-assistant"
|
||||
| "/docs/getting-started/installation/portainer"
|
||||
| "/docs/getting-started/installation/qnap"
|
||||
| "/docs/getting-started/installation/railway"
|
||||
| "/docs/getting-started/installation/saltbox"
|
||||
| "/docs/getting-started/installation/source"
|
||||
| "/docs/getting-started/installation/synology"
|
||||
|
||||
9
packages/docker/eslint.config.js
Normal file
9
packages/docker/eslint.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import baseConfig from "@homarr/eslint-config/base";
|
||||
|
||||
/** @type {import('typescript-eslint').Config} */
|
||||
export default [
|
||||
{
|
||||
ignores: [],
|
||||
},
|
||||
...baseConfig,
|
||||
];
|
||||
1
packages/docker/index.ts
Normal file
1
packages/docker/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./src";
|
||||
38
packages/docker/package.json
Normal file
38
packages/docker/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@homarr/docker",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./index.ts",
|
||||
"./env": "./src/env.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf .turbo node_modules",
|
||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||
"lint": "eslint",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@t3-oss/env-nextjs": "^0.11.1",
|
||||
"dockerode": "^4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/dockerode": "^3.3.34",
|
||||
"eslint": "^9.18.0",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
18
packages/docker/src/env.ts
Normal file
18
packages/docker/src/env.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createEnv } from "@t3-oss/env-nextjs";
|
||||
import { z } from "zod";
|
||||
|
||||
import { shouldSkipEnvValidation } from "@homarr/common/env-validation";
|
||||
|
||||
export const env = createEnv({
|
||||
server: {
|
||||
// Comma separated list of docker hostnames that can be used to connect to query the docker endpoints (localhost:2375,host.docker.internal:2375, ...)
|
||||
DOCKER_HOSTNAMES: z.string().optional(),
|
||||
DOCKER_PORTS: z.string().optional(),
|
||||
},
|
||||
runtimeEnv: {
|
||||
DOCKER_HOSTNAMES: process.env.DOCKER_HOSTNAMES,
|
||||
DOCKER_PORTS: process.env.DOCKER_PORTS,
|
||||
},
|
||||
skipValidation: shouldSkipEnvValidation(),
|
||||
emptyStringAsUndefined: true,
|
||||
});
|
||||
10
packages/docker/src/index.ts
Normal file
10
packages/docker/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type Docker from "dockerode";
|
||||
|
||||
export type { DockerInstance } from "./singleton";
|
||||
export { DockerSingleton } from "./singleton";
|
||||
export type { ContainerInfo, Container, Port } from "dockerode";
|
||||
export type { Docker };
|
||||
|
||||
export const containerStates = ["created", "running", "paused", "restarting", "exited", "removing", "dead"] as const;
|
||||
|
||||
export type ContainerState = (typeof containerStates)[number];
|
||||
53
packages/docker/src/singleton.ts
Normal file
53
packages/docker/src/singleton.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import Docker from "dockerode";
|
||||
|
||||
import { env } from "./env";
|
||||
|
||||
export interface DockerInstance {
|
||||
host: string;
|
||||
instance: Docker;
|
||||
}
|
||||
|
||||
export class DockerSingleton {
|
||||
private static instances: DockerInstance[] | null = null;
|
||||
|
||||
private createInstances() {
|
||||
const hostVariable = env.DOCKER_HOSTNAMES;
|
||||
const portVariable = env.DOCKER_PORTS;
|
||||
if (hostVariable === undefined || portVariable === undefined) {
|
||||
return [{ host: "socket", instance: new Docker() }];
|
||||
}
|
||||
const hostnames = hostVariable.split(",");
|
||||
const ports = portVariable.split(",");
|
||||
|
||||
if (hostnames.length !== ports.length) {
|
||||
throw new Error("The number of hosts and ports must match");
|
||||
}
|
||||
|
||||
return hostnames.map((host, i) => {
|
||||
// Check above ensures that ports[i] is not undefined
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const port = ports[i]!;
|
||||
|
||||
return {
|
||||
host: `${host}:${port}`,
|
||||
instance: new Docker({
|
||||
host,
|
||||
port: parseInt(port, 10),
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static findInstance(host: string): DockerInstance | undefined {
|
||||
return this.instances?.find((instance) => instance.host === host);
|
||||
}
|
||||
|
||||
public static getInstances(): DockerInstance[] {
|
||||
if (this.instances) {
|
||||
return this.instances;
|
||||
}
|
||||
|
||||
this.instances = new DockerSingleton().createInstances();
|
||||
return this.instances;
|
||||
}
|
||||
}
|
||||
8
packages/docker/tsconfig.json
Normal file
8
packages/docker/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@homarr/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["*.ts", "src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/form": "^7.16.0"
|
||||
"@mantine/form": "^7.16.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@jellyfin/sdk": "^0.11.0",
|
||||
"proxmox-api": "1.1.1",
|
||||
"undici": "7.2.3",
|
||||
"undici": "7.3.0",
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -32,10 +32,10 @@
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.16.0",
|
||||
"@tabler/icons-react": "^3.28.1",
|
||||
"@mantine/core": "^7.16.1",
|
||||
"@tabler/icons-react": "^3.29.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"next": "15.1.5",
|
||||
"next": "15.1.6",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
},
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
"dependencies": {
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.16.0",
|
||||
"@mantine/hooks": "^7.16.0",
|
||||
"@mantine/core": "^7.16.1",
|
||||
"@mantine/hooks": "^7.16.1",
|
||||
"react": "19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@mantine/notifications": "^7.16.0",
|
||||
"@tabler/icons-react": "^3.28.1"
|
||||
"@mantine/notifications": "^7.16.1",
|
||||
"@tabler/icons-react": "^3.29.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
|
||||
@@ -37,10 +37,10 @@
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.16.0",
|
||||
"@mantine/hooks": "^7.16.0",
|
||||
"@mantine/core": "^7.16.1",
|
||||
"@mantine/hooks": "^7.16.1",
|
||||
"adm-zip": "0.5.16",
|
||||
"next": "15.1.5",
|
||||
"next": "15.1.6",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"superjson": "2.2.2",
|
||||
|
||||
@@ -15,7 +15,7 @@ export const mapApp = (
|
||||
boardSize: BoardSize,
|
||||
appsMap: Map<string, { id: string }>,
|
||||
sectionMap: Map<string, { id: string }>,
|
||||
): InferInsertModel<typeof items> => {
|
||||
): InferInsertModel<typeof items> | null => {
|
||||
if (app.area.type === "sidebar") throw new Error("Mapping app in sidebar is not supported");
|
||||
|
||||
const shapeForSize = app.shape[boardSize];
|
||||
@@ -25,7 +25,8 @@ export const mapApp = (
|
||||
|
||||
const sectionId = sectionMap.get(app.area.properties.id)?.id;
|
||||
if (!sectionId) {
|
||||
throw new Error(`Failed to find section for app appId='${app.id}' sectionId='${app.area.properties.id}'`);
|
||||
logger.warn(`Failed to find section for app appId='${app.id}' sectionId='${app.area.properties.id}'. Removing app`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -69,9 +70,10 @@ export const mapWidget = (
|
||||
|
||||
const sectionId = sectionMap.get(widget.area.properties.id)?.id;
|
||||
if (!sectionId) {
|
||||
throw new Error(
|
||||
`Failed to find section for widget widgetId='${widget.id}' sectionId='${widget.area.properties.id}'`,
|
||||
logger.warn(
|
||||
`Failed to find section for widget widgetId='${widget.id}' sectionId='${widget.area.properties.id}'. Removing widget`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -10,5 +10,5 @@ export const prepareItems = (
|
||||
) =>
|
||||
widgets
|
||||
.map((widget) => mapWidget(widget, boardSize, appsMap, sectionMap))
|
||||
.filter((widget) => widget !== null)
|
||||
.concat(apps.map((app) => mapApp(app, boardSize, appsMap, sectionMap)));
|
||||
.concat(apps.map((app) => mapApp(app, boardSize, appsMap, sectionMap)))
|
||||
.filter((widget) => widget !== null);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@extractus/feed-extractor": "7.1.3",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/definitions": "workspace:^0.1.0",
|
||||
|
||||
127
packages/request-handler/src/rss-feeds.ts
Normal file
127
packages/request-handler/src/rss-feeds.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import type { FeedData, FeedEntry } from "@extractus/feed-extractor";
|
||||
import { extract } from "@extractus/feed-extractor";
|
||||
import dayjs from "dayjs";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { Modify } from "@homarr/common/types";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
|
||||
|
||||
export const rssFeedsRequestHandler = createCachedWidgetRequestHandler({
|
||||
queryKey: "rssFeedList",
|
||||
widgetKind: "rssFeed",
|
||||
async requestAsync(input: { url: string; count: number }) {
|
||||
const result = (await extract(input.url, {
|
||||
getExtraEntryFields: (feedEntry) => {
|
||||
const media = attemptGetImageFromEntry(input.url, feedEntry);
|
||||
if (!media) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
enclosure: media,
|
||||
};
|
||||
},
|
||||
})) as ExtendedFeedData;
|
||||
|
||||
return {
|
||||
...result,
|
||||
entries: result.entries?.slice(0, input.count) ?? [],
|
||||
};
|
||||
},
|
||||
cacheDuration: dayjs.duration(5, "minutes"),
|
||||
});
|
||||
|
||||
const attemptGetImageFromEntry = (feedUrl: string, entry: object) => {
|
||||
const media = getFirstMediaProperty(entry);
|
||||
if (media !== null) {
|
||||
return media;
|
||||
}
|
||||
return getImageFromStringAsFallback(feedUrl, JSON.stringify(entry));
|
||||
};
|
||||
|
||||
const getImageFromStringAsFallback = (feedUrl: string, content: string) => {
|
||||
const regex = /https?:\/\/\S+?\.(jpg|jpeg|png|gif|bmp|svg|webp|tiff)/i;
|
||||
const result = regex.exec(content);
|
||||
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.debug(
|
||||
`Falling back to regex image search for '${feedUrl}'. Found ${result.length} matches in content: ${content}`,
|
||||
);
|
||||
return result[0];
|
||||
};
|
||||
|
||||
const mediaProperties = [
|
||||
{
|
||||
path: ["enclosure", "@_url"],
|
||||
},
|
||||
{
|
||||
path: ["media:content", "@_url"],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* The RSS and Atom standards are poorly adhered to in most of the web.
|
||||
* We want to show pretty background images on the posts and therefore need to extract
|
||||
* the enclosure (aka. media images). This function uses the dynamic properties defined above
|
||||
* to search through the possible paths and detect valid image URLs.
|
||||
* @param feedObject The object to scan for.
|
||||
* @returns the value of the first path that is found within the object
|
||||
*/
|
||||
const getFirstMediaProperty = (feedObject: object) => {
|
||||
for (const mediaProperty of mediaProperties) {
|
||||
let propertyIndex = 0;
|
||||
let objectAtPath: object = feedObject;
|
||||
while (propertyIndex < mediaProperty.path.length) {
|
||||
const key = mediaProperty.path[propertyIndex];
|
||||
if (key === undefined) {
|
||||
break;
|
||||
}
|
||||
const propertyEntries = Object.entries(objectAtPath);
|
||||
const propertyEntry = propertyEntries.find(([entryKey]) => entryKey === key);
|
||||
if (!propertyEntry) {
|
||||
break;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const [_, propertyEntryValue] = propertyEntry;
|
||||
objectAtPath = propertyEntryValue as object;
|
||||
propertyIndex++;
|
||||
}
|
||||
|
||||
const validationResult = z.string().url().safeParse(objectAtPath);
|
||||
if (!validationResult.success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.debug(`Found an image in the feed entry: ${validationResult.data}`);
|
||||
return validationResult.data;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* We extend the feed with custom properties.
|
||||
* This interface adds properties on top of the default ones.
|
||||
*/
|
||||
interface ExtendedFeedEntry extends FeedEntry {
|
||||
enclosure?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* We extend the feed with custom properties.
|
||||
* This interface omits the default entries with our custom definition.
|
||||
*/
|
||||
type ExtendedFeedData = Modify<
|
||||
FeedData,
|
||||
{
|
||||
entries?: ExtendedFeedEntry[];
|
||||
}
|
||||
>;
|
||||
|
||||
export interface RssFeed {
|
||||
feedUrl: string;
|
||||
feed: ExtendedFeedData;
|
||||
}
|
||||
@@ -32,12 +32,12 @@
|
||||
"@homarr/modals-collection": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.16.0",
|
||||
"@mantine/hooks": "^7.16.0",
|
||||
"@mantine/spotlight": "^7.16.0",
|
||||
"@tabler/icons-react": "^3.28.1",
|
||||
"jotai": "^2.11.0",
|
||||
"next": "15.1.5",
|
||||
"@mantine/core": "^7.16.1",
|
||||
"@mantine/hooks": "^7.16.1",
|
||||
"@mantine/spotlight": "^7.16.1",
|
||||
"@tabler/icons-react": "^3.29.0",
|
||||
"jotai": "^2.11.1",
|
||||
"next": "15.1.6",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"use-deep-compare-effect": "^1.8.1"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"deepmerge": "4.3.1",
|
||||
"mantine-react-table": "2.0.0-beta.8",
|
||||
"next": "15.1.5",
|
||||
"next": "15.1.6",
|
||||
"next-intl": "3.26.3",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "密码",
|
||||
"newLabel": "新密码"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "密鑰 ID",
|
||||
"newLabel": "新密鑰 ID"
|
||||
},
|
||||
"realm": {
|
||||
"label": "領域",
|
||||
"newLabel": "新領域"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": "复制 URL"
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -897,7 +908,7 @@
|
||||
"passwordRequirements": "密码不符合要求",
|
||||
"boardAlreadyExists": "具有此名称的面板已存在",
|
||||
"invalidFileType": "无效的文件类型,例如 {expected}",
|
||||
"invalidFileName": "",
|
||||
"invalidFileName": "無效的檔案名稱",
|
||||
"fileTooLarge": "文件太大,最大大小为 {maxSize}",
|
||||
"invalidConfiguration": "无效配置",
|
||||
"groupNameTaken": "用户组名称已存在"
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "显示文件系统信息"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": "預設頁面"
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": "部分指示需求"
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": "获取健康状态失败"
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "處理器",
|
||||
"memory": "記憶體"
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": "節點"
|
||||
},
|
||||
"qemu": {
|
||||
"name": "虛擬機"
|
||||
},
|
||||
"lxc": {
|
||||
"name": "LXCs"
|
||||
},
|
||||
"storage": {
|
||||
"name": "儲存裝置"
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "節點",
|
||||
"vmId": "虛擬機 ID",
|
||||
"plugin": "插件"
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "核心",
|
||||
"memory": "記憶體",
|
||||
"storage": "儲存裝置",
|
||||
"uptime": "運行時間",
|
||||
"haState": "HA 状态",
|
||||
"storageType": {
|
||||
"local": "本地存储",
|
||||
"shared": "共享存储"
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "名称",
|
||||
"cpu": "处理器",
|
||||
"memory": "内存",
|
||||
"node": "节点"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
@@ -2102,7 +2165,7 @@
|
||||
"docker": "Docker",
|
||||
"logs": "日志",
|
||||
"api": "API",
|
||||
"certificates": "",
|
||||
"certificates": "证书",
|
||||
"tasks": "任务"
|
||||
}
|
||||
},
|
||||
@@ -2710,7 +2773,7 @@
|
||||
"label": "日志"
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
"label": "证书"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -3110,39 +3173,39 @@
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"title": "可信证书",
|
||||
"description": "Homarr 用于从集成请求数据。",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
"title": "还没有证书"
|
||||
},
|
||||
"expires": ""
|
||||
"expires": "到期时间 {when}"
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"label": "添加证书",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "已添加证书",
|
||||
"message": "已成功添加证书"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "添加证书失败",
|
||||
"message": "无法添加证书"
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"label": "删除证书",
|
||||
"confirm": "您确定要删除证书吗?",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "证书已删除",
|
||||
"message": "证书删除成功"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "证书未删除",
|
||||
"message": "无法删除证书"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Heslo",
|
||||
"newLabel": "Nové heslo"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": "Zkopírovat URL"
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "Zobrazit informace o souborovém systému"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Passwort",
|
||||
"newLabel": "Neues Passwort"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": "URL in Zwischenablage kopieren"
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "Dateisystem Info anzeigen"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": "Fehler beim Abrufen des Gesundheitsstatus"
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Κωδικός",
|
||||
"newLabel": "Νέος κωδικός"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": ""
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "Εμφάνιση Πληροφοριών Συστήματος Αρχείων"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -785,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": "Copy URL"
|
||||
},
|
||||
"open": {
|
||||
"label": "Open media"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Contraseña",
|
||||
"newLabel": "Nueva contraseña"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": ""
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "Mostrar información del sistema de archivos"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": ""
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": ""
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Mot de passe",
|
||||
"newLabel": "Nouveau mot de passe"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": "Copier l'URL"
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "Afficher les infos sur le système de fichiers"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "סיסמה",
|
||||
"newLabel": "סיסמה חדשה"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": "העתק קישור"
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "הצג מידע על מערכת הקבצים"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": "נכשל בעדכון סטטוס בריאות המערכת"
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Lozinka",
|
||||
"newLabel": ""
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": ""
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": ""
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Jelszó",
|
||||
"newLabel": "Új jelszó"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": "URL másolása"
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "Fájlrendszer-információk megjelenítése"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "",
|
||||
"newLabel": "Nuova password"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": ""
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "Mostra Informazioni Filesystem"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "パスワード",
|
||||
"newLabel": "新しいパスワード"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": ""
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "ファイルシステム情報を表示"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "비밀번호",
|
||||
"newLabel": ""
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": ""
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": ""
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Slaptažodis",
|
||||
"newLabel": "Naujas slaptažodis"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": ""
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": ""
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Parole",
|
||||
"newLabel": "Jauna parole"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": ""
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "Rādīt failu sistēmas informāciju"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -213,10 +213,10 @@
|
||||
"changeDefaultSearchEngine": {
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "Standaard zoekmachine succesvol gewijzigd"
|
||||
},
|
||||
"error": {
|
||||
"message": ""
|
||||
"message": "Standaard zoekmachine kan niet worden gewijzigd"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -499,7 +499,7 @@
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"search": "",
|
||||
"search": "Vind een app",
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "Apps",
|
||||
@@ -625,8 +625,8 @@
|
||||
"label": "URL"
|
||||
},
|
||||
"attemptSearchEngineCreation": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Zoekmachine aanmaken",
|
||||
"description": "Integratie \"{kind}\" kan worden gebruikt met de zoekmachines. Controleer dit om automatisch de zoekmachine te configureren."
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Wachtwoord",
|
||||
"newLabel": "Nieuw wachtwoord"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "Token ID",
|
||||
"newLabel": "Nieuwe token ID"
|
||||
},
|
||||
"realm": {
|
||||
"label": "Realm",
|
||||
"newLabel": "Nieuwe Realm"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": "URL kopiëren"
|
||||
},
|
||||
"open": {
|
||||
"label": "Media openen"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -897,7 +908,7 @@
|
||||
"passwordRequirements": "Wachtwoord voldoet niet aan de eisen",
|
||||
"boardAlreadyExists": "Er bestaat al een bord met deze naam",
|
||||
"invalidFileType": "Ongeldig bestandstype, verwachtte {expected}",
|
||||
"invalidFileName": "",
|
||||
"invalidFileName": "Ongeldige bestandsnaam",
|
||||
"fileTooLarge": "Bestand is te groot, maximale grootte is {maxSize}",
|
||||
"invalidConfiguration": "Ongeldige configuratie",
|
||||
"groupNameTaken": "Groepsnaam al in gebruik"
|
||||
@@ -967,7 +978,7 @@
|
||||
},
|
||||
"create": {
|
||||
"title": "Kies een item om toe te voegen",
|
||||
"search": "",
|
||||
"search": "Items filteren",
|
||||
"addToBoard": "Toevoegen aan bord"
|
||||
},
|
||||
"moveResize": {
|
||||
@@ -1094,7 +1105,7 @@
|
||||
"dnsQueriesToday": "Aanvragen vandaag",
|
||||
"domainsBeingBlocked": "Domeinen op blokkadelijst"
|
||||
},
|
||||
"domainsTooltip": ""
|
||||
"domainsTooltip": "Vanwege meerdere integraties kan Homarr het exacte aantal geblokkeerde domeinen niet berekenen"
|
||||
},
|
||||
"dnsHoleControls": {
|
||||
"name": "DNS-hole bedieningen",
|
||||
@@ -1170,22 +1181,22 @@
|
||||
}
|
||||
},
|
||||
"minecraftServerStatus": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"name": "Minecraft server status",
|
||||
"description": "Geeft de status van een Minecraft server weer",
|
||||
"option": {
|
||||
"title": {
|
||||
"label": ""
|
||||
"label": "Titel"
|
||||
},
|
||||
"domain": {
|
||||
"label": ""
|
||||
"label": "Serveradres"
|
||||
},
|
||||
"isBedrockServer": {
|
||||
"label": ""
|
||||
"label": "Bedrock server"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"online": "",
|
||||
"offline": ""
|
||||
"online": "Online",
|
||||
"offline": "Offline"
|
||||
}
|
||||
},
|
||||
"notebook": {
|
||||
@@ -1283,7 +1294,7 @@
|
||||
},
|
||||
"error": {
|
||||
"noUrl": "Geen iFrame URL opgegeven",
|
||||
"unsupportedProtocol": "",
|
||||
"unsupportedProtocol": "De opgegeven URL gebruikt een niet-ondersteund protocol. Gebruik een van ({supportedProtocols})",
|
||||
"noBrowerSupport": "Je browser ondersteunt geen iframes. Update je browser."
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "Bestandssysteem info weergeven"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": "Standaard tab"
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": "Sectie indicator vereiste"
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": "Gezondheidsstatus ophalen mislukt"
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "CPU",
|
||||
"memory": "RAM"
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": "Nodes"
|
||||
},
|
||||
"qemu": {
|
||||
"name": "VM's"
|
||||
},
|
||||
"lxc": {
|
||||
"name": "LXC's"
|
||||
},
|
||||
"storage": {
|
||||
"name": "Opslagruimte"
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "Node",
|
||||
"vmId": "VM ID",
|
||||
"plugin": "Plugin"
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "Cores",
|
||||
"memory": "Geheugen",
|
||||
"storage": "Opslagruimte",
|
||||
"uptime": "Uptime",
|
||||
"haState": "HA status",
|
||||
"storageType": {
|
||||
"local": "Lokale opslag",
|
||||
"shared": "Gedeelde opslag"
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "Naam",
|
||||
"cpu": "CPU",
|
||||
"memory": "RAM",
|
||||
"node": "Node"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
@@ -1497,9 +1560,9 @@
|
||||
"description": "De huidige streams op je mediaservers weergeven",
|
||||
"option": {},
|
||||
"items": {
|
||||
"user": "",
|
||||
"name": "",
|
||||
"id": ""
|
||||
"user": "Gebruiker",
|
||||
"name": "Naam",
|
||||
"id": "Id"
|
||||
}
|
||||
},
|
||||
"downloads": {
|
||||
@@ -1776,16 +1839,16 @@
|
||||
"board": {
|
||||
"action": {
|
||||
"duplicate": {
|
||||
"title": "",
|
||||
"message": "",
|
||||
"title": "Bord dupliceren",
|
||||
"message": "Dit zal het bord {name} dupliceren met al zijn inhoud. Als widgets refereren aan integraties, die je niet mag gebruiken, zullen ze worden verwijderd.",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Bord gedupliceerd",
|
||||
"message": "Het bord is succesvol gedupliceerd"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Kan board niet dupliceren",
|
||||
"message": "Het bord kon niet worden gedupliceerd"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2102,7 +2165,7 @@
|
||||
"docker": "Docker",
|
||||
"logs": "Logs",
|
||||
"api": "API",
|
||||
"certificates": "",
|
||||
"certificates": "Certificaten",
|
||||
"tasks": "Taken"
|
||||
}
|
||||
},
|
||||
@@ -2156,14 +2219,14 @@
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"label": "Instellen als je mobiele bord",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
"label": "Mobiel",
|
||||
"tooltip": "Dit bord zal getoond worden als je mobiele bord"
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
"label": "Bord dupliceren"
|
||||
},
|
||||
"delete": {
|
||||
"label": "Permanent verwijderen",
|
||||
@@ -2199,13 +2262,13 @@
|
||||
"item": {
|
||||
"language": "Taal & regio",
|
||||
"board": {
|
||||
"title": "",
|
||||
"title": "Home bord",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
"general": "Algemeen",
|
||||
"mobile": "Mobiel"
|
||||
}
|
||||
},
|
||||
"defaultSearchEngine": "",
|
||||
"defaultSearchEngine": "Standaard zoekmachine",
|
||||
"firstDayOfWeek": "Eerste dag van de week",
|
||||
"accessibility": "Toegankelijkheid"
|
||||
}
|
||||
@@ -2364,15 +2427,15 @@
|
||||
"title": "Borden",
|
||||
"homeBoard": {
|
||||
"label": "Globaal home-bord",
|
||||
"mobileLabel": "",
|
||||
"mobileLabel": "Globaal mobiel bord",
|
||||
"description": "Alleen openbare borden zijn beschikbaar voor selectie"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"title": "",
|
||||
"title": "Zoeken",
|
||||
"defaultSearchEngine": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Globale standaard zoekmachine",
|
||||
"description": "Integratie zoekmachines kunnen hier niet geselecteerd worden"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
@@ -2403,7 +2466,7 @@
|
||||
},
|
||||
"job": {
|
||||
"minecraftServerStatus": {
|
||||
"label": ""
|
||||
"label": "Minecraft server status"
|
||||
},
|
||||
"iconsUpdater": {
|
||||
"label": "Icoon updater"
|
||||
@@ -2601,19 +2664,19 @@
|
||||
}
|
||||
},
|
||||
"addToHomarr": {
|
||||
"label": "",
|
||||
"label": "Toevoegen aan Homarr",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Toegevoegd aan Homarr",
|
||||
"message": "Geselecteerde apps zijn toegevoegd aan Homarr"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Kon niet toevoegen aan Homarr",
|
||||
"message": "Geselecteerde apps konden niet worden toegevoegd aan Homarr"
|
||||
}
|
||||
},
|
||||
"modal": {
|
||||
"title": ""
|
||||
"title": "Docker container('s) toevoegen aan Homarr"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2710,7 +2773,7 @@
|
||||
"label": "Logs"
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
"label": "Certificaten"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2758,7 +2821,7 @@
|
||||
"label": "Instellen als home-bord"
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
"label": "Instellen als mobiel bord"
|
||||
},
|
||||
"settings": {
|
||||
"label": "Instellingen openen"
|
||||
@@ -2826,9 +2889,9 @@
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"requestMovie": "",
|
||||
"requestSeries": "",
|
||||
"openIn": ""
|
||||
"requestMovie": "Film aanvragen",
|
||||
"requestSeries": "Series aanvragen",
|
||||
"openIn": "Openen in {kind}"
|
||||
},
|
||||
"external": {
|
||||
"help": "Gebruik een externe zoekmachine",
|
||||
@@ -2897,20 +2960,20 @@
|
||||
"home": {
|
||||
"group": {
|
||||
"search": {
|
||||
"title": "",
|
||||
"title": "Zoeken",
|
||||
"option": {
|
||||
"other": {
|
||||
"label": ""
|
||||
"label": "Zoeken met andere zoekmachine"
|
||||
},
|
||||
"no-default": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Geen standaard zoekmachine",
|
||||
"description": "Stel een standaard zoekmachine in in voorkeuren"
|
||||
},
|
||||
"search": {
|
||||
"label": ""
|
||||
"label": "Zoeken naar \"{query}\" met {name}"
|
||||
},
|
||||
"from-integration": {
|
||||
"description": ""
|
||||
"description": "Typ om te zoeken"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3092,15 +3155,15 @@
|
||||
"media": {
|
||||
"request": {
|
||||
"modal": {
|
||||
"title": "",
|
||||
"title": "\"{name}\" aanvragen",
|
||||
"table": {
|
||||
"header": {
|
||||
"season": "",
|
||||
"episodes": ""
|
||||
"season": "Seizoen",
|
||||
"episodes": "Afleveringen"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"send": ""
|
||||
"send": "Aanvraag versturen"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3110,39 +3173,39 @@
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"title": "Vertrouwde certificaten",
|
||||
"description": "Gebruikt door Homarr om gegevens op te vragen van integraties.",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
"title": "Er zijn nog geen certificaten"
|
||||
},
|
||||
"expires": ""
|
||||
"expires": "Verloopt {when}"
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"label": "Certificaat toevoegen",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Certificaat toegevoegd",
|
||||
"message": "Het certificaat is succesvol toegevoegd"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Certificaat toevoegen mislukt",
|
||||
"message": "Het certificaat kon niet worden toegevoegd"
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"label": "Certificaten verwijderen",
|
||||
"confirm": "Weet je zeker dat je het certificaat wilt verwijderen?",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Certificaat verwijderd",
|
||||
"message": "Het certificaat is succesvol verwijderd"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Certificaat niet verwijderd",
|
||||
"message": "Certificaat kon niet worden verwijderd"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Passord",
|
||||
"newLabel": "Nytt passord"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": ""
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "Vis filsysteminfo"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Hasło",
|
||||
"newLabel": "Nowe hasło"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": "Kopiuj adres URL"
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "Pokaż informacje o systemie plików"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": "Nie udało się pobrać stanu zdrowia"
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -2,123 +2,123 @@
|
||||
"init": {
|
||||
"step": {
|
||||
"start": {
|
||||
"title": "",
|
||||
"subtitle": "",
|
||||
"description": "",
|
||||
"title": "Bem-vindo ao Homarr",
|
||||
"subtitle": "Vamos começar configurando a sua instância Homarr.",
|
||||
"description": "Para começar, por favor selecione como você deseja configurar sua instância Homarr.",
|
||||
"action": {
|
||||
"scratch": "",
|
||||
"importOldmarr": ""
|
||||
"scratch": "Começar do zero",
|
||||
"importOldmarr": "Importe de uma instância Homarr anterior a 1,0"
|
||||
}
|
||||
},
|
||||
"import": {
|
||||
"title": "",
|
||||
"subtitle": "",
|
||||
"title": "Importar dados",
|
||||
"subtitle": "Você pode importar dados de uma instância existente do Homarr.",
|
||||
"dropzone": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
"title": "Arraste o arquivo zip aqui ou clique para procurar",
|
||||
"description": "O arquivo zip enviado será processado e você poderá selecionar o que deseja importar"
|
||||
},
|
||||
"fileInfo": {
|
||||
"action": {
|
||||
"change": ""
|
||||
"change": "Alterar arquivo"
|
||||
}
|
||||
},
|
||||
"importSettings": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
"title": "Importar configurações",
|
||||
"description": "Configure o comportamento da importação"
|
||||
},
|
||||
"boardSelection": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"title": "{count} quadros encontrados",
|
||||
"description": "Escolha todos os quadros com o tamanho que você deseja importar",
|
||||
"action": {
|
||||
"selectAll": "",
|
||||
"unselectAll": ""
|
||||
"selectAll": "Selecionar todos",
|
||||
"unselectAll": "Desmarcar Todos"
|
||||
}
|
||||
},
|
||||
"summary": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"title": "Resumo da importação",
|
||||
"description": "No resumo abaixo, você pode ver o que será importado",
|
||||
"action": {
|
||||
"import": ""
|
||||
"import": "Confirmar a importação e continuar"
|
||||
},
|
||||
"entities": {
|
||||
"apps": "Aplicativos",
|
||||
"boards": "Placas",
|
||||
"integrations": "",
|
||||
"credentialUsers": ""
|
||||
"integrations": "Integrações",
|
||||
"credentialUsers": "Usuários credenciados"
|
||||
}
|
||||
},
|
||||
"tokenModal": {
|
||||
"title": "",
|
||||
"title": "Insira o token de importação",
|
||||
"field": {
|
||||
"token": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Token",
|
||||
"description": "Insira o token de importação mostrado em sua instância homarr anterior"
|
||||
}
|
||||
},
|
||||
"notification": {
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Token inválido",
|
||||
"message": "O token que você inseriu é inválido"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"title": "",
|
||||
"subtitle": "",
|
||||
"title": "Usuário Administrador",
|
||||
"subtitle": "Especifique as credenciais para seu usuário administrador.",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Usuário criado",
|
||||
"message": "O usuário foi criado com sucesso"
|
||||
},
|
||||
"error": {
|
||||
"title": ""
|
||||
"title": "Falha ao criar usuário"
|
||||
}
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"title": "",
|
||||
"subtitle": "",
|
||||
"title": "Grupo externo",
|
||||
"subtitle": "Especifique o grupo que deve ser usado para usuários externos.",
|
||||
"form": {
|
||||
"name": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Nome do grupo",
|
||||
"description": "O nome tem que coincidir com o grupo administrador do provedor externo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configurações",
|
||||
"subtitle": ""
|
||||
"subtitle": "Definir as configurações do servidor."
|
||||
},
|
||||
"finish": {
|
||||
"title": "",
|
||||
"subtitle": "",
|
||||
"description": "",
|
||||
"title": "Concluir configuração",
|
||||
"subtitle": "Você está pronto para começar!",
|
||||
"description": "Você concluiu com sucesso o processo de configuração. Agora você pode começar a usar o Homarr. Selecione sua próxima ação:",
|
||||
"action": {
|
||||
"goToBoard": "",
|
||||
"createBoard": "",
|
||||
"inviteUser": "",
|
||||
"docs": ""
|
||||
"goToBoard": "Ir para o quadro {name}",
|
||||
"createBoard": "Crie seu primeiro quadro",
|
||||
"inviteUser": "Convidar outros usuários",
|
||||
"docs": "Ler a documentação"
|
||||
}
|
||||
}
|
||||
},
|
||||
"backToStart": ""
|
||||
"backToStart": "Voltar ao início"
|
||||
},
|
||||
"user": {
|
||||
"title": "Usuários",
|
||||
"name": "Usuário",
|
||||
"page": {
|
||||
"login": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
"title": "Acesse a sua conta",
|
||||
"subtitle": "Bem-vindo(a) de volta! Por favor, insira suas credenciais"
|
||||
},
|
||||
"invite": {
|
||||
"title": "",
|
||||
"subtitle": "",
|
||||
"description": ""
|
||||
"title": "Junte-se ao Homarr",
|
||||
"subtitle": "Bem-vindo ao Homarr! Por favor, crie sua conta",
|
||||
"description": "Você foi convidado(a) por {username}"
|
||||
},
|
||||
"init": {
|
||||
"title": "",
|
||||
"title": "Nova instalação do Homarr",
|
||||
"subtitle": ""
|
||||
}
|
||||
},
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Senha",
|
||||
"newLabel": "Nova senha"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": ""
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "Mostrar informações do sistema de arquivos"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Parola",
|
||||
"newLabel": "Parolă nouă"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": ""
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "Afișare informații despre sistemul de fișiere"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -213,10 +213,10 @@
|
||||
"changeDefaultSearchEngine": {
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "Поисковая система по умолчанию успешно изменена"
|
||||
},
|
||||
"error": {
|
||||
"message": ""
|
||||
"message": "Не удалось изменить поисковую систему по умолчанию"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -499,7 +499,7 @@
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"search": "",
|
||||
"search": "Поиск приложений",
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "Приложения",
|
||||
@@ -625,8 +625,8 @@
|
||||
"label": "URL-адрес"
|
||||
},
|
||||
"attemptSearchEngineCreation": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Создать поисковую систему",
|
||||
"description": "Интеграцию \"{kind}\" можно использовать с поисковыми системами. Включите эту опцию для автоматической настройки поисковой системы."
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Пароль",
|
||||
"newLabel": "Новый пароль"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "ID токена",
|
||||
"newLabel": "Новый ID токена"
|
||||
},
|
||||
"realm": {
|
||||
"label": "Область",
|
||||
"newLabel": "Новая область"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": "Копировать URL"
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -897,7 +908,7 @@
|
||||
"passwordRequirements": "Пароль не соответствует требованиям",
|
||||
"boardAlreadyExists": "Панель с таким именем уже существует",
|
||||
"invalidFileType": "Неверный тип файла, ожидается {expected}",
|
||||
"invalidFileName": "",
|
||||
"invalidFileName": "Недопустимое имя файла",
|
||||
"fileTooLarge": "Файл слишком большой, максимальный размер {maxSize}",
|
||||
"invalidConfiguration": "Неверная конфигурация",
|
||||
"groupNameTaken": "Имя группы уже занято"
|
||||
@@ -967,7 +978,7 @@
|
||||
},
|
||||
"create": {
|
||||
"title": "Выберите тип элемента",
|
||||
"search": "",
|
||||
"search": "Фильтр элементов",
|
||||
"addToBoard": "Добавить на панель"
|
||||
},
|
||||
"moveResize": {
|
||||
@@ -1094,7 +1105,7 @@
|
||||
"dnsQueriesToday": "DNS-запросов сегодня",
|
||||
"domainsBeingBlocked": "Доменов в списке блокировки"
|
||||
},
|
||||
"domainsTooltip": ""
|
||||
"domainsTooltip": "Из-за наличия нескольких интеграций Homarr не может рассчитать точное количество заблокированных доменов"
|
||||
},
|
||||
"dnsHoleControls": {
|
||||
"name": "Управление DNS-фильтром",
|
||||
@@ -1170,22 +1181,22 @@
|
||||
}
|
||||
},
|
||||
"minecraftServerStatus": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"name": "Статус сервера Minecraft",
|
||||
"description": "Отображает текущий статус и информацию о сервере Minecraft",
|
||||
"option": {
|
||||
"title": {
|
||||
"label": ""
|
||||
"label": "Заголовок"
|
||||
},
|
||||
"domain": {
|
||||
"label": ""
|
||||
"label": "Домен сервера"
|
||||
},
|
||||
"isBedrockServer": {
|
||||
"label": ""
|
||||
"label": "Сервер Bedrock Edition"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"online": "",
|
||||
"offline": ""
|
||||
"online": "В сети",
|
||||
"offline": "Не в сети"
|
||||
}
|
||||
},
|
||||
"notebook": {
|
||||
@@ -1283,7 +1294,7 @@
|
||||
},
|
||||
"error": {
|
||||
"noUrl": "URL для iFrame не указан",
|
||||
"unsupportedProtocol": "",
|
||||
"unsupportedProtocol": "Указанный URL использует неподдерживаемый протокол. Пожалуйста, используйте один из ({supportedProtocols})",
|
||||
"noBrowerSupport": "Ваш браузер не поддерживает iframes. Пожалуйста, обновите браузер."
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "Показать информацию о файловой системе"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": "Вкладка по умолчанию"
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": "Пороговое значение индикатора раздела"
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": "Не удалось получить данные о состоянии системы"
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "ЦП",
|
||||
"memory": "Память"
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": "Узлы"
|
||||
},
|
||||
"qemu": {
|
||||
"name": "Виртуальные машины"
|
||||
},
|
||||
"lxc": {
|
||||
"name": "Контейнеры LXC"
|
||||
},
|
||||
"storage": {
|
||||
"name": "Хранилище"
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "Узел",
|
||||
"vmId": "ID виртуальной машины",
|
||||
"plugin": "Плагин"
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "Ядер",
|
||||
"memory": "Память",
|
||||
"storage": "Хранилище",
|
||||
"uptime": "Время работы",
|
||||
"haState": "Состояние HA",
|
||||
"storageType": {
|
||||
"local": "Локальное",
|
||||
"shared": "Общее"
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "Название",
|
||||
"cpu": "ЦП",
|
||||
"memory": "Память",
|
||||
"node": "Узел"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
@@ -1497,9 +1560,9 @@
|
||||
"description": "Отображает текущие сеансы воспроизведения на медиасерверах",
|
||||
"option": {},
|
||||
"items": {
|
||||
"user": "",
|
||||
"name": "",
|
||||
"id": ""
|
||||
"user": "Пользователь",
|
||||
"name": "Название",
|
||||
"id": "Id"
|
||||
}
|
||||
},
|
||||
"downloads": {
|
||||
@@ -1776,16 +1839,16 @@
|
||||
"board": {
|
||||
"action": {
|
||||
"duplicate": {
|
||||
"title": "",
|
||||
"message": "",
|
||||
"title": "Дублировать панель",
|
||||
"message": "Это действие создаст копию панели {name} со всем её содержимым. Если виджеты используют интеграции, к которым у вас нет доступа, они будут удалены.",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Панель продублирована",
|
||||
"message": "Панель успешно продублирована"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Ошибка дублирования",
|
||||
"message": "Не удалось продублировать панель"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2102,7 +2165,7 @@
|
||||
"docker": "Docker",
|
||||
"logs": "Логи",
|
||||
"api": "API",
|
||||
"certificates": "",
|
||||
"certificates": "Сертификаты",
|
||||
"tasks": "Задачи"
|
||||
}
|
||||
},
|
||||
@@ -2156,14 +2219,14 @@
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"label": "Сделать домашней панелью для мобильных устройств",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
"label": "Мобильная",
|
||||
"tooltip": "Эта панель будет показываться как ваша домашняя панель на мобильных устройствах"
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
"label": "Дублировать"
|
||||
},
|
||||
"delete": {
|
||||
"label": "Удалить навсегда",
|
||||
@@ -2199,13 +2262,13 @@
|
||||
"item": {
|
||||
"language": "Язык и регион",
|
||||
"board": {
|
||||
"title": "",
|
||||
"title": "Домашняя панель",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
"general": "Основная",
|
||||
"mobile": "Мобильная"
|
||||
}
|
||||
},
|
||||
"defaultSearchEngine": "",
|
||||
"defaultSearchEngine": "Поисковая система по умолчанию",
|
||||
"firstDayOfWeek": "Первый день недели",
|
||||
"accessibility": "Специальные возможности"
|
||||
}
|
||||
@@ -2363,16 +2426,16 @@
|
||||
"board": {
|
||||
"title": "Панели",
|
||||
"homeBoard": {
|
||||
"label": "Общая домашняя панель",
|
||||
"mobileLabel": "",
|
||||
"label": "Глобальная домашняя панель",
|
||||
"mobileLabel": "Глобальная мобильная панель",
|
||||
"description": "Для выбора доступны только публичные панели"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"title": "",
|
||||
"title": "Поиск",
|
||||
"defaultSearchEngine": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Глобальная поисковая система",
|
||||
"description": "Поисковые системы из интеграций нельзя выбрать здесь"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
@@ -2403,7 +2466,7 @@
|
||||
},
|
||||
"job": {
|
||||
"minecraftServerStatus": {
|
||||
"label": ""
|
||||
"label": "Статус сервера Minecraft"
|
||||
},
|
||||
"iconsUpdater": {
|
||||
"label": "Обновление иконок"
|
||||
@@ -2601,19 +2664,19 @@
|
||||
}
|
||||
},
|
||||
"addToHomarr": {
|
||||
"label": "",
|
||||
"label": "Добавить в Homarr",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Приложение добавлено",
|
||||
"message": "Приложение успешно добавлено в Homarr"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Не удалось добавить",
|
||||
"message": "Не удалось добавить приложение в Homarr"
|
||||
}
|
||||
},
|
||||
"modal": {
|
||||
"title": ""
|
||||
"title": "Добавить контейнер(ы) Docker в Homarr"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2710,7 +2773,7 @@
|
||||
"label": "Логи"
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
"label": "Сертификаты"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2758,7 +2821,7 @@
|
||||
"label": "Сделать домашней панелью"
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
"label": "Сделать домашней панелью для мобильных устройств"
|
||||
},
|
||||
"settings": {
|
||||
"label": "Открыть настройки"
|
||||
@@ -2826,9 +2889,9 @@
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"requestMovie": "",
|
||||
"requestSeries": "",
|
||||
"openIn": ""
|
||||
"requestMovie": "Запросить фильм",
|
||||
"requestSeries": "Запросить сериал",
|
||||
"openIn": "Открыть в {kind}"
|
||||
},
|
||||
"external": {
|
||||
"help": "Использовать внешнюю поисковую систему",
|
||||
@@ -2897,20 +2960,20 @@
|
||||
"home": {
|
||||
"group": {
|
||||
"search": {
|
||||
"title": "",
|
||||
"title": "Поиск",
|
||||
"option": {
|
||||
"other": {
|
||||
"label": ""
|
||||
"label": "Искать в другой поисковой системе"
|
||||
},
|
||||
"no-default": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Нет поисковой системы по умолчанию",
|
||||
"description": "Установите поисковую систему по умолчанию в настройках"
|
||||
},
|
||||
"search": {
|
||||
"label": ""
|
||||
"label": "Искать \"{query}\" через {name}"
|
||||
},
|
||||
"from-integration": {
|
||||
"description": ""
|
||||
"description": "Начните вводить для поиска"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3092,15 +3155,15 @@
|
||||
"media": {
|
||||
"request": {
|
||||
"modal": {
|
||||
"title": "",
|
||||
"title": "Запрос \"{name}\"",
|
||||
"table": {
|
||||
"header": {
|
||||
"season": "",
|
||||
"episodes": ""
|
||||
"season": "Сезон",
|
||||
"episodes": "Серии"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"send": ""
|
||||
"send": "Отправить запрос"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3110,39 +3173,39 @@
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"title": "Доверенные сертификаты",
|
||||
"description": "Используются Homarr для запроса данных из интеграций.",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
"title": "Доверенные сертификаты отсутствуют"
|
||||
},
|
||||
"expires": ""
|
||||
"expires": "Истекает {when}"
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"label": "Создать сертификат",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Сертификат создан",
|
||||
"message": "Сертификат успешно создан"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Не удалось создать сертификат",
|
||||
"message": "Произошла ошибка при создании сертификата"
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"label": "Удалить сертификат",
|
||||
"confirm": "Вы уверены, что хотите удалить этот сертификат?",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Сертификат удален",
|
||||
"message": "Сертификат успешно удален"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Не удалось удалить сертификат",
|
||||
"message": "Произошла ошибка при удалении сертификата"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Heslo",
|
||||
"newLabel": "Nové heslo"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": "Skopírovať URL"
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "Zobraziť informácie o súborovom systéme"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": "Nepodarilo sa načítať zdravotný stav"
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Geslo",
|
||||
"newLabel": "Novo geslo"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": ""
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": ""
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Lösenord",
|
||||
"newLabel": "Nytt lösenord"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": ""
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "Visa information om filsystemet"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -712,7 +712,7 @@
|
||||
"label": "Değer ayarlanmadı",
|
||||
"tooltip": "Gizli anahtar gereklidir ve henüz ayarlanmadı"
|
||||
},
|
||||
"secureNotice": "Bu Gizli anahtar oluşturulduktan sonra geri alınamaz",
|
||||
"secureNotice": "Entegrasyonu oluşturduktan sonra gizli anahtarı tekrar görüntüleyemezsiniz",
|
||||
"reset": {
|
||||
"title": "Gizli anahtarı sıfırla",
|
||||
"message": "Gizli anahtarı sıfırlamak istediğinizden emin misiniz?"
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Şifre",
|
||||
"newLabel": "Yeni Şifre"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "Token Kimliği",
|
||||
"newLabel": "Yeni Token Kimliği"
|
||||
},
|
||||
"realm": {
|
||||
"label": "Realm",
|
||||
"newLabel": "Yeni realm"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": "URL'yi kopyala"
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -897,7 +908,7 @@
|
||||
"passwordRequirements": "Parola gereksinimleri karşılamıyor",
|
||||
"boardAlreadyExists": "Bu isimde bir panel zaten mevcut",
|
||||
"invalidFileType": "Geçersiz dosya türü, {expected} bekleniyor",
|
||||
"invalidFileName": "",
|
||||
"invalidFileName": "Geçersiz dosya adı",
|
||||
"fileTooLarge": "Dosya çok büyük, azami boyut {maxSize}",
|
||||
"invalidConfiguration": "Geçersiz yapılandırma",
|
||||
"groupNameTaken": "Grup adı zaten alınmış"
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "Dosya Sistemi Bilgilerini Göster"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": "Varsayılan sekme"
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": "Bölüm göstergesi gereksinimi"
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": "Sağlık durumu alınamadı"
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "İşlemci",
|
||||
"memory": "BELLEK"
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": "Nodes"
|
||||
},
|
||||
"qemu": {
|
||||
"name": "VM's"
|
||||
},
|
||||
"lxc": {
|
||||
"name": "LXC'ler"
|
||||
},
|
||||
"storage": {
|
||||
"name": "Depolama"
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "Node",
|
||||
"vmId": "VM Kimliği",
|
||||
"plugin": "Eklenti"
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "Çekirdekler",
|
||||
"memory": "Hafıza",
|
||||
"storage": "Depolama",
|
||||
"uptime": "Çalışma Süresi",
|
||||
"haState": "HA Durumu",
|
||||
"storageType": {
|
||||
"local": "Yerel depolama",
|
||||
"shared": "Paylaşılan depolama"
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "İsim",
|
||||
"cpu": "İşlemci",
|
||||
"memory": "BELLEK",
|
||||
"node": "Node"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
@@ -1673,7 +1736,7 @@
|
||||
"main": "Medya İstatistikleri",
|
||||
"approved": "Onaylanan",
|
||||
"pending": "Onay bekleyen",
|
||||
"processing": "İşleniyor",
|
||||
"processing": "İşlenen",
|
||||
"declined": "Reddedilen",
|
||||
"available": "Mevcut",
|
||||
"tv": "Dizi talepleri",
|
||||
@@ -2039,9 +2102,9 @@
|
||||
"error": {
|
||||
"noBoard": {
|
||||
"title": "Homarr'a Hoş Geldiniz",
|
||||
"description": "Tüm uygulamalarınızı ve hizmetlerinizi parmaklarınızın ucuna getiren şık ve modern bir gösterge paneli.",
|
||||
"description": "Tüm uygulamalarınızı ve hizmetlerinizi parmaklarınızın ucuna getiren şık ve modern kontrol paneli.",
|
||||
"link": "İlk panelinizi oluşturun",
|
||||
"notice": "Bu sayfanın kaybolmasını sağlamak için bir panel oluşturun ve bunu ana panel olarak ayarlayın"
|
||||
"notice": "Bu sayfanın kaybolmasını sağlamak için bir panel oluşturun ve bunu ön tanımlı panel olarak atayın"
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Panel bulunamadı",
|
||||
@@ -2102,7 +2165,7 @@
|
||||
"docker": "Docker",
|
||||
"logs": "Günlükler",
|
||||
"api": "API",
|
||||
"certificates": "",
|
||||
"certificates": "Sertifikalar",
|
||||
"tasks": "Görevler"
|
||||
}
|
||||
},
|
||||
@@ -2710,7 +2773,7 @@
|
||||
"label": "Günlükler"
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
"label": "Sertifikalar"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -3110,39 +3173,39 @@
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"title": "Güvenilen sertifikalar",
|
||||
"description": "Homarr tarafından, Entegrasyonlardan veri çekmek için kullanılır.",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
"title": "Henüz sertifika yok"
|
||||
},
|
||||
"expires": ""
|
||||
"expires": "{when} Sonra Süresi Doluyor"
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"label": "Sertifika ekle",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Sertifika eklendi",
|
||||
"message": "Sertifika başarıyla eklendi"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Sertifika eklenemedi",
|
||||
"message": "Sertifika eklenemedi"
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"label": "Sertifikayı kaldır",
|
||||
"confirm": "Sertifikayı kaldırmak istediğinizden emin misiniz?",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Sertifika kaldırıldı",
|
||||
"message": "Sertifika başarıyla kaldırıldı"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Sertifika kaldırılmadı",
|
||||
"message": "Sertifika kaldırılamadı"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,19 +316,19 @@
|
||||
"item": {
|
||||
"create": {
|
||||
"label": "Створення додатків",
|
||||
"description": ""
|
||||
"description": "Дозволити учасникам створювати додатки"
|
||||
},
|
||||
"use-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Використовувати всі додатки",
|
||||
"description": "Дозволити учасникам додавати будь-які додатки до своїх дошок"
|
||||
},
|
||||
"modify-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Змінювати всі додатки",
|
||||
"description": "Дозволити учасникам змінювати всі додатки"
|
||||
},
|
||||
"full-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Повний доступ до додатка",
|
||||
"description": "Дозволити учасникам керувати, використовувати і видаляти будь-які додатки"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -336,84 +336,84 @@
|
||||
"title": "Дошки",
|
||||
"item": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Створити дошки",
|
||||
"description": "Дозволити учасникам створювати дошки"
|
||||
},
|
||||
"view-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Переглядати всі дошки",
|
||||
"description": "Дозволити учасникам переглядати всі дошки"
|
||||
},
|
||||
"modify-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Змінювати всі дошки",
|
||||
"description": "Дозволити учасникам змінювати всі дошки (не включає контроль доступу та небезпечну зону)"
|
||||
},
|
||||
"full-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Повний доступ до дошки",
|
||||
"description": "Дозволити учасникам переглядати, змінювати та видаляти всі дошки (включаючи контроль доступу та небезпечну зону)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"integration": {
|
||||
"title": "",
|
||||
"title": "Інтеграції",
|
||||
"item": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Створювати інтеграції",
|
||||
"description": "Дозволити учасникам створювати інтеграції"
|
||||
},
|
||||
"use-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Використовувати всі інтеграції",
|
||||
"description": "Дозволяє учасникам додавати будь-які інтеграції до своїх дошок"
|
||||
},
|
||||
"interact-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Взаємодіяти з будь-якою інтеграцією",
|
||||
"description": "Дозволити учасникам взаємодіяти з будь-якою інтеграцією"
|
||||
},
|
||||
"full-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Повний доступ до інтеграції",
|
||||
"description": "Дозволити учасникам керувати, використовувати та взаємодіяти з будь-якою інтеграцією"
|
||||
}
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"title": "",
|
||||
"title": "Медіа",
|
||||
"item": {
|
||||
"upload": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Завантажити медіафайли",
|
||||
"description": "Дозволити учасникам завантажувати медіафайли"
|
||||
},
|
||||
"view-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Переглядати всі медіафайли",
|
||||
"description": "Дозволити учасникам переглядати всі медіафайли"
|
||||
},
|
||||
"full-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Повний доступ до медіафайлів",
|
||||
"description": "Дозволити учасникам керувати та видаляти будь-які медіафайли"
|
||||
}
|
||||
}
|
||||
},
|
||||
"other": {
|
||||
"title": "",
|
||||
"title": "Інше",
|
||||
"item": {
|
||||
"view-logs": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Переглядати логи",
|
||||
"description": "Дозволити учасникам переглядати логи"
|
||||
}
|
||||
}
|
||||
},
|
||||
"search-engine": {
|
||||
"title": "",
|
||||
"title": "Пошукові системи",
|
||||
"item": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Створення пошукових систем",
|
||||
"description": "Дозволити учасникам створювати пошукові системи"
|
||||
},
|
||||
"modify-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Змінити всі пошукові системи",
|
||||
"description": "Дозволити учасникам змінювати всі пошукові системи"
|
||||
},
|
||||
"full-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Повний доступ до пошукових систем",
|
||||
"description": "Дозволити учасникам керувати та видаляти будь-які пошукові системи"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -423,24 +423,24 @@
|
||||
"external": ""
|
||||
},
|
||||
"reservedNotice": {
|
||||
"message": ""
|
||||
"message": "Ця група зарезервована для використання системою та обмежує деякі дії. <checkoutDocs></checkoutDocs>"
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"label": "Нова група",
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "Групу успішно створено"
|
||||
},
|
||||
"error": {
|
||||
"message": ""
|
||||
"message": "Не вдалося створити групу"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transfer": {
|
||||
"label": "",
|
||||
"description": "",
|
||||
"confirm": "",
|
||||
"confirm": "Ви впевнені, що бажаєте передати право власності на групу {name} користувачу {username}?",
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": "Група {group} перенесена до {user}"
|
||||
@@ -499,7 +499,7 @@
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"search": "",
|
||||
"search": "Пошук додатків",
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "Додатки",
|
||||
@@ -594,25 +594,25 @@
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "Зміни застосовано",
|
||||
"message": ""
|
||||
"message": "Інтеграція успішно збережена"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Не вдалося застосувати зміни",
|
||||
"message": "Не вдалося зберегти інтеграцію"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"title": "",
|
||||
"message": "",
|
||||
"title": "Видалити інтеграцію",
|
||||
"message": "Ви впевнені, що хочете видалити інтеграцію {name}?",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Успішно видалено",
|
||||
"message": "Інтеграцію успішно видалено"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Не вдалося видалити",
|
||||
"message": "Не вдалося видалити інтеграцію"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -625,23 +625,23 @@
|
||||
"label": ""
|
||||
},
|
||||
"attemptSearchEngineCreation": {
|
||||
"label": "",
|
||||
"label": "Створення пошукових систем",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": ""
|
||||
"create": "Нова інтеграція"
|
||||
},
|
||||
"testConnection": {
|
||||
"action": {
|
||||
"create": "",
|
||||
"edit": ""
|
||||
"create": "Перевірити підключення та створити",
|
||||
"edit": "Перевірити підключення та зберегти"
|
||||
},
|
||||
"alertNotice": "",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "З'єднання встановлено",
|
||||
"message": "З'єднання успішно встановлено"
|
||||
},
|
||||
"invalidUrl": {
|
||||
"title": "Неправильна URL-адреса",
|
||||
@@ -656,11 +656,11 @@
|
||||
"message": ""
|
||||
},
|
||||
"commonError": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Помилка підключення",
|
||||
"message": "Не вдалося встановити з'єднання"
|
||||
},
|
||||
"badRequest": {
|
||||
"title": "",
|
||||
"title": "Невірний запит",
|
||||
"message": ""
|
||||
},
|
||||
"unauthorized": {
|
||||
@@ -668,28 +668,28 @@
|
||||
"message": ""
|
||||
},
|
||||
"forbidden": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Заборонено",
|
||||
"message": "Ймовірно, відсутні дозволи"
|
||||
},
|
||||
"notFound": {
|
||||
"title": "",
|
||||
"title": "Не знайдено",
|
||||
"message": ""
|
||||
},
|
||||
"internalServerError": {
|
||||
"title": "",
|
||||
"title": "Внутрішня помилка сервера",
|
||||
"message": ""
|
||||
},
|
||||
"serviceUnavailable": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"message": "Сервер зараз недоступний"
|
||||
},
|
||||
"connectionAborted": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
},
|
||||
"domainNotFound": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Домен не знайдено",
|
||||
"message": "Домен не знайдено"
|
||||
},
|
||||
"connectionRefused": {
|
||||
"title": "",
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Пароль",
|
||||
"newLabel": "Новий пароль"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -744,7 +752,7 @@
|
||||
},
|
||||
"media": {
|
||||
"plural": "",
|
||||
"search": "",
|
||||
"search": "Пошук медіафайлів",
|
||||
"field": {
|
||||
"name": "Ім’я",
|
||||
"size": "Розмір",
|
||||
@@ -753,7 +761,7 @@
|
||||
"action": {
|
||||
"upload": {
|
||||
"label": "",
|
||||
"file": "",
|
||||
"file": "Виберіть файл",
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
@@ -777,11 +785,14 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": ""
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"beta": "",
|
||||
"beta": "Бета-версія",
|
||||
"error": "Помилка",
|
||||
"action": {
|
||||
"add": "Додати",
|
||||
@@ -789,7 +800,7 @@
|
||||
"backToOverview": "",
|
||||
"create": "Створити",
|
||||
"edit": "Редагувати",
|
||||
"import": "",
|
||||
"import": "Імпорт",
|
||||
"insert": "Вставити",
|
||||
"remove": "Видалити",
|
||||
"save": "Зберегти",
|
||||
@@ -798,13 +809,13 @@
|
||||
"delete": "Видалити",
|
||||
"discard": "",
|
||||
"confirm": "Підтвердити",
|
||||
"continue": "",
|
||||
"continue": "Продовжити",
|
||||
"previous": "Попередній",
|
||||
"next": "Далі",
|
||||
"checkoutDocs": "",
|
||||
"checkLogs": "",
|
||||
"checkLogs": "Щоб дізнатися більше, перевірте логи",
|
||||
"tryAgain": "Повторіть спробу",
|
||||
"loading": ""
|
||||
"loading": "Завантаження"
|
||||
},
|
||||
"here": "",
|
||||
"iconPicker": {
|
||||
@@ -813,29 +824,29 @@
|
||||
},
|
||||
"colorScheme": {
|
||||
"options": {
|
||||
"light": "",
|
||||
"dark": ""
|
||||
"light": "Світла",
|
||||
"dark": "Темна"
|
||||
}
|
||||
},
|
||||
"information": {
|
||||
"min": "",
|
||||
"max": "",
|
||||
"days": "",
|
||||
"hours": "",
|
||||
"minutes": ""
|
||||
"days": "Днів",
|
||||
"hours": "Годин",
|
||||
"minutes": "Хвилин"
|
||||
},
|
||||
"notification": {
|
||||
"create": {
|
||||
"success": "",
|
||||
"error": ""
|
||||
"success": "Успішно створено",
|
||||
"error": "Не вдалося створити"
|
||||
},
|
||||
"delete": {
|
||||
"success": "",
|
||||
"error": ""
|
||||
"success": "Успішно видалено",
|
||||
"error": "Не вдалося видалити"
|
||||
},
|
||||
"update": {
|
||||
"success": "",
|
||||
"error": ""
|
||||
"success": "Зміни успішно застосовано",
|
||||
"error": "Не вдалося застосувати зміни"
|
||||
},
|
||||
"transfer": {
|
||||
"success": "",
|
||||
@@ -847,7 +858,7 @@
|
||||
},
|
||||
"multiText": {
|
||||
"placeholder": "",
|
||||
"addLabel": ""
|
||||
"addLabel": "Додати {value}"
|
||||
},
|
||||
"select": {
|
||||
"placeholder": "",
|
||||
@@ -857,14 +868,14 @@
|
||||
},
|
||||
"userAvatar": {
|
||||
"menu": {
|
||||
"switchToDarkMode": "",
|
||||
"switchToLightMode": "",
|
||||
"management": "",
|
||||
"switchToDarkMode": "Перемикнути на темну тему",
|
||||
"switchToLightMode": "Перемикнути на світлу тему",
|
||||
"management": "Управління",
|
||||
"preferences": "Ваші уподобання",
|
||||
"logout": "",
|
||||
"logout": "Вийти",
|
||||
"login": "Логін",
|
||||
"homeBoard": "",
|
||||
"loggedOut": "",
|
||||
"loggedOut": "Ви вийшли",
|
||||
"updateAvailable": ""
|
||||
}
|
||||
},
|
||||
@@ -893,9 +904,9 @@
|
||||
"number": "Це поле повинно бути менше або дорівнювати {maximum}"
|
||||
},
|
||||
"custom": {
|
||||
"passwordsDoNotMatch": "",
|
||||
"passwordRequirements": "",
|
||||
"boardAlreadyExists": "",
|
||||
"passwordsDoNotMatch": "Паролі не збігаються",
|
||||
"passwordRequirements": "Пароль не відповідає вимогам",
|
||||
"boardAlreadyExists": "Дошка з такою назвою вже існує",
|
||||
"invalidFileType": "",
|
||||
"invalidFileName": "",
|
||||
"fileTooLarge": "",
|
||||
@@ -923,29 +934,29 @@
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": "",
|
||||
"edit": "",
|
||||
"remove": "",
|
||||
"create": "Нова категорія",
|
||||
"edit": "Перейменувати категорію",
|
||||
"remove": "Видалити категорію",
|
||||
"moveUp": "Рухайся.",
|
||||
"moveDown": "Вниз.",
|
||||
"createAbove": "",
|
||||
"createBelow": ""
|
||||
},
|
||||
"create": {
|
||||
"title": "",
|
||||
"submit": ""
|
||||
"title": "Нова категорія",
|
||||
"submit": "Додати категорію"
|
||||
},
|
||||
"remove": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Видалити категорію",
|
||||
"message": "Ви впевнені, що хочете видалити категорію {name}?"
|
||||
},
|
||||
"edit": {
|
||||
"title": "",
|
||||
"submit": ""
|
||||
"title": "Перейменувати категорію",
|
||||
"submit": "Перейменувати категорію"
|
||||
},
|
||||
"menu": {
|
||||
"label": {
|
||||
"create": "",
|
||||
"create": "Нова категорія",
|
||||
"changePosition": "Змінити положення"
|
||||
}
|
||||
}
|
||||
@@ -1137,7 +1148,7 @@
|
||||
},
|
||||
"clock": {
|
||||
"name": "",
|
||||
"description": "Відображає поточну дату і час.",
|
||||
"description": "Показує поточні дату і час.",
|
||||
"option": {
|
||||
"customTitleToggle": {
|
||||
"label": "",
|
||||
@@ -1193,7 +1204,7 @@
|
||||
"description": "",
|
||||
"option": {
|
||||
"showToolbar": {
|
||||
"label": "Показати панель інструментів для написання націнки"
|
||||
"label": "Показати панель інструментів для допомоги з розміткою"
|
||||
},
|
||||
"allowReadOnlyCheck": {
|
||||
"label": ""
|
||||
@@ -1235,9 +1246,9 @@
|
||||
"deleteRow": ""
|
||||
},
|
||||
"align": {
|
||||
"left": "Ліворуч.",
|
||||
"left": "Ліворуч",
|
||||
"center": "Центр",
|
||||
"right": "Так."
|
||||
"right": "Праворуч"
|
||||
},
|
||||
"popover": {
|
||||
"clearColor": "",
|
||||
@@ -1250,7 +1261,7 @@
|
||||
}
|
||||
},
|
||||
"iframe": {
|
||||
"name": "IFrame",
|
||||
"name": "iFrame",
|
||||
"description": "Вставити будь-який контент з інтернету. Деякі вебсайти можуть обмежувати доступ.",
|
||||
"option": {
|
||||
"embedUrl": {
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": ""
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
@@ -1776,16 +1839,16 @@
|
||||
"board": {
|
||||
"action": {
|
||||
"duplicate": {
|
||||
"title": "",
|
||||
"message": "",
|
||||
"title": "Скопіювати дошку",
|
||||
"message": "Це продублює дошку {name} з усім її вмістом. Якщо віджети посилаються на інтеграції, які ви не можете використовувати, їх буде видалено.",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Дошку скопійовано",
|
||||
"message": "Дошку успішно продубльовано"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Неможливо продублювати дошку",
|
||||
"message": "Дошку не може бути продубльовано"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1806,7 +1869,7 @@
|
||||
}
|
||||
},
|
||||
"oldImport": {
|
||||
"label": "",
|
||||
"label": "Імпорт з homarr до версії 1.0.0",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
@@ -1825,8 +1888,8 @@
|
||||
"apps": {
|
||||
"label": "Додатки",
|
||||
"avoidDuplicates": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Уникати дублікатів",
|
||||
"description": "Ігнорує додатки, якщо додаток з таким самим посиланням вже існує"
|
||||
},
|
||||
"onlyImportApps": {
|
||||
"label": "",
|
||||
@@ -2108,12 +2171,12 @@
|
||||
},
|
||||
"settings": "Налаштування",
|
||||
"help": {
|
||||
"label": "Допоможіть!",
|
||||
"label": "Допомога",
|
||||
"items": {
|
||||
"documentation": "Документація",
|
||||
"submitIssue": "",
|
||||
"discord": "Розбрат у громаді",
|
||||
"sourceCode": ""
|
||||
"submitIssue": "Повідомити про проблему",
|
||||
"discord": "Discord",
|
||||
"sourceCode": "Вихідний код"
|
||||
}
|
||||
},
|
||||
"about": "Про програму"
|
||||
@@ -2140,30 +2203,30 @@
|
||||
"title": "Твої дошки",
|
||||
"action": {
|
||||
"new": {
|
||||
"label": ""
|
||||
"label": "Нова дошка"
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
"label": "Відкрити дошку"
|
||||
},
|
||||
"settings": {
|
||||
"label": "Налаштування"
|
||||
},
|
||||
"setHomeBoard": {
|
||||
"label": "",
|
||||
"label": "Встановити як домашню дошку",
|
||||
"badge": {
|
||||
"label": "Головна",
|
||||
"tooltip": ""
|
||||
"tooltip": "Ця дошка буде вашою домашньою дошкою"
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"label": "Встановити як мобільну дошку",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
"tooltip": "Ця дошка буде вашою мобільною дошкою"
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
"label": ""
|
||||
"label": "Скопіювати дошку"
|
||||
},
|
||||
"delete": {
|
||||
"label": "Видалити назавжди",
|
||||
@@ -2314,81 +2377,81 @@
|
||||
"title": "Налаштування",
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "Налаштування успішно збережено"
|
||||
},
|
||||
"error": {
|
||||
"message": ""
|
||||
"message": "Не вдалося зберегти налаштування"
|
||||
}
|
||||
},
|
||||
"section": {
|
||||
"analytics": {
|
||||
"title": "",
|
||||
"title": "Аналітика",
|
||||
"general": {
|
||||
"title": "",
|
||||
"text": ""
|
||||
"title": "Надсилати анонімну аналітику",
|
||||
"text": "Homarr буде відправляти анонімну аналітику з використанням програмного забезпечення з відкритим вихідним кодом Umami. Він ніколи не збирає жодної особистої інформації і, таким чином, сумісний з GDPR та CCPA. Ми заохочуємо вас увімкнути аналітику, тому що це допомагає нашій команді визначати проблеми та пріоритетні задачі."
|
||||
},
|
||||
"widgetData": {
|
||||
"title": "",
|
||||
"text": ""
|
||||
"title": "Дані віджетів",
|
||||
"text": "Надсилати, які віджети (і їх кількість) ви налаштували. Не містить URL, назви, або будь-які інші дані."
|
||||
},
|
||||
"integrationData": {
|
||||
"title": "",
|
||||
"text": ""
|
||||
"title": "Дані інтеграції",
|
||||
"text": "Надсилати, які віджети (і їх кількість) ви налаштували. Не містить URL, назви, або будь-які інші дані."
|
||||
},
|
||||
"usersData": {
|
||||
"title": "",
|
||||
"text": ""
|
||||
"title": "Дані користувачів",
|
||||
"text": "Надсилати кількість користувачів і чи активували ви SSO"
|
||||
}
|
||||
},
|
||||
"crawlingAndIndexing": {
|
||||
"title": "",
|
||||
"warning": "",
|
||||
"title": "Пошукові сканери та індексація",
|
||||
"warning": "Увімкнення або вимкнення будь-яких налаштувань тут серйозно вплине на те, як пошукові системи індексуватимуть і скануватимуть вашу сторінку. Будь-яке налаштування є запитом, і пошуковий сканер має застосувати ці налаштування. Будь-які зміни можуть зайняти кілька днів або тижнів. Деякі налаштування можуть залежати від пошукової системи.",
|
||||
"noIndex": {
|
||||
"title": "",
|
||||
"text": ""
|
||||
"title": "Не індексувати (No Index)",
|
||||
"text": "Не індексувати сайт у пошукових системах і не показувати його в жодному результаті пошуку"
|
||||
},
|
||||
"noFollow": {
|
||||
"title": "",
|
||||
"text": ""
|
||||
"title": "Не переходити (No follow)",
|
||||
"text": "Не переходьте за посиланнями під час індексації. Якщо вимкнути це, то сканери намагатимуться переходити за всіма посиланнями на Homarr."
|
||||
},
|
||||
"noTranslate": {
|
||||
"title": "",
|
||||
"text": ""
|
||||
"title": "Не перекладати (No translate)",
|
||||
"text": "Якщо мова сайту не є тією, яку користувач, імовірно, захоче прочитати, Google покаже посилання на переклад у результатах пошуку"
|
||||
},
|
||||
"noSiteLinksSearchBox": {
|
||||
"title": "",
|
||||
"text": ""
|
||||
"title": "Без вікна пошуку з посиланнями на сайт",
|
||||
"text": "Google створює вікно пошуку зі просканованими посиланнями разом з іншими прямими посиланнями. Ввімкнення цього налаштування просить Google відключити таке вікно пошуку."
|
||||
}
|
||||
},
|
||||
"board": {
|
||||
"title": "Дошки",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"mobileLabel": "",
|
||||
"description": ""
|
||||
"label": "Глобальна домашня дошка",
|
||||
"mobileLabel": "Глобальна мобільна дошка",
|
||||
"description": "Тут доступні тільки публічні дошки"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"title": "",
|
||||
"title": "Пошук",
|
||||
"defaultSearchEngine": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Глобальна система пошуку за замовчуванням",
|
||||
"description": "Пошукові системи з інтеграцій тут недоступні"
|
||||
}
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Вигляд",
|
||||
"defaultColorScheme": {
|
||||
"label": "",
|
||||
"label": "Стандартна кольорова тема",
|
||||
"options": {
|
||||
"light": "",
|
||||
"dark": ""
|
||||
"light": "Світла",
|
||||
"dark": "Темна"
|
||||
}
|
||||
}
|
||||
},
|
||||
"culture": {
|
||||
"title": "",
|
||||
"title": "Культура",
|
||||
"defaultLocale": {
|
||||
"label": ""
|
||||
"label": "Стандартна мова"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2485,20 +2548,20 @@
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"version": "",
|
||||
"text": "",
|
||||
"version": "Версія {version}",
|
||||
"text": "Homarr — це проєкт із відкритим кодом, керований спільнотою, який підтримується волонтерами. Завдяки цим людям проєкт Homarr розвивається з 2021 року. Наша команда працює над Homarr абсолютно віддалено з різних країн у вільний час без жодної компенсації.",
|
||||
"accordion": {
|
||||
"contributors": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
"title": "Учасники",
|
||||
"subtitle": "{count} - стільки людей розробляє код та Homarr"
|
||||
},
|
||||
"translators": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
"title": "Перекладачі",
|
||||
"subtitle": "{count} - стільки людей допомагають перекладати багатьма мовами"
|
||||
},
|
||||
"libraries": {
|
||||
"title": "",
|
||||
"subtitle": ""
|
||||
"title": "Бібліотеки",
|
||||
"subtitle": "{count} - стільки бібліотек використано в коді Homarr"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2729,14 +2792,14 @@
|
||||
},
|
||||
"mode": {
|
||||
"appIntegrationBoard": {
|
||||
"help": "",
|
||||
"help": "Пошук додатків, інтеграцій, або дошок",
|
||||
"group": {
|
||||
"app": {
|
||||
"title": "Додатки",
|
||||
"children": {
|
||||
"action": {
|
||||
"open": {
|
||||
"label": ""
|
||||
"label": "Відкрити посилання на додаток"
|
||||
},
|
||||
"edit": {
|
||||
"label": ""
|
||||
@@ -2752,7 +2815,7 @@
|
||||
"children": {
|
||||
"action": {
|
||||
"open": {
|
||||
"label": ""
|
||||
"label": "Відкрити дошку"
|
||||
},
|
||||
"homeBoard": {
|
||||
"label": ""
|
||||
@@ -2761,7 +2824,7 @@
|
||||
"label": ""
|
||||
},
|
||||
"settings": {
|
||||
"label": ""
|
||||
"label": "Відкрити налаштування"
|
||||
}
|
||||
},
|
||||
"detail": {
|
||||
@@ -2796,7 +2859,7 @@
|
||||
}
|
||||
},
|
||||
"newBoard": {
|
||||
"label": ""
|
||||
"label": "Створити нову дошку"
|
||||
},
|
||||
"importBoard": {
|
||||
"label": ""
|
||||
@@ -2831,7 +2894,7 @@
|
||||
"openIn": ""
|
||||
},
|
||||
"external": {
|
||||
"help": "",
|
||||
"help": "Використовувати зовнішню пошукову систему",
|
||||
"group": {
|
||||
"searchEngine": {
|
||||
"title": "",
|
||||
@@ -2876,19 +2939,19 @@
|
||||
"help": {
|
||||
"group": {
|
||||
"mode": {
|
||||
"title": ""
|
||||
"title": "Режими"
|
||||
},
|
||||
"help": {
|
||||
"title": "Допоможіть!",
|
||||
"title": "Допомога",
|
||||
"option": {
|
||||
"documentation": {
|
||||
"label": "Документація"
|
||||
},
|
||||
"submitIssue": {
|
||||
"label": ""
|
||||
"label": "Повідомити про проблему"
|
||||
},
|
||||
"discord": {
|
||||
"label": "Розбрат у громаді"
|
||||
"label": "Discord"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2920,7 +2983,7 @@
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"help": "",
|
||||
"help": "Пошук сторінок",
|
||||
"group": {
|
||||
"page": {
|
||||
"title": "",
|
||||
@@ -2981,7 +3044,7 @@
|
||||
}
|
||||
},
|
||||
"userGroup": {
|
||||
"help": "",
|
||||
"help": "Пошук користувачів або груп",
|
||||
"group": {
|
||||
"user": {
|
||||
"title": "Користувачі",
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "Mật khẩu",
|
||||
"newLabel": "Mật khẩu mới"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"realm": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": ""
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": ""
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": ""
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": ""
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "",
|
||||
"memory": ""
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": ""
|
||||
},
|
||||
"qemu": {
|
||||
"name": ""
|
||||
},
|
||||
"lxc": {
|
||||
"name": ""
|
||||
},
|
||||
"storage": {
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "",
|
||||
"vmId": "",
|
||||
"plugin": ""
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"storage": "",
|
||||
"uptime": "",
|
||||
"haState": "",
|
||||
"storageType": {
|
||||
"local": "",
|
||||
"shared": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "",
|
||||
"cpu": "",
|
||||
"memory": "",
|
||||
"node": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
|
||||
@@ -733,6 +733,14 @@
|
||||
"password": {
|
||||
"label": "密碼",
|
||||
"newLabel": "新密碼"
|
||||
},
|
||||
"tokenId": {
|
||||
"label": "密鑰 ID",
|
||||
"newLabel": "新密鑰 ID"
|
||||
},
|
||||
"realm": {
|
||||
"label": "領域",
|
||||
"newLabel": "新領域"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -777,6 +785,9 @@
|
||||
},
|
||||
"copy": {
|
||||
"label": "複製網址"
|
||||
},
|
||||
"open": {
|
||||
"label": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -897,7 +908,7 @@
|
||||
"passwordRequirements": "密碼不符合要求",
|
||||
"boardAlreadyExists": "此面板名稱已存在",
|
||||
"invalidFileType": "無效的檔案類型,例如 {expected}",
|
||||
"invalidFileName": "",
|
||||
"invalidFileName": "無效的檔案名稱",
|
||||
"fileTooLarge": "檔案太大,最大大小為 {maxSize}",
|
||||
"invalidConfiguration": "無效設定",
|
||||
"groupNameTaken": "用戶組名稱已存在"
|
||||
@@ -1411,6 +1422,12 @@
|
||||
},
|
||||
"fileSystem": {
|
||||
"label": "顯示檔案系統訊息"
|
||||
},
|
||||
"defaultTab": {
|
||||
"label": "預設頁面"
|
||||
},
|
||||
"sectionIndicatorRequirement": {
|
||||
"label": "部分指示需求"
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
@@ -1430,6 +1447,52 @@
|
||||
"memory": {},
|
||||
"error": {
|
||||
"internalServerError": "取得健康狀態失敗"
|
||||
},
|
||||
"cluster": {
|
||||
"summary": {
|
||||
"cpu": "處理器",
|
||||
"memory": "記憶體"
|
||||
},
|
||||
"resource": {
|
||||
"node": {
|
||||
"name": "節點數"
|
||||
},
|
||||
"qemu": {
|
||||
"name": "虛擬機"
|
||||
},
|
||||
"lxc": {
|
||||
"name": "LXCs"
|
||||
},
|
||||
"storage": {
|
||||
"name": "儲存裝置"
|
||||
}
|
||||
},
|
||||
"popover": {
|
||||
"rightSection": {
|
||||
"node": "節點",
|
||||
"vmId": "虛擬機 ID",
|
||||
"plugin": "插件"
|
||||
},
|
||||
"detail": {
|
||||
"cpu": "核心數",
|
||||
"memory": "記憶體",
|
||||
"storage": "儲存裝置",
|
||||
"uptime": "運行時間",
|
||||
"haState": "HA 狀態",
|
||||
"storageType": {
|
||||
"local": "本機儲存",
|
||||
"shared": "共享儲存"
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"name": "名稱",
|
||||
"cpu": "處理器",
|
||||
"memory": "記憶體",
|
||||
"node": "節點"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
@@ -2102,7 +2165,7 @@
|
||||
"docker": "Docker",
|
||||
"logs": "Logs",
|
||||
"api": "API",
|
||||
"certificates": "",
|
||||
"certificates": "憑證",
|
||||
"tasks": "任務"
|
||||
}
|
||||
},
|
||||
@@ -2156,10 +2219,10 @@
|
||||
}
|
||||
},
|
||||
"setMobileHomeBoard": {
|
||||
"label": "",
|
||||
"label": "設定為移動裝置面板",
|
||||
"badge": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
"label": "移動裝置",
|
||||
"tooltip": "此面板將顯示為移動裝置主面板"
|
||||
}
|
||||
},
|
||||
"duplicate": {
|
||||
@@ -2199,10 +2262,10 @@
|
||||
"item": {
|
||||
"language": "語言與地區",
|
||||
"board": {
|
||||
"title": "",
|
||||
"title": "主面板",
|
||||
"type": {
|
||||
"general": "",
|
||||
"mobile": ""
|
||||
"general": "一般",
|
||||
"mobile": "移動裝置"
|
||||
}
|
||||
},
|
||||
"defaultSearchEngine": "預設搜尋引擎",
|
||||
@@ -2364,7 +2427,7 @@
|
||||
"title": "面板",
|
||||
"homeBoard": {
|
||||
"label": "全局主面板",
|
||||
"mobileLabel": "",
|
||||
"mobileLabel": "全局移動裝置主面板",
|
||||
"description": "只有公開面板可供選擇"
|
||||
}
|
||||
},
|
||||
@@ -2710,7 +2773,7 @@
|
||||
"label": "Logs"
|
||||
},
|
||||
"certificates": {
|
||||
"label": ""
|
||||
"label": "憑證"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -2758,7 +2821,7 @@
|
||||
"label": "設定為主面板"
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": ""
|
||||
"label": "設定為移動裝置面板"
|
||||
},
|
||||
"settings": {
|
||||
"label": "開啟設定"
|
||||
@@ -3110,39 +3173,39 @@
|
||||
"certificate": {
|
||||
"page": {
|
||||
"list": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"title": "可信任的憑證",
|
||||
"description": "由 Homarr 用於集成中請求數據",
|
||||
"noResults": {
|
||||
"title": ""
|
||||
"title": "尚無憑證"
|
||||
},
|
||||
"expires": ""
|
||||
"expires": "到期 {when}"
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"label": "新增憑證",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "憑證已新增",
|
||||
"message": "此憑證已新增成功"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "新增憑證失敗",
|
||||
"message": "此憑證無法新增"
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"label": "",
|
||||
"confirm": "",
|
||||
"label": "移除憑證",
|
||||
"confirm": "確定要移除這個憑證?",
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "憑證已移除",
|
||||
"message": "此憑證已移除成功"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "憑證未被移除",
|
||||
"message": "此憑證無法被移除"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,12 @@
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.16.0",
|
||||
"@mantine/dates": "^7.16.0",
|
||||
"@mantine/hooks": "^7.16.0",
|
||||
"@tabler/icons-react": "^3.28.1",
|
||||
"@mantine/core": "^7.16.1",
|
||||
"@mantine/dates": "^7.16.1",
|
||||
"@mantine/hooks": "^7.16.1",
|
||||
"@tabler/icons-react": "^3.29.0",
|
||||
"mantine-react-table": "2.0.0-beta.8",
|
||||
"next": "15.1.5",
|
||||
"next": "15.1.6",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
},
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@extractus/feed-extractor": "^7.1.3",
|
||||
"@homarr/api": "workspace:^0.1.0",
|
||||
"@homarr/auth": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
@@ -41,28 +40,28 @@
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.16.0",
|
||||
"@mantine/hooks": "^7.16.0",
|
||||
"@tabler/icons-react": "^3.28.1",
|
||||
"@tiptap/extension-color": "2.11.2",
|
||||
"@tiptap/extension-highlight": "2.11.2",
|
||||
"@tiptap/extension-image": "2.11.2",
|
||||
"@tiptap/extension-link": "^2.11.2",
|
||||
"@tiptap/extension-table": "2.11.2",
|
||||
"@tiptap/extension-table-cell": "2.11.2",
|
||||
"@tiptap/extension-table-header": "2.11.2",
|
||||
"@tiptap/extension-table-row": "2.11.2",
|
||||
"@tiptap/extension-task-item": "2.11.2",
|
||||
"@tiptap/extension-task-list": "2.11.2",
|
||||
"@tiptap/extension-text-align": "2.11.2",
|
||||
"@tiptap/extension-text-style": "2.11.2",
|
||||
"@tiptap/extension-underline": "2.11.2",
|
||||
"@tiptap/react": "^2.11.2",
|
||||
"@tiptap/starter-kit": "^2.11.2",
|
||||
"@mantine/core": "^7.16.1",
|
||||
"@mantine/hooks": "^7.16.1",
|
||||
"@tabler/icons-react": "^3.29.0",
|
||||
"@tiptap/extension-color": "2.11.3",
|
||||
"@tiptap/extension-highlight": "2.11.3",
|
||||
"@tiptap/extension-image": "2.11.3",
|
||||
"@tiptap/extension-link": "^2.11.3",
|
||||
"@tiptap/extension-table": "2.11.3",
|
||||
"@tiptap/extension-table-cell": "2.11.3",
|
||||
"@tiptap/extension-table-header": "2.11.3",
|
||||
"@tiptap/extension-table-row": "2.11.3",
|
||||
"@tiptap/extension-task-item": "2.11.3",
|
||||
"@tiptap/extension-task-list": "2.11.3",
|
||||
"@tiptap/extension-text-align": "2.11.3",
|
||||
"@tiptap/extension-text-style": "2.11.3",
|
||||
"@tiptap/extension-underline": "2.11.3",
|
||||
"@tiptap/react": "^2.11.3",
|
||||
"@tiptap/starter-kit": "^2.11.3",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"mantine-react-table": "2.0.0-beta.8",
|
||||
"next": "15.1.5",
|
||||
"next": "15.1.6",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"video.js": "^8.21.0"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useParams } from "next/navigation";
|
||||
import { Calendar } from "@mantine/dates";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { CalendarEvent } from "@homarr/integrations/types";
|
||||
|
||||
@@ -12,8 +13,22 @@ import type { WidgetComponentProps } from "../definition";
|
||||
import { CalendarDay } from "./calender-day";
|
||||
import classes from "./component.module.css";
|
||||
|
||||
export default function CalendarWidget({ isEditMode, integrationIds, options }: WidgetComponentProps<"calendar">) {
|
||||
export default function CalendarWidget(props: WidgetComponentProps<"calendar">) {
|
||||
const [month, setMonth] = useState(new Date());
|
||||
|
||||
if (props.integrationIds.length === 0) {
|
||||
return <CalendarBase {...props} events={[]} month={month} setMonth={setMonth} />;
|
||||
}
|
||||
|
||||
return <FetchCalendar month={month} setMonth={setMonth} {...props} />;
|
||||
}
|
||||
|
||||
interface FetchCalendarProps extends WidgetComponentProps<"calendar"> {
|
||||
month: Date;
|
||||
setMonth: (date: Date) => void;
|
||||
}
|
||||
|
||||
const FetchCalendar = ({ month, setMonth, isEditMode, integrationIds, options }: FetchCalendarProps) => {
|
||||
const [events] = clientApi.widget.calendar.findAllEvents.useSuspenseQuery(
|
||||
{
|
||||
integrationIds,
|
||||
@@ -28,6 +43,19 @@ export default function CalendarWidget({ isEditMode, integrationIds, options }:
|
||||
retry: false,
|
||||
},
|
||||
);
|
||||
|
||||
return <CalendarBase isEditMode={isEditMode} events={events} month={month} setMonth={setMonth} options={options} />;
|
||||
};
|
||||
|
||||
interface CalendarBaseProps {
|
||||
isEditMode: boolean;
|
||||
events: RouterOutputs["widget"]["calendar"]["findAllEvents"];
|
||||
month: Date;
|
||||
setMonth: (date: Date) => void;
|
||||
options: WidgetComponentProps<"calendar">["options"];
|
||||
}
|
||||
|
||||
const CalendarBase = ({ isEditMode, events, month, setMonth, options }: CalendarBaseProps) => {
|
||||
const params = useParams();
|
||||
const locale = params.locale as string;
|
||||
const [firstDayOfWeek] = clientApi.user.getFirstDayOfWeekForUserOrDefault.useSuspenseQuery();
|
||||
@@ -37,6 +65,7 @@ export default function CalendarWidget({ isEditMode, integrationIds, options }:
|
||||
defaultDate={new Date()}
|
||||
onPreviousMonth={setMonth}
|
||||
onNextMonth={setMonth}
|
||||
highlightToday
|
||||
locale={locale}
|
||||
hideWeekdays={false}
|
||||
date={month}
|
||||
@@ -95,4 +124,4 @@ export default function CalendarWidget({ isEditMode, integrationIds, options }:
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -27,4 +27,5 @@ export const { definition, componentLoader } = createWidgetDefinition("calendar"
|
||||
}),
|
||||
})),
|
||||
supportedIntegrations: getIntegrationKindsByCategory("calendar"),
|
||||
integrationsRequired: false,
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -29,6 +29,7 @@ export const createWidgetDefinition = <TKind extends WidgetKind, TDefinition ext
|
||||
export interface WidgetDefinition {
|
||||
icon: TablerIcon;
|
||||
supportedIntegrations?: IntegrationKind[];
|
||||
integrationsRequired?: boolean;
|
||||
options: WidgetOptionsRecord;
|
||||
errors?: Partial<
|
||||
Record<
|
||||
|
||||
@@ -181,9 +181,7 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
|
||||
</Modal>
|
||||
</Box>
|
||||
{options.cpu && <CpuRing cpuUtilization={healthInfo.cpuUtilization} />}
|
||||
{healthInfo.cpuTemp && options.cpu && (
|
||||
<CpuTempRing fahrenheit={options.fahrenheit} cpuTemp={healthInfo.cpuTemp} />
|
||||
)}
|
||||
{options.cpu && <CpuTempRing fahrenheit={options.fahrenheit} cpuTemp={healthInfo.cpuTemp} />}
|
||||
{options.memory && <MemoryRing available={healthInfo.memAvailable} used={healthInfo.memUsed} />}
|
||||
</Flex>
|
||||
{
|
||||
@@ -225,7 +223,7 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
|
||||
<Group gap="1cqmin">
|
||||
<IconFileReport className="health-monitoring-disk-status-icon" size="5cqmin" />
|
||||
<Text className="health-monitoring-disk-status-value" size="4cqmin">
|
||||
{disk.overallStatus}
|
||||
{disk.overallStatus ? disk.overallStatus : "N/A"}
|
||||
</Text>
|
||||
</Group>
|
||||
</Flex>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user