refactor: consolidate English translations (#6640)
This commit is contained in:
1 parent
81aa704537
commit
9e14a07e64
449 files changed
+3515
-3922
No files matched your search
@@ -1,7 +1,11 @@
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetOptionOfType, WidgetOptionType } from "../options";
|
||||
import {
|
||||
getWidgetOptionDescriptionTranslationNamespace,
|
||||
getWidgetOptionTranslationNamespace,
|
||||
} from "../option-translation";
|
||||
|
||||
export interface CommonWidgetInputProps<TKey extends WidgetOptionType> {
|
||||
kind: WidgetKind;
|
||||
@@ -25,7 +29,17 @@ type UseWidgetInputTranslationReturnType = (key: "label" | "description" | "plac
|
||||
* is defined for the option. The method does sadly not reconize issues with those definitions. So it does not yell at you when you somewhere show the label without having it defined in the translations.
|
||||
*/
|
||||
export const useWidgetInputTranslation = (kind: WidgetKind, property: string): UseWidgetInputTranslationReturnType => {
|
||||
return useScopedI18n(
|
||||
`widget.${kind}.option.${property}` as never, // Because the type is complex and not recognized by typescript, we need to cast it to never to make it work.
|
||||
const inputNamespace = getWidgetOptionTranslationNamespace(kind, property);
|
||||
const descriptionNamespace = getWidgetOptionDescriptionTranslationNamespace(kind, property);
|
||||
const tInput = useI18n(
|
||||
inputNamespace as never, // Because the type is complex and not recognized by typescript, we need to cast it to never to make it work.
|
||||
) as unknown as UseWidgetInputTranslationReturnType;
|
||||
const tDescription = useI18n(
|
||||
(descriptionNamespace ?? inputNamespace) as never,
|
||||
) as unknown as UseWidgetInputTranslationReturnType;
|
||||
|
||||
return (key) => {
|
||||
if (key === "description" && descriptionNamespace) return tDescription("description");
|
||||
return tInput(key);
|
||||
};
|
||||
};
|
||||
@@ -5,16 +5,16 @@ import { Loader, Select } from "@mantine/core";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
|
||||
export const WidgetAnchorNoteInput = ({ property, kind, options }: CommonWidgetInputProps<"anchorNote">) => {
|
||||
const t = useI18n();
|
||||
const tCommon = useI18n("common");
|
||||
const tWidget = useWidgetInputTranslation(kind, property);
|
||||
const tAnchor = useScopedI18n("widget.anchorNote");
|
||||
const tAnchor = useI18n("widget.anchorNote");
|
||||
const form = useFormContext();
|
||||
|
||||
const integrationId = form.values.integrationIds[0];
|
||||
@@ -46,7 +46,7 @@ export const WidgetAnchorNoteInput = ({ property, kind, options }: CommonWidgetI
|
||||
<Select
|
||||
label={tWidget("label")}
|
||||
description={options.withDescription ? tWidget("description") : undefined}
|
||||
placeholder={integrationId ? t("common.select.placeholder") : tAnchor("integrationRequired")}
|
||||
placeholder={integrationId ? tCommon("select.placeholder") : tAnchor("integrationRequired")}
|
||||
nothingFoundMessage={integrationId ? tAnchor("noNotes") : tAnchor("integrationRequired")}
|
||||
searchable
|
||||
searchValue={search}
|
||||
|
||||
@@ -24,7 +24,8 @@ const renderHereLink = (children: React.ReactNode) => (
|
||||
);
|
||||
|
||||
export const WidgetAppInput = ({ property, kind }: CommonWidgetInputProps<"app">) => {
|
||||
const t = useI18n();
|
||||
const tCommon = useI18n("common");
|
||||
const tApp = useI18n("widget.common.app");
|
||||
const tInput = useWidgetInputTranslation(kind, property);
|
||||
const form = useFormContext();
|
||||
const { data: apps, isPending, refetch } = clientApi.app.selectable.useQuery();
|
||||
@@ -44,7 +45,7 @@ export const WidgetAppInput = ({ property, kind }: CommonWidgetInputProps<"app">
|
||||
label={tInput("label")}
|
||||
searchable
|
||||
leftSection={<MemoizedLeftSection isPending={isPending} currentApp={currentApp} />}
|
||||
nothingFoundMessage={t("widget.common.app.noData")}
|
||||
nothingFoundMessage={tApp("noData")}
|
||||
renderOption={renderSelectOption}
|
||||
data={
|
||||
apps?.map((app) => ({
|
||||
@@ -56,8 +57,8 @@ export const WidgetAppInput = ({ property, kind }: CommonWidgetInputProps<"app">
|
||||
inputWrapperOrder={["label", "input", "description", "error"]}
|
||||
description={
|
||||
<Text component="span" size="xs">
|
||||
{t.rich("widget.common.app.description", {
|
||||
here: renderHereLink,
|
||||
{tApp.rich("description", {
|
||||
here: () => renderHereLink(tCommon("here")),
|
||||
})}
|
||||
</Text>
|
||||
}
|
||||
@@ -79,7 +80,7 @@ export const WidgetAppInput = ({ property, kind }: CommonWidgetInputProps<"app">
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("widget.common.app.quickCreate")}
|
||||
{tApp("quickCreate")}
|
||||
</Button>
|
||||
)}
|
||||
</SimpleGrid>
|
||||
|
||||
@@ -25,7 +25,7 @@ import { normalizeCustomWidgetOptions, validateCustomWidgetOptions } from "@homa
|
||||
import { CustomWidgetCodeEditor } from "@homarr/custom-widgets/workbench";
|
||||
import type { CustomWidgetEditorMessages } from "@homarr/custom-widgets/workbench";
|
||||
import { IconPicker } from "@homarr/forms-collection";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
@@ -34,7 +34,7 @@ export const WidgetCustomWidgetConfigurationInput = ({
|
||||
property,
|
||||
}: CommonWidgetInputProps<"customWidgetConfiguration">) => {
|
||||
const form = useFormContext();
|
||||
const labels = useScopedI18n("widget.customApi.configuration");
|
||||
const labels = useI18n("widget.customApi.configuration");
|
||||
const board = useOptionalBoard();
|
||||
const definitionId = typeof form.values.options.definitionId === "string" ? form.values.options.definitionId : "";
|
||||
const configurationValue = form.values.options[property];
|
||||
|
||||
@@ -72,7 +72,7 @@ vi.mock("@homarr/boards/context", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/translation/client", () => ({
|
||||
useScopedI18n: () => (key: string) =>
|
||||
useI18n: () => (key: string) =>
|
||||
({
|
||||
migrationRequired: "Migration required",
|
||||
migrationDescription: "Copy its redacted prompt to an LLM, then paste the single JSON response into Homarr.",
|
||||
|
||||
@@ -19,7 +19,7 @@ import { IconAlertTriangle, IconApi } from "@tabler/icons-react";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { useOptionalBoard } from "@homarr/boards/context";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
@@ -32,7 +32,7 @@ export const WidgetCustomWidgetSelectInput = ({
|
||||
options,
|
||||
}: CommonWidgetInputProps<"customWidgetSelect">) => {
|
||||
const t = useWidgetInputTranslation(kind, property);
|
||||
const labels = useScopedI18n("widget.customApi.picker");
|
||||
const labels = useI18n("widget.customApi.picker");
|
||||
const { data: session } = useSession();
|
||||
const isAdmin = session?.user.permissions.includes("admin") ?? false;
|
||||
const form = useFormContext();
|
||||
|
||||
@@ -24,7 +24,7 @@ import dayjs from "dayjs";
|
||||
|
||||
import { createId } from "@homarr/common";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { formatWallDateTime, resolveWallDateTimeString } from "../countdown/recurrence";
|
||||
import type { DateTimeEventRecurrence, OptionDateTimeEvent } from "../options";
|
||||
@@ -314,4 +314,4 @@ type EventListTranslationKey =
|
||||
type EventListTranslation = (key: EventListTranslationKey, values?: Record<string, string | number>) => string;
|
||||
|
||||
const useEventListTranslation = (kind: WidgetKind) =>
|
||||
useScopedI18n(`widget.${kind}.dateTimeEventList` as never) as unknown as EventListTranslation;
|
||||
useI18n(`widget.${kind}.dateTimeEventList` as never) as unknown as EventListTranslation;
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Loader, MultiSelect } from "@mantine/core";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
@@ -13,7 +13,8 @@ export const WidgetDynamicMultiSelectInput = ({
|
||||
kind,
|
||||
options,
|
||||
}: CommonWidgetInputProps<"dynamicMultiSelect">) => {
|
||||
const t = useScopedI18n("widget.dynamicSelect");
|
||||
const t = useI18n("widget.common.select");
|
||||
const tCommon = useI18n("common");
|
||||
const tInput = useWidgetInputTranslation(kind, property);
|
||||
const form = useFormContext();
|
||||
const inputProps = form.getInputProps(`options.${property}`);
|
||||
@@ -36,7 +37,7 @@ export const WidgetDynamicMultiSelectInput = ({
|
||||
searchable
|
||||
clearable
|
||||
maxValues={options.maxValues}
|
||||
nothingFoundMessage={t("noResults")}
|
||||
nothingFoundMessage={tCommon("noResults")}
|
||||
data={data}
|
||||
disabled={isError}
|
||||
error={isError ? t("loadError") : inputProps.error}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
|
||||
import { translateIfNecessary } from "@homarr/translation";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
@@ -25,7 +25,7 @@ export const WidgetDynamicSelectInput = ({
|
||||
boardId,
|
||||
}: CommonWidgetInputProps<"dynamicSelect">) => {
|
||||
const t = useI18n();
|
||||
const tSelect = useScopedI18n("widget.dynamicSelect");
|
||||
const tCommon = useI18n("common");
|
||||
const tWidget = useWidgetInputTranslation(kind, property);
|
||||
const form = useFormContext();
|
||||
const inputProps = form.getInputProps(`options.${property}`);
|
||||
@@ -78,7 +78,7 @@ export const WidgetDynamicSelectInput = ({
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
placeholder={tWidget("placeholder")}
|
||||
nothingFoundMessage={tSelect("noResults")}
|
||||
nothingFoundMessage={tCommon("noResults")}
|
||||
leftSection={isPending && <Loader size="xs" />}
|
||||
renderOption={({ option, checked }) => {
|
||||
return (
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
|
||||
import { MultiSelect, Text } from "@mantine/core";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
|
||||
const useIntegrationSelectTranslation = () => useScopedI18n("widget.integrationSelect");
|
||||
|
||||
export const WidgetIntegrationMultiSelectInput = ({
|
||||
property,
|
||||
kind,
|
||||
options,
|
||||
}: CommonWidgetInputProps<"integrationMultiSelect">) => {
|
||||
const t = useIntegrationSelectTranslation();
|
||||
const t = useI18n("widget.integrationSelect");
|
||||
const tSelect = useI18n("widget.common.select");
|
||||
const tCommon = useI18n("common");
|
||||
const tInput = useWidgetInputTranslation(kind, property);
|
||||
const form = useFormContext();
|
||||
|
||||
@@ -40,8 +40,8 @@ export const WidgetIntegrationMultiSelectInput = ({
|
||||
}
|
||||
|
||||
const description = options.withDescription ? tInput("description") : undefined;
|
||||
const placeholder = isPending ? t("loading") : tInput("placeholder");
|
||||
const loadError = isError ? t("loadError") : undefined;
|
||||
const placeholder = isPending ? tSelect("loading") : tInput("placeholder");
|
||||
const loadError = isError ? tSelect("loadError") : undefined;
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
@@ -51,7 +51,7 @@ export const WidgetIntegrationMultiSelectInput = ({
|
||||
placeholder={placeholder}
|
||||
searchable
|
||||
clearable
|
||||
nothingFoundMessage={t("noResults")}
|
||||
nothingFoundMessage={tCommon("noResults")}
|
||||
data={data}
|
||||
disabled={isError}
|
||||
error={loadError ?? inputProps.error}
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
|
||||
import { Select, Text } from "@mantine/core";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
|
||||
const useIntegrationSelectTranslation = () => useScopedI18n("widget.integrationSelect");
|
||||
|
||||
export const WidgetIntegrationSelectInput = ({
|
||||
property,
|
||||
kind,
|
||||
options,
|
||||
}: CommonWidgetInputProps<"integrationSelect">) => {
|
||||
const t = useIntegrationSelectTranslation();
|
||||
const t = useI18n("widget.integrationSelect");
|
||||
const tSelect = useI18n("widget.common.select");
|
||||
const tCommon = useI18n("common");
|
||||
const tInput = useWidgetInputTranslation(kind, property);
|
||||
const form = useFormContext();
|
||||
|
||||
@@ -35,8 +35,8 @@ export const WidgetIntegrationSelectInput = ({
|
||||
}
|
||||
|
||||
const description = options.withDescription ? tInput("description") : undefined;
|
||||
const placeholder = isPending ? t("loading") : t("placeholder");
|
||||
const error = isError ? t("loadError") : undefined;
|
||||
const placeholder = isPending ? tSelect("loading") : t("placeholder");
|
||||
const error = isError ? tSelect("loadError") : undefined;
|
||||
|
||||
return (
|
||||
<Select
|
||||
@@ -45,7 +45,7 @@ export const WidgetIntegrationSelectInput = ({
|
||||
placeholder={placeholder}
|
||||
clearable={options.clearable}
|
||||
searchable={options.searchable}
|
||||
nothingFoundMessage={t("noResults")}
|
||||
nothingFoundMessage={tCommon("noResults")}
|
||||
data={data}
|
||||
disabled={isError}
|
||||
error={error}
|
||||
|
||||
@@ -46,7 +46,7 @@ vi.mock("@homarr/modals", () => ({
|
||||
|
||||
vi.mock("@homarr/translation/client", () => ({
|
||||
useCurrentIntlLocale: () => "en",
|
||||
useScopedI18n: () => (key: string) => key,
|
||||
useI18n: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
vi.mock("./common", async (importOriginal) => ({
|
||||
|
||||
@@ -21,7 +21,7 @@ import { IconClick, IconListSearch } from "@tabler/icons-react";
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { createModal, useModalAction } from "@homarr/modals";
|
||||
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { formatLocalizedCompactNumber } from "../common/locale";
|
||||
import type { OptionLocation } from "../options";
|
||||
@@ -31,7 +31,7 @@ import { useFormContext } from "./form";
|
||||
|
||||
export const WidgetLocationInput = ({ property, kind, options }: CommonWidgetInputProps<"location">) => {
|
||||
const t = useWidgetInputTranslation(kind, property);
|
||||
const tLocation = useScopedI18n("widget.common.location");
|
||||
const tLocation = useI18n("widget.common.location");
|
||||
const form = useFormContext();
|
||||
const { openModal } = useModalAction(LocationSearchModal);
|
||||
const fieldPath = `options.${property}`;
|
||||
@@ -149,8 +149,8 @@ interface LocationSearchInnerProps {
|
||||
}
|
||||
|
||||
const LocationSearchModal = createModal<LocationSearchInnerProps>(({ actions, innerProps }) => {
|
||||
const t = useScopedI18n("widget.common.location.table");
|
||||
const tCommon = useScopedI18n("common");
|
||||
const t = useI18n("widget.common.location.table");
|
||||
const tCommon = useI18n("common");
|
||||
const { data, isPending, error } = clientApi.location.searchCity.useQuery({
|
||||
query: innerProps.query,
|
||||
});
|
||||
@@ -216,7 +216,7 @@ interface LocationSearchTableRowProps {
|
||||
}
|
||||
|
||||
const LocationSelectTableRow = ({ city, onLocationSelect, closeModal }: LocationSearchTableRowProps) => {
|
||||
const t = useScopedI18n("widget.common.location.table");
|
||||
const t = useI18n("widget.common.location.table");
|
||||
const locale = useCurrentIntlLocale();
|
||||
const onSelect = useCallback(() => {
|
||||
onLocationSelect({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Combobox, Pill, PillsInput, useCombobox } from "@mantine/core";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
@@ -9,7 +9,7 @@ import { useFormContext } from "./form";
|
||||
|
||||
export const WidgetMultiTextInput = ({ property, kind, options }: CommonWidgetInputProps<"multiText">) => {
|
||||
const t = useWidgetInputTranslation(kind, property);
|
||||
const tCommon = useScopedI18n("common");
|
||||
const tCommon = useI18n("common");
|
||||
const combobox = useCombobox({
|
||||
onDropdownClose: () => combobox.resetSelectedOption(),
|
||||
onDropdownOpen: () => combobox.updateSelectedOptionIndex("active"),
|
||||
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
import { IconPicker } from "@homarr/forms-collection";
|
||||
import { createModal, useModalAction } from "@homarr/modals";
|
||||
import { showErrorNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { MaskedImage } from "@homarr/ui";
|
||||
|
||||
import type { ReleasesRepository, ReleasesVersionFilter } from "../releases/releases-repository";
|
||||
@@ -69,7 +69,8 @@ export const WidgetMultiReleasesRepositoriesInput = ({
|
||||
itemId,
|
||||
}: CommonWidgetInputProps<"multiReleasesRepositories">) => {
|
||||
const t = useWidgetInputTranslation(kind, property);
|
||||
const tRepository = useScopedI18n("widget.releases.option.repositories");
|
||||
const tRepository = useI18n("widget.releases.option.repositories");
|
||||
const actionT = useI18n("common.action");
|
||||
const form = useFormContext();
|
||||
const repositories = form.values.options[property] as ReleasesRepository[];
|
||||
const { openModal: openEditModal } = useModalAction(RepositoryEditModal);
|
||||
@@ -232,7 +233,7 @@ export const WidgetMultiReleasesRepositoriesInput = ({
|
||||
leftSection={<IconEdit size="var(--mantine-font-size-sm)" />}
|
||||
size="xs"
|
||||
>
|
||||
{tRepository("edit.label")}
|
||||
{actionT("edit")}
|
||||
</Button>
|
||||
|
||||
<ActionIcon variant="transparent" color="red" onClick={() => onRepositoryRemove(index)}>
|
||||
@@ -267,7 +268,7 @@ const providersWithAuth: ReleaseProviderKind[] = [
|
||||
];
|
||||
|
||||
const ProviderTokensSection = ({ itemId, repositories }: { itemId: string; repositories: ReleasesRepository[] }) => {
|
||||
const tRepository = useScopedI18n("widget.releases.option.repositories");
|
||||
const tRepository = useI18n("widget.releases.option.repositories");
|
||||
const { data: configuredKinds = [], refetch } = clientApi.widget.secrets.getConfiguredKinds.useQuery({ itemId });
|
||||
const setSecret = clientApi.widget.secrets.setSecret.useMutation({
|
||||
onSuccess: () => refetch(),
|
||||
@@ -331,7 +332,8 @@ const ProviderTokenInput = ({
|
||||
onSave: (value: string) => Promise<void>;
|
||||
onDelete: () => Promise<void>;
|
||||
}) => {
|
||||
const tRepository = useScopedI18n("widget.releases.option.repositories");
|
||||
const tRepository = useI18n("widget.releases.option.repositories");
|
||||
const actionT = useI18n("common.action");
|
||||
const [value, setValue] = useState("");
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -375,7 +377,7 @@ const ProviderTokenInput = ({
|
||||
/>
|
||||
{editing && value.trim() && (
|
||||
<Button size="xs" onClick={handleSave} loading={saving}>
|
||||
{tRepository("tokens.save")}
|
||||
{actionT("save")}
|
||||
</Button>
|
||||
)}
|
||||
{hasToken && !editing && (
|
||||
@@ -412,7 +414,8 @@ interface RepositoryEditProps {
|
||||
}
|
||||
|
||||
const RepositoryEditModal = createModal<RepositoryEditProps>(({ innerProps, actions }) => {
|
||||
const tRepository = useScopedI18n("widget.releases.option.repositories");
|
||||
const tRepository = useI18n("widget.releases.option.repositories");
|
||||
const actionT = useI18n("common.action");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tempRepository, setTempRepository] = useState(() => ({ ...innerProps.repository }));
|
||||
const [formErrors, setFormErrors] = useState<FormErrors>({});
|
||||
@@ -576,11 +579,11 @@ const RepositoryEditModal = createModal<RepositoryEditProps>(({ innerProps, acti
|
||||
<Divider my={"sm"} />
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={handleCancel} color="gray.5">
|
||||
{tRepository("editForm.cancel.label")}
|
||||
{actionT("cancel")}
|
||||
</Button>
|
||||
|
||||
<Button data-autofocus onClick={handleConfirm} loading={loading}>
|
||||
{tRepository("editForm.confirm.label")}
|
||||
{actionT("confirm")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
@@ -611,7 +614,7 @@ const ImportRepositorySelect = ({
|
||||
disabled = false,
|
||||
onImageSelectionChanged = undefined,
|
||||
}: ImportRepositorySelectProps) => {
|
||||
const tRepository = useScopedI18n("widget.releases.option.repositories");
|
||||
const tRepository = useI18n("widget.releases.option.repositories");
|
||||
const provider = repository.provider;
|
||||
|
||||
return (
|
||||
@@ -687,7 +690,8 @@ interface RepositoryImportProps {
|
||||
}
|
||||
|
||||
const RepositoryImportModal = createModal<RepositoryImportProps>(({ innerProps, actions }) => {
|
||||
const tRepository = useScopedI18n("widget.releases.option.repositories");
|
||||
const tRepository = useI18n("widget.releases.option.repositories");
|
||||
const actionT = useI18n("common.action");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedImages, setSelectedImages] = useState([] as ReleasesRepositoryImport[]);
|
||||
|
||||
@@ -847,11 +851,11 @@ const RepositoryImportModal = createModal<RepositoryImportProps>(({ innerProps,
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={actions.closeModal} color="gray.5">
|
||||
{tRepository("editForm.cancel.label")}
|
||||
{actionT("cancel")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={handleConfirm} loading={loading} disabled={selectedImages.length === 0}>
|
||||
{tRepository("editForm.confirm.label")}
|
||||
{actionT("confirm")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
@@ -22,7 +22,7 @@ import { IconGripVertical, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||
|
||||
import { createId } from "@homarr/common";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { OptionTimezone } from "../options";
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
@@ -246,4 +246,4 @@ type TimezoneListTranslationKey =
|
||||
type TimezoneListTranslation = (key: TimezoneListTranslationKey, values?: Record<string, string | number>) => string;
|
||||
|
||||
const useTimezoneListTranslation = (kind: WidgetKind) =>
|
||||
useScopedI18n(`widget.${kind}.timezoneList` as never) as unknown as TimezoneListTranslation;
|
||||
useI18n(`widget.${kind}.timezoneList` as never) as unknown as TimezoneListTranslation;
|
||||
@@ -3,14 +3,15 @@
|
||||
import { Select, Text } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
|
||||
export const WidgetUmamiEventInput = ({ property, kind }: CommonWidgetInputProps<"umamiEventName">) => {
|
||||
const t = useScopedI18n("widget.umami.option.eventName");
|
||||
const t = useI18n("widget.umami.option.eventName");
|
||||
const tEvents = useI18n("widget.umami.option.events");
|
||||
const tInput = useWidgetInputTranslation(kind, property);
|
||||
const form = useFormContext();
|
||||
|
||||
@@ -25,7 +26,7 @@ export const WidgetUmamiEventInput = ({ property, kind }: CommonWidgetInputProps
|
||||
if (!websiteId || integrationIds.length === 0) {
|
||||
return (
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("configureWebsiteFirst")}
|
||||
{tEvents("configureWebsiteFirst")}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
@@ -36,9 +37,9 @@ export const WidgetUmamiEventInput = ({ property, kind }: CommonWidgetInputProps
|
||||
<Select
|
||||
label={tInput("label")}
|
||||
description={tInput("description")}
|
||||
placeholder={isPending ? t("loading") : undefined}
|
||||
placeholder={isPending ? tEvents("loading") : undefined}
|
||||
searchable
|
||||
nothingFoundMessage={t("noEvents")}
|
||||
nothingFoundMessage={tEvents("noEvents")}
|
||||
data={data}
|
||||
{...form.getInputProps(`options.${property}`)}
|
||||
/>
|
||||
|
||||
@@ -3,14 +3,15 @@
|
||||
import { MultiSelect, Text } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
|
||||
export const WidgetUmamiEventNamesInput = ({ property, kind }: CommonWidgetInputProps<"umamiEventNames">) => {
|
||||
const t = useScopedI18n("widget.umami.option.eventNames");
|
||||
const t = useI18n("widget.umami.option.eventNames");
|
||||
const tEvents = useI18n("widget.umami.option.events");
|
||||
const tInput = useWidgetInputTranslation(kind, property);
|
||||
const form = useFormContext();
|
||||
|
||||
@@ -25,7 +26,7 @@ export const WidgetUmamiEventNamesInput = ({ property, kind }: CommonWidgetInput
|
||||
if (!websiteId || integrationIds.length === 0) {
|
||||
return (
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("configureWebsiteFirst")}
|
||||
{tEvents("configureWebsiteFirst")}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
@@ -34,9 +35,9 @@ export const WidgetUmamiEventNamesInput = ({ property, kind }: CommonWidgetInput
|
||||
<MultiSelect
|
||||
label={tInput("label")}
|
||||
description={tInput("description")}
|
||||
placeholder={isPending ? t("loading") : t("placeholder")}
|
||||
placeholder={isPending ? tEvents("loading") : t("placeholder")}
|
||||
searchable
|
||||
nothingFoundMessage={t("noEvents")}
|
||||
nothingFoundMessage={tEvents("noEvents")}
|
||||
data={eventNames ?? []}
|
||||
{...form.getInputProps(`options.${property}`)}
|
||||
/>
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
import { Select, Text } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
|
||||
export const WidgetUmamiWebsiteInput = ({ property, kind }: CommonWidgetInputProps<"umamiWebsite">) => {
|
||||
const t = useScopedI18n("widget.umami.option.websiteId");
|
||||
const t = useI18n("widget.umami.option.websiteId");
|
||||
const tInput = useWidgetInputTranslation(kind, property);
|
||||
const form = useFormContext();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Box, Group, Paper, ScrollArea, SimpleGrid, Stack, Text, VisuallyHidden
|
||||
import { IconMapPin } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { formatLocalizedDate, formatLocalizedTime } from "../common/locale";
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
} from "./display";
|
||||
|
||||
export default function AirQualityWidget({ options, width, height, displayMode }: WidgetComponentProps<"airQuality">) {
|
||||
const t = useScopedI18n("widget.airQuality");
|
||||
const t = useI18n("widget.airQuality");
|
||||
const query = clientApi.widget.airQuality.atLocation.useQuery({
|
||||
latitude: options.location.latitude,
|
||||
longitude: options.location.longitude,
|
||||
@@ -66,7 +66,7 @@ const CompactAirQuality = ({
|
||||
width,
|
||||
}: AirQualityViewProps & { height: number; width: number }) => {
|
||||
const locale = useCurrentIntlLocale();
|
||||
const t = useScopedI18n("widget.airQuality");
|
||||
const t = useI18n("widget.airQuality");
|
||||
const layout = getCompactAirQualityLayout(width, height);
|
||||
const standard = getAqiStandard(options.aqiStandard, locale);
|
||||
const value = getAqiValue(airQuality.current, standard);
|
||||
@@ -158,7 +158,7 @@ const CompactAirQuality = ({
|
||||
const AdvancedAirQuality = ({ airQuality, options, width }: AirQualityViewProps & { width: number }) => {
|
||||
const chartId = useId();
|
||||
const locale = useCurrentIntlLocale();
|
||||
const t = useScopedI18n("widget.airQuality");
|
||||
const t = useI18n("widget.airQuality");
|
||||
const standard = getAqiStandard(options.aqiStandard, locale);
|
||||
const value = getAqiValue(airQuality.current, standard);
|
||||
const category = getAqiCategory(value, standard);
|
||||
@@ -358,7 +358,7 @@ const pollutantKeys: PollutantKey[] = ["pm2_5", "pm10", "ozone", "nitrogenDioxid
|
||||
const pollenKeys: PollenKey[] = ["alder", "birch", "grass", "mugwort", "olive", "ragweed"];
|
||||
|
||||
const useAirQualityLabels = () => {
|
||||
const t = useScopedI18n("widget.airQuality");
|
||||
const t = useI18n("widget.airQuality");
|
||||
return {
|
||||
pollutants: {
|
||||
pm2_5: t("pollutant.pm2_5"),
|
||||
@@ -389,7 +389,7 @@ const formatDecimal = (value: number | null, locale: string) => {
|
||||
return new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value);
|
||||
};
|
||||
|
||||
type ScopedAirQualityTranslator = ReturnType<typeof useScopedI18n<"widget.airQuality">>;
|
||||
type ScopedAirQualityTranslator = ReturnType<typeof useI18n<"widget.airQuality">>;
|
||||
|
||||
const formatConcentration = (value: number | null, locale: string, t: ScopedAirQualityTranslator) =>
|
||||
t("unit.concentration", { value: formatDecimal(value, locale) });
|
||||
|
||||
@@ -9,7 +9,7 @@ import { clientApi } from "@homarr/api/client";
|
||||
import { useIntegrationsWithInteractAccess } from "@homarr/auth/client";
|
||||
import { useTimeAgo } from "@homarr/common";
|
||||
import type { AnchorNotePermission } from "@homarr/integrations";
|
||||
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
@@ -42,7 +42,7 @@ export default function AnchorNoteWidget({
|
||||
height,
|
||||
displayMode,
|
||||
}: WidgetComponentProps<"anchorNote">) {
|
||||
const t = useScopedI18n("widget.anchorNote");
|
||||
const t = useI18n("widget.anchorNote");
|
||||
const noteId = options.noteId.trim();
|
||||
if (!noteId) {
|
||||
return (
|
||||
@@ -85,7 +85,9 @@ const AnchorNoteWidgetContent = ({
|
||||
height,
|
||||
isAdvanced,
|
||||
}: AnchorNoteWidgetContentProps) => {
|
||||
const t = useScopedI18n("widget.anchorNote");
|
||||
const t = useI18n("widget.anchorNote");
|
||||
const tWidgetCommon = useI18n("widget.common");
|
||||
const actionT = useI18n("common.action");
|
||||
const locale = useCurrentIntlLocale();
|
||||
const noteQuery = clientApi.widget.anchorNotes.getNote.useQuery({
|
||||
integrationId,
|
||||
@@ -206,7 +208,7 @@ const AnchorNoteWidgetContent = ({
|
||||
)}
|
||||
{!isEditing && (isAdvanced || options.showUpdatedAt) && (
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("updatedAt", { date: updatedAtRelative })}
|
||||
{tWidgetCommon("updatedAt", { date: updatedAtRelative })}
|
||||
</Text>
|
||||
)}
|
||||
{!isEditing && isViewer && (
|
||||
@@ -243,21 +245,21 @@ const AnchorNoteWidgetContent = ({
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Button size="xs" onClick={handleSave} loading={isUpdating} disabled={!hasChanges || !canEdit}>
|
||||
{t("save")}
|
||||
{actionT("save")}
|
||||
</Button>
|
||||
<Button size="xs" variant="subtle" onClick={handleCancel} disabled={isUpdating}>
|
||||
{t("cancel")}
|
||||
{actionT("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
) : isAdvanced ? (
|
||||
<Button size="xs" variant="light" onClick={handleEdit} disabled={isUpdating}>
|
||||
{t("edit")}
|
||||
{actionT("edit")}
|
||||
</Button>
|
||||
) : (
|
||||
<Tooltip label={t("edit")}>
|
||||
<Tooltip label={actionT("edit")}>
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
aria-label={t("edit")}
|
||||
aria-label={actionT("edit")}
|
||||
size="md"
|
||||
variant="light"
|
||||
onClick={handleEdit}
|
||||
|
||||
@@ -23,7 +23,8 @@ import { PingDot } from "./ping/ping-dot";
|
||||
import { PingIndicator } from "./ping/ping-indicator";
|
||||
|
||||
export default function AppWidget({ options, isEditMode, height, width }: WidgetComponentProps<"app">) {
|
||||
const t = useI18n();
|
||||
const tCommon = useI18n("common");
|
||||
const tApp = useI18n("widget.app");
|
||||
const settings = useSettings();
|
||||
const board = useRequiredBoard();
|
||||
const appQuery = clientApi.app.byId.useQuery({ id: options.appId }, { enabled: Boolean(options.appId) });
|
||||
@@ -121,14 +122,14 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
|
||||
!settings.forceDisableStatus &&
|
||||
!board.disableStatus &&
|
||||
Boolean(app.pingUrl ?? app.href) ? (
|
||||
<Suspense fallback={<PingDot icon={IconMinus} color="gray" tooltip={`${t("common.action.loading")}…`} />}>
|
||||
<Suspense fallback={<PingDot icon={IconMinus} color="gray" tooltip={`${tCommon("action.loading")}…`} />}>
|
||||
<PingIndicator appId={app.id} />
|
||||
</Suspense>
|
||||
) : null}
|
||||
</AppLink>
|
||||
{appQuery.error && (
|
||||
<Box pos="absolute" top={4} right={4}>
|
||||
<WidgetQueryErrorIndicator error={appQuery.error} label={t("widget.app.name")} />
|
||||
<WidgetQueryErrorIndicator error={appQuery.error} label={tApp("name")} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -55,7 +55,7 @@ export const { definition, componentLoader } = createWidgetDefinition("app", {
|
||||
options: [
|
||||
{
|
||||
label(t) {
|
||||
return t("widget.app.option.layout.option.column");
|
||||
return t("widget.common.layout.option.vertical");
|
||||
},
|
||||
value: "column",
|
||||
icon: IconLayoutNavbarExpand,
|
||||
@@ -69,7 +69,7 @@ export const { definition, componentLoader } = createWidgetDefinition("app", {
|
||||
},
|
||||
{
|
||||
label(t) {
|
||||
return t("widget.app.option.layout.option.row");
|
||||
return t("widget.common.layout.option.horizontal");
|
||||
},
|
||||
value: "row",
|
||||
icon: IconLayoutSidebarLeftExpand,
|
||||
|
||||
@@ -10,12 +10,12 @@ interface PingIndicatorProps {
|
||||
}
|
||||
|
||||
export const PingIndicator = ({ appId }: PingIndicatorProps) => {
|
||||
const t = useI18n();
|
||||
const t = useI18n("common");
|
||||
const { data: pingResult, error } = clientApi.widget.app.ping.useQuery({ id: appId }, { refetchOnMount: false });
|
||||
|
||||
if (error) return <PingDot icon={IconX} color="red" tooltip={t("common.error")} />;
|
||||
if (error) return <PingDot icon={IconX} color="red" tooltip={t("error")} />;
|
||||
|
||||
if (!pingResult) return <PingDot icon={IconMinus} color="gray" tooltip={`${t("common.action.loading")}…`} />;
|
||||
if (!pingResult) return <PingDot icon={IconMinus} color="gray" tooltip={`${t("action.loading")}…`} />;
|
||||
|
||||
const isError = "error" in pingResult || pingResult.statusCode >= 500;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Avatar, Badge, Card, Group, ScrollArea, SimpleGrid, Stack, Text } from
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { formatByteRate } from "@homarr/common";
|
||||
import { getIconUrl } from "@homarr/definitions";
|
||||
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
@@ -47,7 +47,8 @@ const ArchiveTeamWarriorWidgetContent = ({
|
||||
height: number;
|
||||
isAdvanced: boolean;
|
||||
}) => {
|
||||
const t = useScopedI18n("widget.archiveTeamWarrior");
|
||||
const t = useI18n("widget.archiveTeamWarrior");
|
||||
const tWidgetCommon = useI18n("widget.common");
|
||||
const locale = useCurrentIntlLocale();
|
||||
const [data, statusQuery] = clientApi.widget.archiveTeamWarrior.getStatus.useSuspenseQuery({ integrationId });
|
||||
|
||||
@@ -107,7 +108,7 @@ const ArchiveTeamWarriorWidgetContent = ({
|
||||
{t("runnerStatus", { status: t(`status.${getStatusKey(status.runnerStatus ?? status.status)}`) })}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("updatedAt", { date: new Date(status.updatedAt).toLocaleString(locale) })}
|
||||
{tWidgetCommon("updatedAt", { date: new Date(status.updatedAt).toLocaleString(locale) })}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap="xs" wrap="wrap">
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
import { Center, Stack, Text, ThemeIcon } from "@mantine/core";
|
||||
import { IconRobot } from "@tabler/icons-react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { useAssistantWidgetRenderer } from "./context";
|
||||
|
||||
export default function AssistantWidget(props: WidgetComponentProps<"assistant">) {
|
||||
const Renderer = useAssistantWidgetRenderer();
|
||||
const t = useScopedI18n("widget.assistant.unavailable");
|
||||
const t = useI18n("widget.assistant.unavailable");
|
||||
|
||||
if (Renderer) return <Renderer {...props} />;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { CSSProperties } from "react";
|
||||
import { Text } from "@mantine/core";
|
||||
|
||||
import type { AudiobookshelfDashboardData, NavidromeDashboardData } from "@homarr/integrations/types";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import classes from "./component.module.css";
|
||||
import type { AudioStatsBackend, AudioStatsDisplayOptions } from "./shared";
|
||||
@@ -49,7 +49,7 @@ export function AudioStatsContent({
|
||||
width,
|
||||
height,
|
||||
}: AudioStatsContentProps) {
|
||||
const t = useScopedI18n(AUDIO_STATS_TRANSLATION_SCOPE);
|
||||
const t = useI18n(AUDIO_STATS_TRANSLATION_SCOPE);
|
||||
const compact = options.compactMode ?? false;
|
||||
const enabledStats = prioritizeVisibleStats(getVisibleStats(backend, options, stats, showAllStats), compact);
|
||||
const visibleStats = enabledStats.slice(0, getVisibleStatLimit(width, height, enabledStats.length, compact));
|
||||
|
||||
@@ -23,7 +23,8 @@ export default function AudioStatsWidget({
|
||||
displayMode = "compact",
|
||||
widgetRuntimeRef,
|
||||
}: WidgetComponentProps<"audioStats">) {
|
||||
const t = useI18n();
|
||||
const tAudio = useI18n("widget.audioStats");
|
||||
const tMediaServer = useI18n("widget.mediaServer");
|
||||
const statsInput = { integrationId: integrationIds[0] ?? "" };
|
||||
const { data: response, error: statsError } = clientApi.widget.audioStats.getStats.useQuery(statsInput);
|
||||
const streamsInput = { integrationIds: [statsInput.integrationId], showOnlyPlaying: true };
|
||||
@@ -56,7 +57,7 @@ export default function AudioStatsWidget({
|
||||
return (
|
||||
<Box h="100%" pos="relative">
|
||||
<Box pos="absolute" top={4} right={8} style={{ zIndex: 1 }}>
|
||||
<WidgetQueryErrorIndicator error={statsError} label={t("widget.audioStats.name")} />
|
||||
<WidgetQueryErrorIndicator error={statsError} label={tAudio("name")} />
|
||||
</Box>
|
||||
{summary}
|
||||
</Box>
|
||||
@@ -74,8 +75,8 @@ export default function AudioStatsWidget({
|
||||
<div style={{ minHeight: 150 }}>{summary}</div>
|
||||
{(statsError || streamsError || hasStreamErrors) && (
|
||||
<Group justify="flex-end">
|
||||
<WidgetQueryErrorIndicator error={statsError} label={t("widget.audioStats.name")} />
|
||||
<WidgetQueryErrorIndicator error={streamsError} label={t("widget.mediaServer.name")} />
|
||||
<WidgetQueryErrorIndicator error={statsError} label={tAudio("name")} />
|
||||
<WidgetQueryErrorIndicator error={streamsError} label={tMediaServer("name")} />
|
||||
<IntegrationErrorIndicator results={currentStreams} />
|
||||
</Group>
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,8 @@ import { Text } from "@mantine/core";
|
||||
import { IconAlertTriangle, IconBell, IconDeviceTv, IconMovie, IconPlugConnected } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { getIntegrationName } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
@@ -60,7 +61,7 @@ export default function BazarrWidget({
|
||||
height,
|
||||
displayMode,
|
||||
}: WidgetComponentProps<"bazarr">) {
|
||||
const t = useScopedI18n("widget.bazarr");
|
||||
const t = useI18n("widget.bazarr");
|
||||
const { data: badges, error } = clientApi.widget.bazarr.getBadges.useQuery(
|
||||
{ integrationId: integrationIds[0] ?? "" },
|
||||
{ enabled: Boolean(integrationIds[0]) },
|
||||
@@ -106,6 +107,14 @@ export default function BazarrWidget({
|
||||
{visibleStatKeys.map((statKey) => {
|
||||
const Icon = statIcons[statKey];
|
||||
const value = statValues[statKey];
|
||||
let label: string;
|
||||
if (statKey === "sonarrSignalr") {
|
||||
label = `${getIntegrationName("sonarr")} SignalR`;
|
||||
} else if (statKey === "radarrSignalr") {
|
||||
label = `${getIntegrationName("radarr")} SignalR`;
|
||||
} else {
|
||||
label = t(statKey);
|
||||
}
|
||||
const isWarning =
|
||||
typeof value === "number" &&
|
||||
(statKey === "providers" || statKey === "status" || statKey === "announcements") &&
|
||||
@@ -117,8 +126,8 @@ export default function BazarrWidget({
|
||||
<span className={`${classes.statValue} ${isWarning ? classes.statValueWarning : ""}`}>
|
||||
{typeof value === "string" && value.trim() === "" ? "—" : value}
|
||||
</span>
|
||||
<span className={classes.statLabel} title={t(statKey)}>
|
||||
{t(statKey)}
|
||||
<span className={classes.statLabel} title={label}>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ import type { LucideIcon } from "lucide-react";
|
||||
import { Activity, Battery, Cpu, HardDrive, MemoryStick, Monitor, Network, Server, Thermometer } from "lucide-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
|
||||
@@ -58,7 +58,7 @@ export default function BeszelAlertsWidget({
|
||||
displayMode,
|
||||
widgetRuntimeRef,
|
||||
}: WidgetComponentProps<"beszelAlerts">) {
|
||||
const t = useScopedI18n("widget.beszelAlerts");
|
||||
const t = useI18n("widget.beszelAlerts");
|
||||
const isAdvanced = displayMode === "advanced";
|
||||
const alertsInput = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -31,7 +31,7 @@ export const { definition, componentLoader } = createWidgetDefinition("beszelAle
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconServerOff,
|
||||
message: (t) => t("widget.beszelAlerts.error.internalServerError"),
|
||||
message: (t) => t("widget.beszel.error.internalServerError"),
|
||||
},
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -1,7 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import type { MantineSize } from "@mantine/core";
|
||||
import { Badge, Box, Card, Center, Group, Loader, Progress, SimpleGrid, Stack, Text, UnstyledButton } from "@mantine/core";
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Card,
|
||||
Center,
|
||||
Group,
|
||||
Loader,
|
||||
Progress,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
UnstyledButton,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
Activity,
|
||||
Battery,
|
||||
@@ -20,8 +32,9 @@ import { IconServerOff } from "@tabler/icons-react";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { formatBytes } from "@homarr/common";
|
||||
import { invariantTechnicalLabels } from "@homarr/definitions";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import classes from "./component.module.css";
|
||||
|
||||
@@ -168,7 +181,8 @@ const MetricRow = ({ icon, label, value, progress, size }: MetricRowProps) => (
|
||||
interface SystemCardProps {
|
||||
system: BeszelSystemRow & { rowKey: string };
|
||||
options: WidgetComponentProps<"beszelSystemGrid">["options"];
|
||||
t: ReturnType<typeof useScopedI18n<"widget.beszelSystemGrid">>;
|
||||
t: ReturnType<typeof useI18n<"widget.beszel">>;
|
||||
tCommon: ReturnType<typeof useI18n<"common">>;
|
||||
size: SizeConfig;
|
||||
maxMetrics: number;
|
||||
itemRadius: MantineSize;
|
||||
@@ -177,14 +191,25 @@ interface SystemCardProps {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const metricRenderers = [
|
||||
type BeszelMetricRenderer = {
|
||||
key: string;
|
||||
render: (
|
||||
system: BeszelSystemRow,
|
||||
t: SystemCardProps["t"],
|
||||
size: SizeConfig,
|
||||
tCommon: SystemCardProps["tCommon"],
|
||||
) => React.ReactNode;
|
||||
visible: (system: BeszelSystemRow, options: SystemCardProps["options"], advanced: boolean) => boolean;
|
||||
};
|
||||
|
||||
const metricRenderers: BeszelMetricRenderer[] = [
|
||||
{
|
||||
key: "showCpu",
|
||||
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
<MetricRow
|
||||
key="cpu"
|
||||
icon={<Cpu size={sz.iconSize} />}
|
||||
label={t("metric.cpu")}
|
||||
label={invariantTechnicalLabels.cpu}
|
||||
value={formatPercent(s.cpu)}
|
||||
progress={{ value: s.cpu, color: thresholdColor(s.cpu) }}
|
||||
size={sz}
|
||||
@@ -228,7 +253,7 @@ const metricRenderers = [
|
||||
<MetricRow
|
||||
key="gpu"
|
||||
icon={<Monitor size={sz.iconSize} />}
|
||||
label={t("metric.gpu")}
|
||||
label={invariantTechnicalLabels.gpu}
|
||||
value={formatPercent(s.gpu)}
|
||||
progress={{ value: s.gpu, color: thresholdColor(s.gpu) }}
|
||||
size={sz}
|
||||
@@ -299,11 +324,11 @@ const metricRenderers = [
|
||||
},
|
||||
{
|
||||
key: "showServices",
|
||||
render: (s: BeszelSystemRow, t: SystemCardProps["t"], sz: SizeConfig) => (
|
||||
render: (s: BeszelSystemRow, _t: SystemCardProps["t"], sz: SizeConfig, tCommon: SystemCardProps["tCommon"]) => (
|
||||
<MetricRow
|
||||
key="svc"
|
||||
icon={<Server size={sz.iconSize} />}
|
||||
label={t("metric.services")}
|
||||
label={tCommon("services")}
|
||||
value={String(s.services)}
|
||||
size={sz}
|
||||
/>
|
||||
@@ -345,6 +370,7 @@ const SystemCard = ({
|
||||
system,
|
||||
options,
|
||||
t,
|
||||
tCommon,
|
||||
size,
|
||||
maxMetrics,
|
||||
itemRadius,
|
||||
@@ -375,12 +401,7 @@ const SystemCard = ({
|
||||
}}
|
||||
>
|
||||
{onClick && (
|
||||
<UnstyledButton
|
||||
type="button"
|
||||
className={classes.cardAction}
|
||||
aria-label={system.name}
|
||||
onClick={onClick}
|
||||
/>
|
||||
<UnstyledButton type="button" className={classes.cardAction} aria-label={system.name} onClick={onClick} />
|
||||
)}
|
||||
<Group gap="xs" mb={2}>
|
||||
<Badge
|
||||
@@ -395,20 +416,20 @@ const SystemCard = ({
|
||||
|
||||
{isAdvanced && (
|
||||
<SimpleGrid cols={2} spacing={4} verticalSpacing={2} mb={4}>
|
||||
<SystemMetadata label={t("metadata.hostname")} value={system.hostname || "—"} />
|
||||
<SystemMetadata label={t("metadata.os")} value={system.osName || "—"} />
|
||||
<SystemMetadata label={t("metadata.cpuModel")} value={system.cpuModel || "—"} />
|
||||
<SystemMetadata label={t("metadata.cores")} value={String(system.cores)} />
|
||||
<SystemMetadata label={t("metric.hostname")} value={system.hostname || "—"} />
|
||||
<SystemMetadata label={invariantTechnicalLabels.os} value={system.osName || "—"} />
|
||||
<SystemMetadata label={t("metric.cpuModel")} value={system.cpuModel || "—"} />
|
||||
<SystemMetadata label={t("metric.cores")} value={String(system.cores)} />
|
||||
<SystemMetadata
|
||||
label={t("metadata.memoryTotal")}
|
||||
label={t("metric.memoryTotal")}
|
||||
value={system.memoryTotal > 0 ? formatBytes(system.memoryTotal) : "—"}
|
||||
/>
|
||||
<SystemMetadata label={t("metadata.source")} value={integrationName} />
|
||||
<SystemMetadata label={t("metric.source")} value={integrationName} />
|
||||
</SimpleGrid>
|
||||
)}
|
||||
|
||||
<Stack gap={0} style={{ flex: 1 }} justify="space-evenly">
|
||||
{visibleMetrics.map((m) => m.render(system, t, size))}
|
||||
{visibleMetrics.map((m) => m.render(system, t, size, tCommon))}
|
||||
</Stack>
|
||||
{hiddenMetricCount > 0 && (
|
||||
<Text size="xs" c="dimmed" ta="right">
|
||||
@@ -438,7 +459,9 @@ export default function BeszelSystemGridWidget({
|
||||
height,
|
||||
displayMode,
|
||||
}: WidgetComponentProps<"beszelSystemGrid">) {
|
||||
const t = useScopedI18n("widget.beszelSystemGrid");
|
||||
const t = useI18n("widget.beszel");
|
||||
const tFeature = useI18n("widget.beszelSystemGrid");
|
||||
const tCommon = useI18n("common");
|
||||
const board = useRequiredBoard();
|
||||
const { openModal } = useModalAction(BeszelSystemStatsModal);
|
||||
|
||||
@@ -462,7 +485,7 @@ export default function BeszelSystemGridWidget({
|
||||
return (
|
||||
<Box h="100%" pos="relative" style={{ pointerEvents: isEditMode ? "none" : undefined }}>
|
||||
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 1 }}>
|
||||
<WidgetQueryErrorIndicator error={systemsQuery.error} label={t("name")} />
|
||||
<WidgetQueryErrorIndicator error={systemsQuery.error} label={tFeature("name")} />
|
||||
<IntegrationErrorIndicator results={results} />
|
||||
</Group>
|
||||
<Center h="100%">
|
||||
@@ -491,7 +514,7 @@ export default function BeszelSystemGridWidget({
|
||||
return (
|
||||
<Box h="100%" pos="relative" style={{ pointerEvents: isEditMode ? "none" : undefined }}>
|
||||
<Group pos="absolute" top={4} right={8} gap={0} style={{ zIndex: 1 }}>
|
||||
<WidgetQueryErrorIndicator error={systemsQuery.error} label={t("name")} />
|
||||
<WidgetQueryErrorIndicator error={systemsQuery.error} label={tFeature("name")} />
|
||||
<IntegrationErrorIndicator results={results} />
|
||||
</Group>
|
||||
<Box
|
||||
@@ -516,6 +539,7 @@ export default function BeszelSystemGridWidget({
|
||||
system={system}
|
||||
options={options}
|
||||
t={t}
|
||||
tCommon={tCommon}
|
||||
size={size}
|
||||
maxMetrics={maxMetrics}
|
||||
itemRadius={board.itemRadius}
|
||||
|
||||
@@ -31,7 +31,7 @@ export const { definition, componentLoader } = createWidgetDefinition("beszelSys
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconServerOff,
|
||||
message: (t) => t("widget.beszelSystemGrid.error.internalServerError"),
|
||||
message: (t) => t("widget.beszel.error.internalServerError"),
|
||||
},
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -10,7 +10,7 @@ import { useSession } from "@homarr/auth/client";
|
||||
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||
import { useOptionalBoard } from "@homarr/boards/context";
|
||||
import { showErrorNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import classes from "./component.module.css";
|
||||
|
||||
@@ -33,7 +33,8 @@ export default function BeszelSystemStatsWidget({
|
||||
setOptions,
|
||||
widgetRuntimeRef,
|
||||
}: WidgetComponentProps<"beszelSystemStats">) {
|
||||
const t = useScopedI18n("widget.beszelSystemStats");
|
||||
const t = useI18n("widget.beszelSystemStats");
|
||||
const tBeszel = useI18n("widget.beszel");
|
||||
const board = useOptionalBoard();
|
||||
const { data: session } = useSession();
|
||||
const hasChangeAccess = board ? constructBoardPermissions(board, session).hasChangeAccess : false;
|
||||
@@ -150,7 +151,7 @@ export default function BeszelSystemStatsWidget({
|
||||
<Stack align="center" gap="xs">
|
||||
<IconServerOff size={28} opacity={0.5} />
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("empty.noSystems")}
|
||||
{tBeszel("empty.noSystems")}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
|
||||
@@ -74,7 +74,7 @@ export const { definition, componentLoader } = createWidgetDefinition("beszelSys
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconServerOff,
|
||||
message: (t) => t("widget.beszelSystemStats.error.internalServerError"),
|
||||
message: (t) => t("widget.beszel.error.internalServerError"),
|
||||
},
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -21,9 +21,10 @@ import { useSession } from "@homarr/auth/client";
|
||||
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||
import { useOptionalBoard } from "@homarr/boards/context";
|
||||
import { formatBytes } from "@homarr/common";
|
||||
import { invariantTechnicalLabels } from "@homarr/definitions";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { showErrorNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { HomarrDataTable } from "../common/homarr-data-table";
|
||||
@@ -124,7 +125,9 @@ export default function BeszelSystemTableWidget({
|
||||
setOptions,
|
||||
displayMode,
|
||||
}: WidgetComponentProps<"beszelSystemTable">) {
|
||||
const t = useScopedI18n("widget.beszelSystemTable");
|
||||
const t = useI18n("widget.beszelSystemTable");
|
||||
const tBeszel = useI18n("widget.beszel");
|
||||
const tCommon = useI18n("common");
|
||||
const { openModal } = useModalAction(BeszelSystemStatsModal);
|
||||
const board = useOptionalBoard();
|
||||
const { data: session } = useSession();
|
||||
@@ -201,7 +204,7 @@ export default function BeszelSystemTableWidget({
|
||||
title: (
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<Server size={size.iconSize} />
|
||||
<Text inherit>{t("column.system")}</Text>
|
||||
<Text inherit>{tBeszel("metric.system")}</Text>
|
||||
</Group>
|
||||
),
|
||||
sortable: true,
|
||||
@@ -218,7 +221,7 @@ export default function BeszelSystemTableWidget({
|
||||
accessor: "integrationName",
|
||||
width: 140,
|
||||
ellipsis: true,
|
||||
title: t("column.source"),
|
||||
title: tBeszel("metric.source"),
|
||||
sortable: true,
|
||||
render: (record) => <Text size={size.fontSize}>{record.integrationName}</Text>,
|
||||
},
|
||||
@@ -226,7 +229,7 @@ export default function BeszelSystemTableWidget({
|
||||
accessor: "hostname",
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
title: t("column.hostname"),
|
||||
title: tBeszel("metric.hostname"),
|
||||
sortable: true,
|
||||
render: (record) => <Text size={size.fontSize}>{record.hostname || "—"}</Text>,
|
||||
},
|
||||
@@ -234,7 +237,7 @@ export default function BeszelSystemTableWidget({
|
||||
accessor: "osName",
|
||||
width: 160,
|
||||
ellipsis: true,
|
||||
title: t("column.os"),
|
||||
title: invariantTechnicalLabels.os,
|
||||
sortable: true,
|
||||
render: (record) => <Text size={size.fontSize}>{record.osName || "—"}</Text>,
|
||||
},
|
||||
@@ -242,21 +245,21 @@ export default function BeszelSystemTableWidget({
|
||||
accessor: "cpuModel",
|
||||
width: 220,
|
||||
ellipsis: true,
|
||||
title: t("column.cpuModel"),
|
||||
title: tBeszel("metric.cpuModel"),
|
||||
sortable: true,
|
||||
render: (record) => <Text size={size.fontSize}>{record.cpuModel || "—"}</Text>,
|
||||
},
|
||||
isAdvanced && {
|
||||
accessor: "cores",
|
||||
width: 80,
|
||||
title: t("column.cores"),
|
||||
title: tBeszel("metric.cores"),
|
||||
sortable: true,
|
||||
render: (record) => <Text size={size.fontSize}>{record.cores}</Text>,
|
||||
},
|
||||
isAdvanced && {
|
||||
accessor: "memoryTotal",
|
||||
width: 120,
|
||||
title: t("column.memoryTotal"),
|
||||
title: tBeszel("metric.memoryTotal"),
|
||||
sortable: true,
|
||||
render: (record) => (
|
||||
<Text size={size.fontSize}>{record.memoryTotal > 0 ? formatBytes(record.memoryTotal) : "—"}</Text>
|
||||
@@ -268,7 +271,7 @@ export default function BeszelSystemTableWidget({
|
||||
title: (
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<Cpu size={size.iconSize} />
|
||||
<Text inherit>{t("column.cpu")}</Text>
|
||||
<Text inherit>{invariantTechnicalLabels.cpu}</Text>
|
||||
</Group>
|
||||
),
|
||||
sortable: true,
|
||||
@@ -280,7 +283,7 @@ export default function BeszelSystemTableWidget({
|
||||
title: (
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<MemoryStick size={size.iconSize} />
|
||||
<Text inherit>{t("column.memory")}</Text>
|
||||
<Text inherit>{tBeszel("metric.memory")}</Text>
|
||||
</Group>
|
||||
),
|
||||
sortable: true,
|
||||
@@ -292,7 +295,7 @@ export default function BeszelSystemTableWidget({
|
||||
title: (
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<HardDrive size={size.iconSize} />
|
||||
<Text inherit>{t("column.disk")}</Text>
|
||||
<Text inherit>{tBeszel("metric.disk")}</Text>
|
||||
</Group>
|
||||
),
|
||||
sortable: true,
|
||||
@@ -312,7 +315,7 @@ export default function BeszelSystemTableWidget({
|
||||
title: (
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<Monitor size={size.iconSize} />
|
||||
<Text inherit>{t("column.gpu")}</Text>
|
||||
<Text inherit>{invariantTechnicalLabels.gpu}</Text>
|
||||
</Group>
|
||||
),
|
||||
sortable: true,
|
||||
@@ -324,7 +327,7 @@ export default function BeszelSystemTableWidget({
|
||||
title: (
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<Activity size={size.iconSize} />
|
||||
<Text inherit>{t("column.loadAvg")}</Text>
|
||||
<Text inherit>{tBeszel("metric.loadAvg")}</Text>
|
||||
</Group>
|
||||
),
|
||||
sortable: true,
|
||||
@@ -346,7 +349,7 @@ export default function BeszelSystemTableWidget({
|
||||
title: (
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<Network size={size.iconSize} />
|
||||
<Text inherit>{t("column.net")}</Text>
|
||||
<Text inherit>{tBeszel("metric.net")}</Text>
|
||||
</Group>
|
||||
),
|
||||
sortable: true,
|
||||
@@ -362,7 +365,7 @@ export default function BeszelSystemTableWidget({
|
||||
title: (
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<Thermometer size={size.iconSize} />
|
||||
<Text inherit>{t("column.temp")}</Text>
|
||||
<Text inherit>{tBeszel("metric.temp")}</Text>
|
||||
</Group>
|
||||
),
|
||||
sortable: true,
|
||||
@@ -374,7 +377,7 @@ export default function BeszelSystemTableWidget({
|
||||
title: (
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<Battery size={size.iconSize} />
|
||||
<Text inherit>{t("column.battery")}</Text>
|
||||
<Text inherit>{tBeszel("metric.battery")}</Text>
|
||||
</Group>
|
||||
),
|
||||
render: (record) => <Text size={size.fontSize}>{record.battery ? `${record.battery[0]}%` : "—"}</Text>,
|
||||
@@ -385,7 +388,7 @@ export default function BeszelSystemTableWidget({
|
||||
title: (
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<Server size={size.iconSize} />
|
||||
<Text inherit>{t("column.services")}</Text>
|
||||
<Text inherit>{tCommon("services")}</Text>
|
||||
</Group>
|
||||
),
|
||||
sortable: true,
|
||||
@@ -397,7 +400,7 @@ export default function BeszelSystemTableWidget({
|
||||
title: (
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<Activity size={size.iconSize} />
|
||||
<Text inherit>{t("column.uptime")}</Text>
|
||||
<Text inherit>{tBeszel("metric.uptime")}</Text>
|
||||
</Group>
|
||||
),
|
||||
sortable: true,
|
||||
@@ -409,7 +412,7 @@ export default function BeszelSystemTableWidget({
|
||||
title: (
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<Wifi size={size.iconSize} />
|
||||
<Text inherit>{t("column.agent")}</Text>
|
||||
<Text inherit>{tBeszel("metric.agent")}</Text>
|
||||
</Group>
|
||||
),
|
||||
sortable: true,
|
||||
@@ -418,7 +421,7 @@ export default function BeszelSystemTableWidget({
|
||||
];
|
||||
|
||||
return cols.filter(Boolean) as DataTableColumn<SystemRowWithKey>[];
|
||||
}, [t, size, visibleMetricKeys, isAdvanced]);
|
||||
}, [tBeszel, tCommon, size, visibleMetricKeys, isAdvanced]);
|
||||
|
||||
const { effectiveColumns, storeKey } = usePersistedTableLayout({
|
||||
columns,
|
||||
@@ -462,7 +465,7 @@ export default function BeszelSystemTableWidget({
|
||||
columns={effectiveColumns}
|
||||
sortStatus={sortStatus}
|
||||
onSortStatusChange={setSortStatus}
|
||||
noRecordsText={t("noRecords")}
|
||||
noRecordsText={tBeszel("empty.noSystems")}
|
||||
idAccessor="rowKey"
|
||||
storeColumnsKey={storeKey}
|
||||
onRowClick={isEditMode ? undefined : handleRowClick}
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import { IconServerOff, IconTable } from "@tabler/icons-react";
|
||||
|
||||
import { invariantTechnicalLabels } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition, matchesWidgetRuntimeQuery, widgetQueryInputMatches } from "../definition";
|
||||
import type { SelectOption } from "../_inputs/widget-select-input";
|
||||
import { statusOptions } from "../beszel/_shared/options";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
const sortOptions = [
|
||||
{ value: "name", label: "System" },
|
||||
{ value: "cpu", label: "CPU" },
|
||||
{ value: "memory", label: "Memory" },
|
||||
{ value: "disk", label: "Disk" },
|
||||
{ value: "gpu", label: "GPU" },
|
||||
{ value: "loadAvg", label: "Load Avg" },
|
||||
{ value: "netBytes", label: "Net" },
|
||||
{ value: "temp", label: "Temp" },
|
||||
{ value: "services", label: "Services" },
|
||||
{ value: "uptime", label: "Uptime" },
|
||||
{ value: "agentVersion", label: "Agent" },
|
||||
];
|
||||
{ value: "name", label: (t) => t("widget.beszel.metric.system") },
|
||||
{ value: "cpu", label: invariantTechnicalLabels.cpu },
|
||||
{ value: "memory", label: (t) => t("widget.beszel.metric.memory") },
|
||||
{ value: "disk", label: (t) => t("widget.beszel.metric.disk") },
|
||||
{ value: "gpu", label: invariantTechnicalLabels.gpu },
|
||||
{ value: "loadAvg", label: (t) => t("widget.beszel.metric.loadAvg") },
|
||||
{ value: "netBytes", label: (t) => t("widget.beszel.metric.net") },
|
||||
{ value: "temp", label: (t) => t("widget.beszel.metric.temp") },
|
||||
{ value: "services", label: (t) => t("common.services") },
|
||||
{ value: "uptime", label: (t) => t("widget.beszel.metric.uptime") },
|
||||
{ value: "agentVersion", label: (t) => t("widget.beszel.metric.agent") },
|
||||
] satisfies SelectOption[];
|
||||
|
||||
const sortDirectionOptions = [
|
||||
{ value: "asc", label: "Ascending" },
|
||||
@@ -67,7 +70,7 @@ export const { definition, componentLoader } = createWidgetDefinition("beszelSys
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconServerOff,
|
||||
message: (t) => t("widget.beszelSystemTable.error.internalServerError"),
|
||||
message: (t) => t("widget.beszel.error.internalServerError"),
|
||||
},
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Box, Group, Popover, Progress, Stack, Text, UnstyledButton } from "@mantine/core";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { BeszelSystemRow } from "./types";
|
||||
import { thresholdColor } from "./colors";
|
||||
@@ -21,7 +21,7 @@ const mountLabel = (path: string) =>
|
||||
const severityColor = (value: number) => `var(--mantine-color-${thresholdColor(value)}-6)`;
|
||||
|
||||
export const DiskUsage = ({ system, fontSize, progressSize, valueMiw, valueGap = 6 }: DiskUsageProps) => {
|
||||
const t = useScopedI18n("common");
|
||||
const t = useI18n("common");
|
||||
const [opened, setOpened] = useState(false);
|
||||
const filesystems = Object.entries(system.extraFilesystems).filter(([path]) => path !== "/");
|
||||
const filesystemUsageLabel = t("filesystemUsage", { count: filesystems.length + 1 });
|
||||
|
||||
@@ -5,8 +5,9 @@ import { Button, Center, Group, SimpleGrid, Skeleton, Stack, Text } from "@manti
|
||||
import { IconPlugConnectedX, IconServerOff } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { invariantTechnicalLabels } from "@homarr/definitions";
|
||||
import type { BeszelContainerStatsRecord, BeszelSystemStatsRecord } from "@homarr/integrations/types";
|
||||
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { getUsableWidgetQueryData } from "../../common/query-state";
|
||||
import { containerColors } from "./colors";
|
||||
@@ -73,7 +74,8 @@ export function BeszelStatsView({
|
||||
columns,
|
||||
onSwitchToHistorical,
|
||||
}: BeszelStatsViewProps) {
|
||||
const t = useScopedI18n("widget.beszelSystemStats");
|
||||
const t = useI18n("widget.beszelSystemStats");
|
||||
const tBeszel = useI18n("widget.beszel");
|
||||
const locale = useCurrentIntlLocale();
|
||||
const showDocker = visibility.dockerCpu || visibility.dockerMemory || visibility.dockerNetwork;
|
||||
const isLive = timePeriod === "1m";
|
||||
@@ -91,7 +93,7 @@ export function BeszelStatsView({
|
||||
|
||||
const mappers = useMemo(
|
||||
() => ({
|
||||
cpu: (s: { cpu: number }) => ({ [t("chart.cpu.series")]: s.cpu }),
|
||||
cpu: (s: { cpu: number }) => ({ [invariantTechnicalLabels.cpu]: s.cpu }),
|
||||
memory: (s: { mu: number; mb: number }) => ({
|
||||
[t("chart.memory.series")]: s.mu,
|
||||
[t("chart.memory.cache")]: s.mb ?? 0,
|
||||
@@ -110,7 +112,7 @@ export function BeszelStatsView({
|
||||
|
||||
const series = useMemo(
|
||||
() => ({
|
||||
cpu: [{ name: t("chart.cpu.series"), color: "teal.6" }],
|
||||
cpu: [{ name: invariantTechnicalLabels.cpu, color: "teal.6" }],
|
||||
memory: [
|
||||
{ name: t("chart.memory.series"), color: "teal.5" },
|
||||
{ name: t("chart.memory.cache"), color: "teal.8" },
|
||||
@@ -238,7 +240,7 @@ export function BeszelStatsView({
|
||||
<Stack align="center" gap="xs">
|
||||
<IconServerOff size={24} opacity={0.5} />
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("error.internalServerError")}
|
||||
{tBeszel("error.internalServerError")}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { CSSProperties } from "react";
|
||||
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { formatLocalizedDate } from "../../common/locale";
|
||||
|
||||
@@ -54,7 +54,7 @@ interface BeszelTooltipProps {
|
||||
|
||||
const BeszelTooltipContent = ({ active, label, payload, formatter, showTotal }: BeszelTooltipProps) => {
|
||||
const locale = useCurrentIntlLocale();
|
||||
const t = useScopedI18n("common");
|
||||
const t = useI18n("common");
|
||||
const isActive = active && (payload?.length ?? 0) > 0;
|
||||
if (!isActive) return null;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { IconCheck } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { SortableItemListInput } from "../options";
|
||||
|
||||
@@ -17,9 +17,9 @@ export const BookmarkAddButton: SortableItemListInput<SelectableApp, string>["Ad
|
||||
removeItem,
|
||||
values,
|
||||
}) => {
|
||||
const t = useScopedI18n("widget.bookmarks.option.items.apps");
|
||||
const tCommon = useScopedI18n("common");
|
||||
const tSelect = useScopedI18n("app.action.select");
|
||||
const t = useI18n("widget.bookmarks.option.items.apps");
|
||||
const tCommon = useI18n("common");
|
||||
const tSelect = useI18n("app.action.select");
|
||||
const { data: apps = [], isPending, error } = clientApi.app.selectable.useQuery();
|
||||
const appsById = new Map(apps.map((app) => [app.id, app]));
|
||||
|
||||
|
||||
@@ -20,7 +20,12 @@ export const { definition, componentLoader } = createWidgetDefinition("bookmarks
|
||||
layout: factory.select({
|
||||
options: (["grid", "gridHorizontal", "row", "column"] as const).map((value) => ({
|
||||
value,
|
||||
label: (t) => t(`widget.bookmarks.option.layout.option.${value}.label`),
|
||||
label: (t) => {
|
||||
if (value === "grid") return t("widget.common.layout.option.grid");
|
||||
if (value === "row") return t("widget.common.layout.option.horizontal");
|
||||
if (value === "column") return t("widget.common.layout.option.vertical");
|
||||
return t("widget.bookmarks.option.layout.option.gridHorizontal.label");
|
||||
},
|
||||
})),
|
||||
defaultValue: "column",
|
||||
}),
|
||||
|
||||
@@ -95,7 +95,7 @@ const CalendarEventRows = ({
|
||||
advanced,
|
||||
}: Pick<CalendarEventListProps, "events" | "advanced"> & { locale: string }) => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.calendar");
|
||||
return (
|
||||
<>
|
||||
{events.map((event, eventIndex) => (
|
||||
@@ -145,7 +145,7 @@ const CalendarEventRows = ({
|
||||
{event.metadata?.type === "radarr" && (
|
||||
<Group wrap="nowrap">
|
||||
<Text c="dimmed" size="sm">
|
||||
{t(`widget.calendar.option.releaseType.options.${event.metadata.releaseType}`)}
|
||||
{t(`option.releaseType.options.${event.metadata.releaseType}`)}
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
@@ -154,7 +154,7 @@ const CalendarEventRows = ({
|
||||
<IconClock opacity={0.7} size={"1rem"} />
|
||||
{isAllDay(event) ? (
|
||||
<Text c={"dimmed"} size={"sm"}>
|
||||
{t("widget.calendar.duration.allDay")}
|
||||
{t("duration.allDay")}
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -11,7 +11,7 @@ import dayjs from "dayjs";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useSettings } from "@homarr/settings";
|
||||
import { useCurrentIntlLocale, useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
|
||||
import actionTargetClasses from "../common/action-target.module.css";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
@@ -138,7 +138,7 @@ const CalendarBase = ({
|
||||
options,
|
||||
displayMode,
|
||||
}: CalendarBaseProps) => {
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.calendar");
|
||||
const locale = useCurrentIntlLocale();
|
||||
const { firstDayOfWeek } = useSettings();
|
||||
const board = useRequiredBoard();
|
||||
@@ -171,7 +171,7 @@ const CalendarBase = ({
|
||||
failedIntegrations={failedIntegrations}
|
||||
isPending={isPending}
|
||||
queryError={queryError}
|
||||
queryErrorLabel={t("widget.calendar.name")}
|
||||
queryErrorLabel={t("name")}
|
||||
isEditMode={isEditMode}
|
||||
locale={locale}
|
||||
month={month}
|
||||
@@ -185,7 +185,7 @@ const CalendarBase = ({
|
||||
{(failedIntegrations.length > 0 || Boolean(queryError)) && (
|
||||
<Group px="xs" justify="flex-end">
|
||||
<IntegrationErrorIndicator results={failedIntegrations} />
|
||||
<WidgetQueryErrorIndicator error={queryError} label={t("widget.calendar.name")} />
|
||||
<WidgetQueryErrorIndicator error={queryError} label={t("name")} />
|
||||
</Group>
|
||||
)}
|
||||
<Calendar
|
||||
@@ -287,7 +287,7 @@ const CalendarAgenda = ({
|
||||
month,
|
||||
setMonth,
|
||||
}: CalendarAgendaProps) => {
|
||||
const t = useScopedI18n("widget.calendar.advanced");
|
||||
const t = useI18n("widget.calendar.advanced");
|
||||
const monthLabel = new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" }).format(month);
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { IconAlertTriangle, IconClock } from "@tabler/icons-react";
|
||||
import type { Dayjs } from "dayjs";
|
||||
|
||||
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { formatLocalizedDate, formatLocalizedTime } from "../common/locale";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
@@ -47,7 +47,7 @@ export const AdvancedClockView = ({
|
||||
primaryTimeZone,
|
||||
primaryTimeZoneInvalid,
|
||||
}: AdvancedClockViewProps) => {
|
||||
const t = useScopedI18n("widget.clock");
|
||||
const t = useI18n("widget.clock");
|
||||
const primaryLabel = getPrimaryLabel(options, t);
|
||||
const primary: WorldClockTime = {
|
||||
id: "primary",
|
||||
@@ -95,7 +95,7 @@ interface ModeProps {
|
||||
|
||||
const PrimaryClock = ({ now, options, primary }: Pick<ModeProps, "now" | "options" | "primary">) => {
|
||||
const locale = useCurrentIntlLocale();
|
||||
const t = useScopedI18n("widget.clock");
|
||||
const t = useI18n("widget.clock");
|
||||
const primaryTime = primary.zonedTime;
|
||||
if (!primaryTime || primary.phase === null || primary.minuteOfDay === null) return null;
|
||||
|
||||
@@ -152,7 +152,7 @@ const PrimaryClock = ({ now, options, primary }: Pick<ModeProps, "now" | "option
|
||||
};
|
||||
|
||||
const OverviewMode = ({ now, options, primary, cities }: ModeProps) => {
|
||||
const t = useScopedI18n("widget.clock");
|
||||
const t = useI18n("widget.clock");
|
||||
if (cities.length === 0) return <EmptyCities />;
|
||||
|
||||
return (
|
||||
@@ -218,7 +218,7 @@ const ClockTime = ({
|
||||
compact = false,
|
||||
}: Omit<ClockEntryProps, "primaryOffset"> & { primary?: boolean; compact?: boolean }) => {
|
||||
const locale = useCurrentIntlLocale();
|
||||
const t = useScopedI18n("widget.clock");
|
||||
const t = useI18n("widget.clock");
|
||||
if (!entry.zonedTime) return null;
|
||||
let formattedTime = formatLocalizedTime(now, locale, {
|
||||
hour12: clockTimeFormatUses12Hours(options.customTimeFormat, options.is24HourFormat),
|
||||
@@ -257,7 +257,7 @@ const ClockTime = ({
|
||||
|
||||
const DayAndOffset = ({ entry, now, primaryOffset }: Pick<ClockEntryProps, "entry" | "now" | "primaryOffset">) => {
|
||||
const locale = useCurrentIntlLocale();
|
||||
const t = useScopedI18n("widget.clock");
|
||||
const t = useI18n("widget.clock");
|
||||
if (entry.dayDifference === null || entry.utcOffset === null) return null;
|
||||
const dayRelation = getDayRelation(entry.dayDifference, t);
|
||||
const date = formatLocalizedDate(now, locale, {
|
||||
@@ -280,7 +280,7 @@ const DayAndOffset = ({ entry, now, primaryOffset }: Pick<ClockEntryProps, "entr
|
||||
};
|
||||
|
||||
const InvalidTimeZone = ({ entry }: { entry: WorldClockTime }) => {
|
||||
const t = useScopedI18n("widget.clock");
|
||||
const t = useI18n("widget.clock");
|
||||
return (
|
||||
<Paper component="li" withBorder radius="md" p="md">
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
@@ -297,7 +297,7 @@ const InvalidTimeZone = ({ entry }: { entry: WorldClockTime }) => {
|
||||
};
|
||||
|
||||
const EmptyCities = () => {
|
||||
const t = useScopedI18n("widget.clock");
|
||||
const t = useI18n("widget.clock");
|
||||
return (
|
||||
<Paper withBorder radius="md" p="xl">
|
||||
<Text c="dimmed" ta="center">
|
||||
@@ -307,7 +307,7 @@ const EmptyCities = () => {
|
||||
);
|
||||
};
|
||||
|
||||
type ScopedClockTranslator = ReturnType<typeof useScopedI18n<"widget.clock">>;
|
||||
type ScopedClockTranslator = ReturnType<typeof useI18n<"widget.clock">>;
|
||||
|
||||
const getDayRelation = (difference: number, t: ScopedClockTranslator) => {
|
||||
if (difference < 0) return t("worldClock.yesterday");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Badge, Box, Group, Progress, Stack, Text } from "@mantine/core";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { TimeOfDayPhase } from "./world-clock";
|
||||
|
||||
@@ -36,7 +36,7 @@ export const TimeOfDayBar = ({
|
||||
showTicks = false,
|
||||
label,
|
||||
}: TimeOfDayBarProps) => {
|
||||
const t = useScopedI18n("widget.clock");
|
||||
const t = useI18n("widget.clock");
|
||||
const progress = Math.max(0, Math.min(100, (minuteOfDay / 1440) * 100));
|
||||
const remainingMinutes = Math.max(0, 1440 - Math.floor(minuteOfDay));
|
||||
const remainingHours = Math.floor(remainingMinutes / 60);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Group, Loader, Stack, Text } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { isInitialWidgetQueryPending } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
@@ -26,7 +26,7 @@ export const ClockWeatherSummary = ({
|
||||
detailed,
|
||||
}: ClockWeatherSummaryProps) => {
|
||||
const t = useI18n();
|
||||
const tCommon = useScopedI18n("common");
|
||||
const tCommon = useI18n("common");
|
||||
const weatherQuery = clientApi.widget.weather.atLocation.useQuery({ latitude, longitude });
|
||||
const weather = weatherQuery.data;
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ import { Center, Text } from "@mantine/core";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
export const WidgetEmptyState = () => {
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.common.error");
|
||||
|
||||
return (
|
||||
<Center h="100%" w="100%" p="sm">
|
||||
<Text c="dimmed" size="sm" ta="center">
|
||||
{t("widget.common.error.noData")}
|
||||
{t("noData")}
|
||||
</Text>
|
||||
</Center>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { ActionIcon, Tooltip } from "@mantine/core";
|
||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
interface IntegrationResult {
|
||||
integrationId: string;
|
||||
@@ -16,7 +16,7 @@ interface IntegrationErrorIndicatorProps {
|
||||
}
|
||||
|
||||
export function IntegrationErrorIndicator({ results }: IntegrationErrorIndicatorProps) {
|
||||
const t = useScopedI18n("common");
|
||||
const t = useI18n("common");
|
||||
const failed = results.filter((result) => Boolean(result.error));
|
||||
if (failed.length === 0) return null;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { WidgetQueryErrorIndicator, WidgetQueryLoadingState } from "./query-state-indicator";
|
||||
|
||||
vi.mock("@homarr/translation/client", () => ({
|
||||
useScopedI18n: () => (key: string, values?: { widget?: string }) => {
|
||||
useI18n: () => (key: string, values?: { widget?: string }) => {
|
||||
if (key === "stale") return `Could not refresh ${values?.widget}; showing saved data`;
|
||||
if (key === "loading") return "Loading widget data";
|
||||
return key;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { ActionIcon, Center, Loader, Tooltip, VisuallyHidden } from "@mantine/core";
|
||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
interface WidgetQueryErrorIndicatorProps {
|
||||
error: unknown;
|
||||
@@ -12,7 +12,7 @@ interface WidgetQueryErrorIndicatorProps {
|
||||
|
||||
/** Localized warning for stale or optional-query failures. Never renders the raw error. */
|
||||
export function WidgetQueryErrorIndicator({ error, label }: WidgetQueryErrorIndicatorProps) {
|
||||
const t = useScopedI18n("common.widgetQuery");
|
||||
const t = useI18n("common.widgetQuery");
|
||||
if (!error) return null;
|
||||
|
||||
const accessibleLabel = t("stale", { widget: label });
|
||||
@@ -35,7 +35,7 @@ export function WidgetQueryErrorIndicator({ error, label }: WidgetQueryErrorIndi
|
||||
}
|
||||
|
||||
export function WidgetQueryLoadingState() {
|
||||
const t = useScopedI18n("common.widgetQuery");
|
||||
const t = useI18n("common.widgetQuery");
|
||||
return (
|
||||
<Center component="output" h="100%" w="100%" p="sm" aria-live="polite">
|
||||
<Loader size="sm" />
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Accordion, Badge, Group, Stack, Text } from "@mantine/core";
|
||||
import { IconCloud } from "@tabler/icons-react";
|
||||
|
||||
import type { CoolifyApplicationWithContext } from "@homarr/integrations/types";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { getBadgeColor, parseStatus } from "./coolify-utils";
|
||||
import { ResourceRow } from "./resource-row";
|
||||
@@ -16,8 +16,8 @@ interface ApplicationsSectionProps {
|
||||
}
|
||||
|
||||
export function ApplicationsSection({ applications, baseUrl, isTiny }: ApplicationsSectionProps) {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const tCommon = useScopedI18n("common");
|
||||
const t = useI18n("widget.coolify");
|
||||
const tCommon = useI18n("common");
|
||||
const runningApps = applications.filter((app) => parseStatus(app.status ?? "") === "running").length;
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import { Badge, Box, Group, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { getIntegrationName } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } from "../common/query-state-indicator";
|
||||
@@ -21,7 +22,7 @@ export default function CoolifyWidget({
|
||||
height,
|
||||
displayMode,
|
||||
}: WidgetComponentProps<"coolify">) {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const t = useI18n("widget.coolify");
|
||||
|
||||
if (integrationIds.length === 0) {
|
||||
return (
|
||||
@@ -54,7 +55,7 @@ interface CoolifyContentProps {
|
||||
}
|
||||
|
||||
function CoolifyContent({ integrationIds, options, width, height, isAdvanced }: CoolifyContentProps) {
|
||||
const t = useScopedI18n("common");
|
||||
const t = useI18n("common");
|
||||
const instancesQuery = clientApi.widget.coolify.getInstancesInfo.useQuery({
|
||||
integrationIds,
|
||||
});
|
||||
@@ -137,7 +138,7 @@ const FailedSourceBadges = ({
|
||||
sources: { integrationId: string; integrationName: string }[];
|
||||
queryError: unknown;
|
||||
}) => {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const t = useI18n("widget.coolify");
|
||||
if (sources.length === 0 && !queryError) return null;
|
||||
|
||||
return (
|
||||
@@ -148,7 +149,7 @@ const FailedSourceBadges = ({
|
||||
</Badge>
|
||||
))}
|
||||
<Box ml="auto">
|
||||
<WidgetQueryErrorIndicator error={queryError} label={t("name")} />
|
||||
<WidgetQueryErrorIndicator error={queryError} label={getIntegrationName("coolify")} />
|
||||
</Box>
|
||||
</Group>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Accordion, Anchor, Badge, Card, Group, Image, Stack, Text, Tooltip } fr
|
||||
import { useLocalStorage } from "@mantine/hooks";
|
||||
|
||||
import { useTimeAgo } from "@homarr/common";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { ApplicationsSection } from "./applications-section";
|
||||
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url";
|
||||
@@ -25,8 +25,8 @@ interface InstanceCardProps {
|
||||
}
|
||||
|
||||
export function InstanceCard({ instance, options, isTiny, isAdvanced, widgetKey, hideFooter }: InstanceCardProps) {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const tCommon = useScopedI18n("common");
|
||||
const t = useI18n("widget.coolify");
|
||||
const tCommon = useI18n("common");
|
||||
const cardKey = `${widgetKey}-${instance.integrationId}`;
|
||||
const [showIp, setShowIp] = useLocalStorage({
|
||||
key: `coolify-show-ip-${cardKey}`,
|
||||
@@ -172,7 +172,7 @@ export function InstanceCard({ instance, options, isTiny, isAdvanced, widgetKey,
|
||||
}
|
||||
|
||||
const InstanceFooter = ({ version, updatedAt }: { version: string; updatedAt: Date }) => {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const t = useI18n("widget.coolify");
|
||||
const relativeTime = useTimeAgo(updatedAt, 60_000);
|
||||
return (
|
||||
<Group
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
IconLoader2,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import actionTargetClasses from "../common/action-target.module.css";
|
||||
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url";
|
||||
@@ -35,7 +35,7 @@ interface ResourceRowProps {
|
||||
}
|
||||
|
||||
export function ResourceRow({ item, baseUrl, isTiny, resourceType }: ResourceRowProps) {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const t = useI18n("widget.coolify");
|
||||
const status = parseStatus(item.status ?? "");
|
||||
const statusColor = getStatusColor(status);
|
||||
const isTransitioning = status === "starting" || status === "restarting";
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import type { CoolifyServer } from "@homarr/integrations/types";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import actionTargetClasses from "../common/action-target.module.css";
|
||||
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url";
|
||||
@@ -36,14 +36,17 @@ export function ServersSection({
|
||||
showIp,
|
||||
onToggleIp,
|
||||
}: ServersSectionProps) {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const tCommon = useScopedI18n("common");
|
||||
const t = useI18n("widget.coolify");
|
||||
const tCommon = useI18n("common");
|
||||
const onlineServers = servers.filter(isCoolifyServerOnline).length;
|
||||
|
||||
return (
|
||||
<Accordion.Item value="servers">
|
||||
<Group gap={0} wrap="nowrap">
|
||||
<Accordion.Control icon={isTiny ? null : <IconServer size="var(--mantine-font-size-md)" />} style={{ flex: 1, minWidth: 0 }}>
|
||||
<Accordion.Control
|
||||
icon={isTiny ? null : <IconServer size="var(--mantine-font-size-md)" />}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
>
|
||||
<Group gap="xs">
|
||||
<Text size="xs">{tCommon("servers")}</Text>
|
||||
<Badge variant="dot" color={getBadgeColor(onlineServers, servers.length)} size="xs">
|
||||
@@ -101,7 +104,7 @@ interface ServerRowProps {
|
||||
}
|
||||
|
||||
function ServerRow({ server, counts, baseUrl, isTiny, isAdvanced, showIp }: ServerRowProps) {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const t = useI18n("widget.coolify");
|
||||
const isBuildServer = server.settings?.is_build_server === true;
|
||||
const reachability = getCoolifyServerState(server, "is_reachable");
|
||||
const usability = getCoolifyServerState(server, "is_usable");
|
||||
@@ -158,7 +161,7 @@ function ServerRow({ server, counts, baseUrl, isTiny, isAdvanced, showIp }: Serv
|
||||
}
|
||||
|
||||
const ServerStateBadge = ({ field, value }: { field: "reachability" | "usability"; value: boolean | undefined }) => {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const t = useI18n("widget.coolify");
|
||||
const status =
|
||||
value === undefined
|
||||
? t("status.unknown")
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Accordion, Anchor, Badge, Group, Stack, Text } from "@mantine/core";
|
||||
import { IconStack2 } from "@tabler/icons-react";
|
||||
|
||||
import type { CoolifyServiceWithContext } from "@homarr/integrations/types";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import actionTargetClasses from "../common/action-target.module.css";
|
||||
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url";
|
||||
@@ -19,8 +19,8 @@ interface ServicesSectionProps {
|
||||
}
|
||||
|
||||
export function ServicesSection({ services, baseUrl, isTiny, isAdvanced }: ServicesSectionProps) {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const tCommon = useScopedI18n("common");
|
||||
const t = useI18n("widget.coolify");
|
||||
const tCommon = useI18n("common");
|
||||
const runningServices = services.filter((svc) => parseStatus(svc.status ?? "") === "running").length;
|
||||
|
||||
return (
|
||||
@@ -65,7 +65,7 @@ export function ServicesSection({ services, baseUrl, isTiny, isAdvanced }: Servi
|
||||
type ServiceApplication = NonNullable<CoolifyServiceWithContext["applications"]>[number];
|
||||
|
||||
const ServiceApplicationRow = ({ application }: { application: ServiceApplication }) => {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const t = useI18n("widget.coolify");
|
||||
const status = parseStatus(application.status ?? "");
|
||||
const publicUrl = getSafeApplicationUrl(cleanFqdn(application.fqdn));
|
||||
const statusLabel =
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Accordion, Anchor, Group, Image, ScrollArea, Stack, Text } from "@manti
|
||||
import { useLocalStorage } from "@mantine/hooks";
|
||||
|
||||
import { useTimeAgo } from "@homarr/common";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { ApplicationsSection } from "./applications-section";
|
||||
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url";
|
||||
@@ -32,7 +32,7 @@ export function SingleInstanceLayout({
|
||||
widgetKey,
|
||||
hideFooter,
|
||||
}: SingleInstanceLayoutProps) {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const t = useI18n("widget.coolify");
|
||||
const [showIp, setShowIp] = useLocalStorage({
|
||||
key: `coolify-show-ip-${widgetKey}`,
|
||||
defaultValue: false,
|
||||
@@ -120,7 +120,7 @@ export function SingleInstanceLayout({
|
||||
}
|
||||
|
||||
const InstanceFooter = ({ version, updatedAt }: { version: string; updatedAt: Date }) => {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const t = useI18n("widget.coolify");
|
||||
const relativeTime = useTimeAgo(updatedAt, 60_000);
|
||||
return (
|
||||
<Group
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { IconBell, IconBellOff, IconHourglass } from "@tabler/icons-react";
|
||||
|
||||
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
|
||||
import {
|
||||
claimBrowserAlertOccurrence,
|
||||
@@ -59,7 +59,7 @@ export default function CountdownWidget({
|
||||
}: WidgetComponentProps<"countdown">) {
|
||||
const now = useWidgetNow(options.showSeconds ? "second" : "minute");
|
||||
const locale = useCurrentIntlLocale();
|
||||
const t = useScopedI18n("widget.countdown");
|
||||
const t = useI18n("widget.countdown");
|
||||
const storagePrefix = boardId && itemId ? `homarr:countdown:${boardId}:${itemId}` : undefined;
|
||||
const [preferences, setPreferences] = useLocalWidgetState<AlertPreferences>({
|
||||
key: storagePrefix ? `${storagePrefix}:alert-preferences` : undefined,
|
||||
@@ -228,7 +228,7 @@ const CountdownValue = ({
|
||||
showSeconds: boolean;
|
||||
compact?: boolean;
|
||||
}) => {
|
||||
const t = useScopedI18n("widget.countdown");
|
||||
const t = useI18n("widget.countdown");
|
||||
let value = t("invalid.value");
|
||||
if (state.completed) value = t("completed");
|
||||
else if (state.nextOccurrence)
|
||||
@@ -287,7 +287,7 @@ const EventSection = ({
|
||||
showProgress: boolean;
|
||||
columns: number;
|
||||
}) => {
|
||||
const t = useScopedI18n("widget.countdown");
|
||||
const t = useI18n("widget.countdown");
|
||||
if (states.length === 0) return null;
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
@@ -330,7 +330,7 @@ const AlertControls = ({
|
||||
notificationPermission: ReturnType<typeof getBrowserNotificationPermission>;
|
||||
onNotificationsChange: (enabled: boolean) => Promise<void>;
|
||||
}) => {
|
||||
const t = useScopedI18n("widget.countdown.alert");
|
||||
const t = useI18n("widget.countdown.alert");
|
||||
if (!persistent) return null;
|
||||
return (
|
||||
<Paper withBorder p="sm" radius="md">
|
||||
|
||||
@@ -6,7 +6,7 @@ import { IconAlertTriangle } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
@@ -20,8 +20,8 @@ export default function CustomApiWidget({
|
||||
isEditMode,
|
||||
removeItem,
|
||||
}: WidgetComponentProps<"customApi">) {
|
||||
const t = useScopedI18n("widget.customApi");
|
||||
const tCustomJsx = useScopedI18n("widget.customApi.customJsx");
|
||||
const t = useI18n("widget.customApi");
|
||||
const tCustomJsx = useI18n("widget.customApi.customJsx");
|
||||
const { data: session } = useSession();
|
||||
const isAdmin = session?.user.permissions.includes("admin") ?? false;
|
||||
const definitionId = options.definitionId;
|
||||
|
||||
@@ -25,7 +25,7 @@ import { SAFE_TABLER_ICON_NAMES } from "./jsx-icon-adapter";
|
||||
const CUSTOM_WIDGET_COMPONENTS = createCustomWidgetComponents({ copy: "Copy", copied: "Copied" });
|
||||
|
||||
vi.mock("@homarr/translation/client", () => ({
|
||||
useScopedI18n: () => (key: string, params?: Record<string, string>) =>
|
||||
useI18n: () => (key: string, params?: Record<string, string>) =>
|
||||
key === "templateWarnings" ? `${params?.count ?? "0"} template warning(s):` : key,
|
||||
}));
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { CustomWidgetPublishedQueryState } from "@homarr/custom-widgets/runtime";
|
||||
import { CustomJsxRenderer, parseRequestCapabilities } from "@homarr/custom-widgets/runtime";
|
||||
|
||||
@@ -12,9 +12,13 @@ import { InactiveWidgetDefinitionProvider, WidgetDefinitionProvider } from "./wi
|
||||
export { CUSTOM_JSX_METHOD_COLORS } from "@homarr/custom-widgets/runtime";
|
||||
|
||||
export default function CustomJsxDisplay({ data }: { data: Record<string, unknown> }) {
|
||||
const t = useScopedI18n("widget.customApi.customJsx");
|
||||
const t = useI18n("widget.customApi.customJsx");
|
||||
const actionT = useI18n("common.action");
|
||||
const capabilities = useMemo(() => parseRequestCapabilities(data.requestCapabilities), [data.requestCapabilities]);
|
||||
const components = useMemo(() => createCustomWidgetComponents({ copy: t("copy"), copied: t("copied") }), [t]);
|
||||
const components = useMemo(
|
||||
() => createCustomWidgetComponents({ copy: actionT("copy"), copied: t("copied") }),
|
||||
[actionT, t],
|
||||
);
|
||||
const [queryState, setQueryState] = useState<Record<string, CustomWidgetPublishedQueryState>>({});
|
||||
useEffect(
|
||||
() => setQueryState({}),
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
import { CustomWidgetRuntimeProvider } from "@homarr/custom-widgets/runtime";
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
interface WidgetDefinitionProviderProps {
|
||||
@@ -36,7 +36,8 @@ const INACTIVE_PORT: CustomWidgetRuntimePort = {
|
||||
};
|
||||
|
||||
const useRuntimeMessages = (): CustomWidgetRuntimeMessages => {
|
||||
const t = useScopedI18n("widget.customApi.customJsx");
|
||||
const t = useI18n("widget.customApi.customJsx");
|
||||
const actionT = useI18n("common.action");
|
||||
return {
|
||||
requestIdRequired: t("requestIdRequired"),
|
||||
unsavedPreview: t("unsavedPreview"),
|
||||
@@ -51,7 +52,7 @@ const useRuntimeMessages = (): CustomWidgetRuntimeMessages => {
|
||||
actionCompleted: t("actionCompleted"),
|
||||
confirmDelete: t("confirmDelete"),
|
||||
toggle: t("toggle"),
|
||||
refresh: t("refresh"),
|
||||
refresh: actionT("refresh"),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ interface TimerModalProps {
|
||||
}
|
||||
|
||||
const TimerModal = ({ opened, close, selectedIntegrationIds, disableDns }: TimerModalProps) => {
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.dnsHoleControls");
|
||||
const tCommon = useI18n("common");
|
||||
const [hours, setHours] = useState(0);
|
||||
const [minutes, setMinutes] = useState(0);
|
||||
const hoursHandlers = useRef<NumberInputHandlers>(null);
|
||||
@@ -41,12 +42,12 @@ const TimerModal = ({ opened, close, selectedIntegrationIds, disableDns }: Timer
|
||||
setHours(0);
|
||||
setMinutes(0);
|
||||
}}
|
||||
title={t("widget.dnsHoleControls.controls.setTimer")}
|
||||
title={t("controls.setTimer")}
|
||||
>
|
||||
<Flex direction="column" align="center" justify="center">
|
||||
<Stack align="flex-end">
|
||||
<Group>
|
||||
<Text>{t("widget.dnsHoleControls.controls.hours")}</Text>
|
||||
<Text>{tCommon("information.hours")}</Text>
|
||||
<ActionIcon size={35} variant="default" onClick={() => hoursHandlers.current?.decrement()}>
|
||||
–
|
||||
</ActionIcon>
|
||||
@@ -65,7 +66,7 @@ const TimerModal = ({ opened, close, selectedIntegrationIds, disableDns }: Timer
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
<Group>
|
||||
<Text>{t("widget.dnsHoleControls.controls.minutes")}</Text>
|
||||
<Text>{tCommon("information.minutes")}</Text>
|
||||
<ActionIcon size={35} variant="default" onClick={() => minutesHandlers.current?.decrement()}>
|
||||
–
|
||||
</ActionIcon>
|
||||
@@ -85,7 +86,7 @@ const TimerModal = ({ opened, close, selectedIntegrationIds, disableDns }: Timer
|
||||
</Group>
|
||||
</Stack>
|
||||
<Text ta="center" c="dimmed" my={5}>
|
||||
{t("widget.dnsHoleControls.controls.unlimited")}
|
||||
{t("controls.unlimited")}
|
||||
</Text>
|
||||
<Button
|
||||
variant="light"
|
||||
@@ -95,7 +96,7 @@ const TimerModal = ({ opened, close, selectedIntegrationIds, disableDns }: Timer
|
||||
w="12rem"
|
||||
onClick={handleSetTimer}
|
||||
>
|
||||
{t("widget.dnsHoleControls.controls.set")}
|
||||
{t("controls.set")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Modal>
|
||||
|
||||
@@ -28,7 +28,6 @@ import { useIntegrationsWithInteractAccess } from "@homarr/auth/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useIntegrationConnected } from "@homarr/common";
|
||||
import { integrationDefs } from "@homarr/definitions";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { MaskedOrNormalImage } from "@homarr/ui";
|
||||
|
||||
@@ -123,7 +122,8 @@ export default function DnsHoleControlsWidget({
|
||||
{ enabled: [] as string[], disabled: [] as string[] },
|
||||
);
|
||||
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.dnsHoleControls");
|
||||
const tCommon = useI18n("common");
|
||||
|
||||
// Timer modal setup
|
||||
const [selectedIntegrationIds, setSelectedIntegrationIds] = useState<string[]>([]);
|
||||
@@ -146,7 +146,7 @@ export default function DnsHoleControlsWidget({
|
||||
return (
|
||||
<Stack h="100%" justify="center" align="center">
|
||||
<Text c="dimmed" size="sm">
|
||||
{t("common.action.loading")}
|
||||
{tCommon("action.loading")}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
@@ -165,14 +165,14 @@ export default function DnsHoleControlsWidget({
|
||||
<Box pos="absolute" top={4} right={4} style={{ zIndex: 2 }}>
|
||||
<Group gap={0}>
|
||||
<IntegrationErrorIndicator results={summaryResults} />
|
||||
<WidgetQueryErrorIndicator error={summaryQuery.error} label={t("widget.dnsHoleControls.name")} />
|
||||
<WidgetQueryErrorIndicator error={summaryQuery.error} label={t("name")} />
|
||||
</Group>
|
||||
</Box>
|
||||
{controlAllButtonsVisible && (
|
||||
<Flex className="dns-hole-controls-buttons" gap="sm">
|
||||
<Tooltip label={t("widget.dnsHoleControls.controls.enableAll")}>
|
||||
<Tooltip label={t("controls.enableAll")}>
|
||||
<Button
|
||||
aria-label={t("widget.dnsHoleControls.controls.enableAll")}
|
||||
aria-label={t("controls.enableAll")}
|
||||
size="xs"
|
||||
p={0}
|
||||
className="dns-hole-controls-enable-all-button"
|
||||
@@ -188,9 +188,9 @@ export default function DnsHoleControlsWidget({
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label={t("widget.dnsHoleControls.controls.setTimer")}>
|
||||
<Tooltip label={t("controls.setTimer")}>
|
||||
<Button
|
||||
aria-label={t("widget.dnsHoleControls.controls.setTimer")}
|
||||
aria-label={t("controls.setTimer")}
|
||||
size="xs"
|
||||
p={0}
|
||||
className="dns-hole-controls-timer-all-button"
|
||||
@@ -209,9 +209,9 @@ export default function DnsHoleControlsWidget({
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label={t("widget.dnsHoleControls.controls.disableAll")}>
|
||||
<Tooltip label={t("controls.disableAll")}>
|
||||
<Button
|
||||
aria-label={t("widget.dnsHoleControls.controls.disableAll")}
|
||||
aria-label={t("controls.disableAll")}
|
||||
size="xs"
|
||||
p={0}
|
||||
className="dns-hole-controls-disable-all-button"
|
||||
@@ -256,13 +256,13 @@ export default function DnsHoleControlsWidget({
|
||||
|
||||
{bulkFailureCount > 0 && (
|
||||
<Text size="xs" c="red" ta="center">
|
||||
{t("widget.dnsHoleControls.error.bulkActionsFailed", { count: bulkFailureCount })}
|
||||
{t("error.bulkActionsFailed", { count: bulkFailureCount })}
|
||||
</Text>
|
||||
)}
|
||||
{actionError && (
|
||||
<Tooltip label={t("widget.dnsHoleControls.error.internalServerError")}>
|
||||
<Tooltip label={t("error.internalServerError")}>
|
||||
<Text size="xs" c="red" ta="center" lineClamp={2} tabIndex={0}>
|
||||
{t("widget.dnsHoleControls.error.internalServerError")}
|
||||
{t("error.internalServerError")}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -283,7 +283,7 @@ interface ControlsCardProps {
|
||||
data: AvailableDnsSummaryResult;
|
||||
setSelectedIntegrationIds: (integrationId: string[]) => void;
|
||||
open: () => void;
|
||||
t: TranslationFunction;
|
||||
t: ReturnType<typeof useI18n<"widget.dnsHoleControls">>;
|
||||
hasIconColor: boolean;
|
||||
rootWidth: number;
|
||||
rootHeight: number;
|
||||
@@ -371,7 +371,7 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
|
||||
{!isEnabled ? (
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
aria-label={`${t("widget.dnsHoleControls.controls.enableAll")}: ${data.integration.name}`}
|
||||
aria-label={`${t("controls.enableAll")}: ${data.integration.name}`}
|
||||
onClick={() => void toggleDns(data.integration.id).catch(() => undefined)}
|
||||
disabled={!controlEnabled}
|
||||
size="sm"
|
||||
@@ -383,7 +383,7 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
|
||||
) : (
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
aria-label={`${t("widget.dnsHoleControls.controls.disableAll")}: ${data.integration.name}`}
|
||||
aria-label={`${t("controls.disableAll")}: ${data.integration.name}`}
|
||||
onClick={() => void toggleDns(data.integration.id).catch(() => undefined)}
|
||||
disabled={!controlEnabled}
|
||||
size="sm"
|
||||
@@ -395,7 +395,7 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
|
||||
)}
|
||||
<ActionIcon
|
||||
className={actionTargetClasses.root}
|
||||
aria-label={`${t("widget.dnsHoleControls.controls.setTimer")}: ${data.integration.name}`}
|
||||
aria-label={`${t("controls.setTimer")}: ${data.integration.name}`}
|
||||
onClick={() => {
|
||||
setSelectedIntegrationIds([data.integration.id]);
|
||||
open();
|
||||
@@ -413,9 +413,7 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
|
||||
{layout === "md" && (
|
||||
<UnstyledButton
|
||||
aria-label={`${
|
||||
isEnabled
|
||||
? t("widget.dnsHoleControls.controls.disableAll")
|
||||
: t("widget.dnsHoleControls.controls.enableAll")
|
||||
isEnabled ? t("controls.disableAll") : t("controls.enableAll")
|
||||
}: ${data.integration.name}`}
|
||||
className="dns-hole-controls-item-toggle-button"
|
||||
disabled={!controlEnabled}
|
||||
@@ -442,7 +440,7 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
|
||||
}
|
||||
>
|
||||
{t(
|
||||
`widget.dnsHoleControls.controls.${
|
||||
`controls.${
|
||||
!isConnected
|
||||
? "disconnected"
|
||||
: typeof isEnabled === "undefined"
|
||||
@@ -450,7 +448,7 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
|
||||
: isEnabled
|
||||
? "enabled"
|
||||
: "disabled"
|
||||
}`,
|
||||
}` as never,
|
||||
)}
|
||||
</Badge>
|
||||
</UnstyledButton>
|
||||
@@ -459,7 +457,7 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
|
||||
</Flex>
|
||||
{layout === "md" && (
|
||||
<ActionIcon
|
||||
aria-label={`${t("widget.dnsHoleControls.controls.setTimer")}: ${data.integration.name}`}
|
||||
aria-label={`${t("controls.setTimer")}: ${data.integration.name}`}
|
||||
className={combineClasses("dns-hole-controls-item-timer-button", actionTargetClasses.root)}
|
||||
display={isInteractPermitted ? undefined : "none"}
|
||||
disabled={!controlEnabled || !isEnabled}
|
||||
|
||||
@@ -47,6 +47,9 @@ export default function DnsHoleSummaryWidget({
|
||||
const { isPending } = summaryQuery;
|
||||
|
||||
const t = useI18n();
|
||||
const tCommon = useI18n("common");
|
||||
const tDns = useI18n("widget.dnsHoleSummary");
|
||||
const tWidgetCommon = useI18n("widget.common");
|
||||
|
||||
const successfulSummaries = summaries.filter(({ summary }) => summary !== null);
|
||||
const data = successfulSummaries.flatMap(({ summary }) => (summary ? [summary] : []));
|
||||
@@ -57,7 +60,7 @@ export default function DnsHoleSummaryWidget({
|
||||
return (
|
||||
<Stack h="100%" justify="center" align="center">
|
||||
<Text c="dimmed" size="sm">
|
||||
{t("common.action.loading")}
|
||||
{tCommon("action.loading")}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
@@ -68,7 +71,7 @@ export default function DnsHoleSummaryWidget({
|
||||
<Box pos="absolute" top={4} right={4} style={{ zIndex: 2 }}>
|
||||
<Group gap={0}>
|
||||
<IntegrationErrorIndicator results={summaries} />
|
||||
<WidgetQueryErrorIndicator error={summaryQuery.error} label={t("widget.dnsHoleSummary.name")} />
|
||||
<WidgetQueryErrorIndicator error={summaryQuery.error} label={tDns("name")} />
|
||||
</Group>
|
||||
</Box>
|
||||
<SimpleGrid cols={2} spacing="xs" p="xs" {...layoutProps} style={{ ...layoutProps.style, flex: 1, minHeight: 0 }}>
|
||||
@@ -95,7 +98,7 @@ export default function DnsHoleSummaryWidget({
|
||||
))}
|
||||
</AvatarGroup>
|
||||
<Text fz="md" ta="center">
|
||||
{t("widget.dnsHoleSummary.error.integrationsDisconnected")}
|
||||
{tWidgetCommon("integrationDisconnected")}
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
@@ -110,7 +113,7 @@ export default function DnsHoleSummaryWidget({
|
||||
color={summary?.status === "enabled" ? "green" : summary?.status === "disabled" ? "red" : "gray"}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
{integration.name}: {t(`widget.dnsHoleSummary.status.${summary?.status ?? "unknown"}` as never)}
|
||||
{integration.name}: {tDns(`status.${summary?.status ?? "unknown"}` as never)}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
|
||||
@@ -20,7 +20,11 @@ export const { definition, componentLoader } = createWidgetDefinition(widgetKind
|
||||
layout: factory.select({
|
||||
options: (["grid", "row", "column"] as const).map((value) => ({
|
||||
value,
|
||||
label: (t) => t(`widget.dnsHoleSummary.option.layout.option.${value}.label`),
|
||||
label: (t) => {
|
||||
if (value === "grid") return t("widget.common.layout.option.grid");
|
||||
if (value === "row") return t("widget.common.layout.option.horizontal");
|
||||
return t("widget.common.layout.option.vertical");
|
||||
},
|
||||
})),
|
||||
defaultValue: "grid",
|
||||
}),
|
||||
|
||||
@@ -31,6 +31,7 @@ import type { DataTableColumn, DataTableSortStatus } from "mantine-datatable";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { invariantTechnicalLabels } from "@homarr/definitions";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||
import { useOptionalBoard } from "@homarr/boards/context";
|
||||
@@ -40,7 +41,7 @@ import { containerStateColorMap, cpuUsageColor, memoryUsageColor, safeValue } fr
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { AddDockerAppToHomarr, useDockerContainerRemovalConfirmation } from "@homarr/modals-collection";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
@@ -92,7 +93,7 @@ const getContainersQueryInput = (endpointIds: string[]) => {
|
||||
};
|
||||
|
||||
const ContainerStateBadge = ({ state }: { state: ContainerState }) => {
|
||||
const t = useScopedI18n("docker.field.state.option");
|
||||
const t = useI18n("docker.field.state.option");
|
||||
|
||||
return (
|
||||
<Badge
|
||||
@@ -108,7 +109,7 @@ const ContainerStateBadge = ({ state }: { state: ContainerState }) => {
|
||||
};
|
||||
|
||||
const ContainerStateDot = ({ state }: { state: ContainerState }) => {
|
||||
const t = useScopedI18n("docker.field.state.option");
|
||||
const t = useI18n("docker.field.state.option");
|
||||
const label = t(state);
|
||||
|
||||
return (
|
||||
@@ -129,14 +130,15 @@ const ContainerStateDot = ({ state }: { state: ContainerState }) => {
|
||||
};
|
||||
|
||||
const createColumns = (
|
||||
t: ReturnType<typeof useScopedI18n<"docker">>,
|
||||
t: ReturnType<typeof useI18n<"docker">>,
|
||||
tCommon: ReturnType<typeof useI18n<"common">>,
|
||||
handlers: ContainerActionHandlers,
|
||||
sortingEnabled: boolean,
|
||||
inlineState: boolean,
|
||||
): DataTableColumn<DockerContainer>[] => [
|
||||
{
|
||||
accessor: "name",
|
||||
title: t("field.name.label"),
|
||||
title: tCommon("field.name"),
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
sortable: sortingEnabled,
|
||||
@@ -179,7 +181,7 @@ const createColumns = (
|
||||
},
|
||||
{
|
||||
accessor: "cpuUsage",
|
||||
title: t("field.stats.cpu.label"),
|
||||
title: invariantTechnicalLabels.cpu,
|
||||
width: 80,
|
||||
sortable: sortingEnabled,
|
||||
render: (container) => {
|
||||
@@ -232,7 +234,7 @@ const compareContainers = (
|
||||
};
|
||||
|
||||
const useContainerAction = (action: ContainerAction) => {
|
||||
const t = useScopedI18n("docker.action");
|
||||
const t = useI18n("docker.action");
|
||||
const utils = clientApi.useUtils();
|
||||
|
||||
return clientApi.docker[`${action}All`].useMutation({
|
||||
@@ -264,8 +266,9 @@ export default function DockerWidget({
|
||||
itemId,
|
||||
setOptions,
|
||||
}: WidgetComponentProps<"dockerContainers">) {
|
||||
const t = useScopedI18n("docker");
|
||||
const tWidget = useScopedI18n("widget.dockerContainers");
|
||||
const t = useI18n("docker");
|
||||
const tCommon = useI18n("common");
|
||||
const tWidget = useI18n("widget.dockerContainers");
|
||||
const { openModal } = useModalAction(AddDockerAppToHomarr);
|
||||
const confirmRemoval = useDockerContainerRemovalConfirmation();
|
||||
const board = useOptionalBoard();
|
||||
@@ -384,10 +387,10 @@ export default function DockerWidget({
|
||||
!isAdvanced && width < 340 && options.columns.includes("name") && options.columns.includes("state");
|
||||
const columns = useMemo(() => {
|
||||
const sortingEnabled = (isAdvanced || options.enableRowSorting) && !isEditMode;
|
||||
return createColumns(t, actionHandlers, sortingEnabled, inlineState).filter(
|
||||
return createColumns(t, tCommon, actionHandlers, sortingEnabled, inlineState).filter(
|
||||
({ accessor }) => columnVisibility[String(accessor) as keyof typeof columnVisibility],
|
||||
);
|
||||
}, [actionHandlers, columnVisibility, inlineState, isAdvanced, isEditMode, options.enableRowSorting, t]);
|
||||
}, [actionHandlers, columnVisibility, inlineState, isAdvanced, isEditMode, options.enableRowSorting, t, tCommon]);
|
||||
const { effectiveColumns, storeKey } = usePersistedTableLayout({
|
||||
columns,
|
||||
columnAccessors,
|
||||
@@ -507,7 +510,7 @@ function ContainerMenuButton({
|
||||
container: DockerContainer;
|
||||
handlers: ContainerActionHandlers;
|
||||
}) {
|
||||
const t = useScopedI18n("docker.action");
|
||||
const t = useI18n("docker.action");
|
||||
const [opened, setOpened] = useState(false);
|
||||
const close = () => setOpened(false);
|
||||
|
||||
@@ -548,7 +551,8 @@ function ContainerActionItems({
|
||||
handlers: ContainerActionHandlers;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const t = useScopedI18n("docker.action");
|
||||
const t = useI18n("docker.action");
|
||||
const tCommon = useI18n("common");
|
||||
const stateAction = container.state === "running" ? "stop" : "start";
|
||||
const StateIcon = stateAction === "stop" ? IconPlayerStop : IconPlayerPlay;
|
||||
|
||||
@@ -595,7 +599,7 @@ function ContainerActionItems({
|
||||
disabled={!handlers.canUse(container, "remove")}
|
||||
onClick={() => invokeAction("remove")}
|
||||
>
|
||||
{t("remove.label")}
|
||||
{tCommon("action.remove")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
|
||||
@@ -2,6 +2,7 @@ import { IconBrandDocker, IconServerOff } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { invariantTechnicalLabels } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
@@ -16,13 +17,12 @@ const columnsList = [
|
||||
const allColumnsList = ["name", "state", "host", "cpuUsage", "memoryUsage", "actions"] as const;
|
||||
|
||||
const columnTranslationKeyMap = {
|
||||
name: "docker.field.name.label",
|
||||
name: "common.field.name",
|
||||
state: "docker.field.state.label",
|
||||
host: "docker.field.host.label",
|
||||
cpuUsage: "docker.field.stats.cpu.label",
|
||||
memoryUsage: "docker.field.stats.memory.label",
|
||||
actions: "docker.action.title",
|
||||
} as const satisfies Record<(typeof allColumnsList)[number], string>;
|
||||
} as const satisfies Record<Exclude<(typeof allColumnsList)[number], "cpuUsage">, string>;
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("dockerContainers", {
|
||||
icon: IconBrandDocker,
|
||||
@@ -49,7 +49,10 @@ export const { definition, componentLoader } = createWidgetDefinition("dockerCon
|
||||
defaultValue: [...allColumnsList],
|
||||
options: allColumnsList.map((value) => ({
|
||||
value,
|
||||
label: (t) => t(columnTranslationKeyMap[value]),
|
||||
label: (t) => {
|
||||
if (value === "cpuUsage") return invariantTechnicalLabels.cpu;
|
||||
return t(columnTranslationKeyMap[value]);
|
||||
},
|
||||
})),
|
||||
searchable: true,
|
||||
}),
|
||||
|
||||
@@ -50,7 +50,7 @@ import { formatByteRate, formatBytes, useIntegrationConnected } from "@homarr/co
|
||||
import { getIconUrl, getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import type { ExtendedClientStatus, ExtendedDownloadClientItem } from "@homarr/integrations";
|
||||
import { showErrorNotification } from "@homarr/notifications";
|
||||
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { IntegrationErrorIndicator } from "../common/integration-error-indicator";
|
||||
@@ -69,7 +69,7 @@ import {
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
type DownloadsT = ReturnType<typeof useScopedI18n<"widget.downloads">>;
|
||||
type DownloadsT = ReturnType<typeof useI18n<"widget.downloads">>;
|
||||
type DownloadState = ExtendedDownloadClientItem["state"];
|
||||
|
||||
interface SizeConfig {
|
||||
@@ -315,7 +315,7 @@ export default function DownloadClientsWidget({
|
||||
const availableItems = useMemo(() => currentItems?.filter((item) => item.data !== null) ?? [], [currentItems]);
|
||||
const { isFetching } = downloadsQuery;
|
||||
|
||||
const t = useScopedI18n("widget.downloads");
|
||||
const t = useI18n("widget.downloads");
|
||||
const queryIndicators = (
|
||||
<Group gap={0}>
|
||||
<IntegrationErrorIndicator results={currentItems ?? []} />
|
||||
@@ -793,9 +793,7 @@ export default function DownloadClientsWidget({
|
||||
animateOpacity: true,
|
||||
transitionTimingFunction: "ease-out",
|
||||
},
|
||||
content: ({ record, collapse }) => (
|
||||
<ExpandedRow item={record} collapse={collapse} />
|
||||
),
|
||||
content: ({ record, collapse }) => <ExpandedRow item={record} collapse={collapse} />,
|
||||
}}
|
||||
onScroll={() => {
|
||||
if (contextMenu) closeContextMenu();
|
||||
@@ -871,14 +869,8 @@ function buildHoverTooltip(record: ExtendedDownloadClientItem, t: DownloadsT): R
|
||||
);
|
||||
}
|
||||
|
||||
function ExpandedRow({
|
||||
item,
|
||||
collapse,
|
||||
}: {
|
||||
item: ExtendedDownloadClientItem;
|
||||
collapse: () => void;
|
||||
}) {
|
||||
const t = useScopedI18n("widget.downloads");
|
||||
function ExpandedRow({ item, collapse }: { item: ExtendedDownloadClientItem; collapse: () => void }) {
|
||||
const t = useI18n("widget.downloads");
|
||||
const locale = useCurrentIntlLocale();
|
||||
const progressPercent = Math.floor(item.progress * 100);
|
||||
const categoryDisplay = formatCategoryDisplay(item.category);
|
||||
@@ -994,7 +986,7 @@ function GlobalStatsBar({
|
||||
hasTorrents: boolean;
|
||||
clients: ExtendedClientStatus[];
|
||||
}) {
|
||||
const t = useScopedI18n("widget.downloads");
|
||||
const t = useI18n("widget.downloads");
|
||||
|
||||
let overallProgress = 0;
|
||||
if (queueStats.totalSize > 0) overallProgress = queueStats.completedSize / queueStats.totalSize;
|
||||
@@ -1214,7 +1206,7 @@ function WidgetFooter({
|
||||
showStats,
|
||||
toggleStats,
|
||||
}: WidgetFooterProps) {
|
||||
const t = useScopedI18n("widget.downloads");
|
||||
const t = useI18n("widget.downloads");
|
||||
const [filterOpen, { toggle: toggleFilter }] = useDisclosure(false);
|
||||
const someInteract = clients.some(({ interact }) => interact);
|
||||
const hasActiveFilter = clientFilter.length > 0 || statusFilter.length > 0;
|
||||
@@ -1372,12 +1364,18 @@ function WidgetFooter({
|
||||
{!showStats && (
|
||||
<Group gap={2}>
|
||||
<Text size="xs" fw={600} c="blue">
|
||||
<IconDownload size="var(--mantine-font-size-xs)" style={{ verticalAlign: "middle", marginRight: 2 }} />
|
||||
<IconDownload
|
||||
size="var(--mantine-font-size-xs)"
|
||||
style={{ verticalAlign: "middle", marginRight: 2 }}
|
||||
/>
|
||||
{formatByteRate(totalSpeed)}
|
||||
</Text>
|
||||
{totalUpSpeed > 0 && (
|
||||
<Text size="xs" fw={600} c="green">
|
||||
<IconUpload size="var(--mantine-font-size-xs)" style={{ verticalAlign: "middle", marginRight: 2 }} />
|
||||
<IconUpload
|
||||
size="var(--mantine-font-size-xs)"
|
||||
style={{ verticalAlign: "middle", marginRight: 2 }}
|
||||
/>
|
||||
{formatByteRate(totalUpSpeed)}
|
||||
</Text>
|
||||
)}
|
||||
@@ -1409,7 +1407,7 @@ function ClientIndicator({ integration }: { integration: ExtendedClientStatus["i
|
||||
const isConnected = useIntegrationConnected(integration.updatedAt, { timeout: 30000 });
|
||||
|
||||
let tooltipLabel = integration.name;
|
||||
const t = useScopedI18n("widget.downloads");
|
||||
const t = useI18n("widget.downloads");
|
||||
if (!isConnected) tooltipLabel = `${integration.name} ${t("disconnected")}`;
|
||||
|
||||
let avatarFilter: string | undefined;
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface BaseWidgetErrorProps {
|
||||
|
||||
export const BaseWidgetError = (props: BaseWidgetErrorProps) => {
|
||||
const t = useI18n();
|
||||
const tCommon = useI18n("common");
|
||||
const { data: session } = useSession();
|
||||
const [errorTimestamp] = useState(Date.now);
|
||||
|
||||
@@ -33,13 +34,13 @@ export const BaseWidgetError = (props: BaseWidgetErrorProps) => {
|
||||
ta="center"
|
||||
size="sm"
|
||||
>
|
||||
{t("common.action.checkLogs")}
|
||||
{tCommon("action.checkLogs")}
|
||||
</Anchor>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Button onClick={props.onRetry} size="sm" variant="light">
|
||||
{t("common.action.tryAgain")}
|
||||
{tCommon("action.tryAgain")}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -19,8 +19,8 @@ import { useLocalStorage } from "@mantine/hooks";
|
||||
import { IconArrowBarDown, IconArrowBarUp, IconBrain, IconCpu, IconTopologyBus } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { invariantTechnicalLabels } from "@homarr/definitions";
|
||||
import type { FirewallInterfacesSummary } from "@homarr/integrations";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
@@ -67,7 +67,8 @@ export default function FirewallWidget({
|
||||
const isTiny = !isAdvanced && (width < 256 || height < 180);
|
||||
const ringSize = isAdvanced ? 100 : height < 120 ? 44 : isTiny ? 64 : 100;
|
||||
const showInterfaces = isAdvanced || (!isTiny && height >= 120);
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.firewall");
|
||||
const tCommon = useI18n("common");
|
||||
|
||||
const handleSelect = useCallback((value: string | null) => {
|
||||
setSelectedFirewall(value ?? "");
|
||||
@@ -96,7 +97,7 @@ export default function FirewallWidget({
|
||||
});
|
||||
|
||||
if (hasTotalFailure) {
|
||||
throw new Error(t("widget.firewall.error.internalServerError"));
|
||||
throw new Error(t("error.internalServerError"));
|
||||
}
|
||||
|
||||
const firewallMetadata = new Map(
|
||||
@@ -119,7 +120,7 @@ export default function FirewallWidget({
|
||||
return (
|
||||
<Center h="100%" p="sm">
|
||||
<Text size="sm" c="dimmed" ta="center">
|
||||
{isLoading ? t("common.action.loading") : t("widget.firewall.empty.noInterfaces")}
|
||||
{isLoading ? tCommon("action.loading") : t("empty.noInterfaces")}
|
||||
</Text>
|
||||
</Center>
|
||||
);
|
||||
@@ -138,9 +139,9 @@ export default function FirewallWidget({
|
||||
/>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
{activeFirewall && firewallHasError(activeFirewall) && (
|
||||
<Tooltip label={t("widget.firewall.error.internalServerError")}>
|
||||
<Tooltip label={t("error.internalServerError")}>
|
||||
<Badge color="red" variant="light" size="xs">
|
||||
{t("common.error")}
|
||||
{tCommon("error")}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -179,12 +180,13 @@ export default function FirewallWidget({
|
||||
showInterfaces={showInterfaces}
|
||||
accordionValue={accordionValue}
|
||||
setAccordionValue={setAccordionValue}
|
||||
errorLabel={t("widget.firewall.error.internalServerError")}
|
||||
noDataLabel={t("widget.firewall.empty.noInterfaces")}
|
||||
loadingLabel={t("common.action.loading")}
|
||||
errorBadgeLabel={t("common.error")}
|
||||
interfacesLabel={t("widget.firewall.widget.interfaces.title")}
|
||||
errorLabel={t("error.internalServerError")}
|
||||
noDataLabel={t("empty.noInterfaces")}
|
||||
loadingLabel={tCommon("action.loading")}
|
||||
errorBadgeLabel={tCommon("error")}
|
||||
interfacesLabel={t("widget.interfaces.title")}
|
||||
t={t}
|
||||
tCommon={tCommon}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -214,7 +216,8 @@ interface FirewallPanelProps {
|
||||
loadingLabel: string;
|
||||
errorBadgeLabel: string;
|
||||
interfacesLabel: string;
|
||||
t: TranslationFunction;
|
||||
t: ReturnType<typeof useI18n<"widget.firewall">>;
|
||||
tCommon: ReturnType<typeof useI18n<"common">>;
|
||||
}
|
||||
|
||||
const FirewallPanel = ({
|
||||
@@ -238,6 +241,7 @@ const FirewallPanel = ({
|
||||
errorBadgeLabel,
|
||||
interfacesLabel,
|
||||
t,
|
||||
tCommon,
|
||||
}: FirewallPanelProps) => (
|
||||
<Paper withBorder={isAdvanced} radius="sm" p={isAdvanced ? "xs" : 0}>
|
||||
<Stack gap="xs">
|
||||
@@ -257,12 +261,12 @@ const FirewallPanel = ({
|
||||
{hasError && (
|
||||
<Tooltip label={errorLabel}>
|
||||
<Badge color="red" variant="light" size="xs">
|
||||
{t("common.error")}
|
||||
{tCommon("error")}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Badge variant="outline" color="gray" size="xs">
|
||||
{formatVersion(version ?? "", t("widget.firewall.versionUnknown"))}
|
||||
{formatVersion(version ?? "", t("versionUnknown"))}
|
||||
</Badge>
|
||||
</Group>
|
||||
</Group>
|
||||
@@ -270,16 +274,10 @@ const FirewallPanel = ({
|
||||
|
||||
<Flex justify="center" align="center" wrap="wrap">
|
||||
{cpu !== undefined && (
|
||||
<MetricRing value={cpu} icon={IconCpu} size={ringSize} label={t("widget.firewall.widget.cpu")} t={t} />
|
||||
<MetricRing value={cpu} icon={IconCpu} size={ringSize} label={invariantTechnicalLabels.cpu} t={t} />
|
||||
)}
|
||||
{memory !== undefined && (
|
||||
<MetricRing
|
||||
value={memory}
|
||||
icon={IconBrain}
|
||||
size={ringSize}
|
||||
label={t("widget.firewall.widget.memory")}
|
||||
t={t}
|
||||
/>
|
||||
<MetricRing value={memory} icon={IconBrain} size={ringSize} label={t("widget.memory")} t={t} />
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
@@ -307,19 +305,19 @@ interface MetricRingProps {
|
||||
icon: typeof IconCpu;
|
||||
size: number;
|
||||
label: string;
|
||||
t: TranslationFunction;
|
||||
t: ReturnType<typeof useI18n<"widget.firewall">>;
|
||||
}
|
||||
|
||||
const MetricRing = ({ value, icon: Icon, size, label, t }: MetricRingProps) => {
|
||||
const safeValue = Math.max(0, Math.min(100, Number.isFinite(value) ? value : 0));
|
||||
const status = getMetricStatus(safeValue);
|
||||
const statusLabel = t(`widget.firewall.status.${status}`);
|
||||
const statusLabel = t(`status.${status}` as never);
|
||||
const showIcon = size >= 64;
|
||||
const showStatus = size >= 96;
|
||||
|
||||
return (
|
||||
<RingProgress
|
||||
aria-label={t("widget.firewall.metricAccessible", {
|
||||
aria-label={t("metricAccessible", {
|
||||
metric: label,
|
||||
value: safeValue.toFixed(1),
|
||||
status: statusLabel,
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface FirewallIntegration {
|
||||
}
|
||||
|
||||
export const FirewallVersion = ({ firewallsVersionData, selectedFirewall, isTiny }: FirewallVersionProps) => {
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.firewall");
|
||||
const version = firewallsVersionData.find(({ integration }) => integration.id === selectedFirewall)?.summary.version;
|
||||
|
||||
return (
|
||||
@@ -33,7 +33,7 @@ export const FirewallVersion = ({ firewallsVersionData, selectedFirewall, isTiny
|
||||
style={{ minHeight: "24px", maxWidth: isTiny ? 72 : undefined }}
|
||||
styles={{ label: { overflow: "hidden", textOverflow: "ellipsis" } }}
|
||||
>
|
||||
{formatVersion(version ?? "", t("widget.firewall.versionUnknown"))}
|
||||
{formatVersion(version ?? "", t("versionUnknown"))}
|
||||
</Badge>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Accordion, Box, Center, Flex, Group, RingProgress, Stack, Text } from "
|
||||
import { IconBrain, IconCpu, IconCube, IconDatabase, IconDeviceLaptop, IconServer } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { invariantTechnicalLabels } from "@homarr/definitions";
|
||||
import type { Resource } from "@homarr/integrations/types";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
@@ -39,7 +40,7 @@ export const ClusterHealthMonitoring = ({
|
||||
width,
|
||||
displayMode,
|
||||
}: WidgetComponentProps<"healthMonitoring"> & { integrationId: string }) => {
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.healthMonitoring");
|
||||
const healthQuery = clientApi.widget.healthMonitoring.getClusterHealthStatus.useQuery({ integrationId });
|
||||
const healthData = getUsableWidgetQueryData(healthQuery);
|
||||
const visibleSections = getClusterVisibleSections(displayMode, options.visibleClusterSections);
|
||||
@@ -71,7 +72,7 @@ export const ClusterHealthMonitoring = ({
|
||||
return (
|
||||
<Stack h={isAdvanced ? "auto" : "100%"} p="xs" gap={isTiny ? "xs" : "md"} pos="relative">
|
||||
<Box pos="absolute" top={4} right={8} style={{ zIndex: 2 }}>
|
||||
<WidgetQueryErrorIndicator error={healthQuery.error} label={t("widget.healthMonitoring.name")} />
|
||||
<WidgetQueryErrorIndicator error={healthQuery.error} label={t("name")} />
|
||||
</Box>
|
||||
{(isAdvanced || options.showUptime) && !isTiny && (
|
||||
<Group justify="center" wrap="nowrap">
|
||||
@@ -102,7 +103,7 @@ export const ClusterHealthMonitoring = ({
|
||||
{visibleSections.includes("node") && (
|
||||
<ResourceAccordionItem
|
||||
value="node"
|
||||
title={t("widget.healthMonitoring.cluster.resource.node.name")}
|
||||
title={t("cluster.resource.node.name")}
|
||||
icon={IconServer}
|
||||
badge={addBadgeColor({
|
||||
activeCount: activeNodes,
|
||||
@@ -118,7 +119,7 @@ export const ClusterHealthMonitoring = ({
|
||||
{visibleSections.includes("qemu") && (
|
||||
<ResourceAccordionItem
|
||||
value="qemu"
|
||||
title={t("widget.healthMonitoring.cluster.resource.qemu.name")}
|
||||
title={t("cluster.resource.qemu.name")}
|
||||
icon={IconDeviceLaptop}
|
||||
badge={addBadgeColor({
|
||||
activeCount: activeVMs,
|
||||
@@ -134,7 +135,7 @@ export const ClusterHealthMonitoring = ({
|
||||
{visibleSections.includes("lxc") && (
|
||||
<ResourceAccordionItem
|
||||
value="lxc"
|
||||
title={t("widget.healthMonitoring.cluster.resource.lxc.name")}
|
||||
title={t("cluster.resource.lxc.name")}
|
||||
icon={IconCube}
|
||||
badge={addBadgeColor({
|
||||
activeCount: activeLXCs,
|
||||
@@ -150,7 +151,7 @@ export const ClusterHealthMonitoring = ({
|
||||
{visibleSections.includes("storage") && (
|
||||
<ResourceAccordionItem
|
||||
value="storage"
|
||||
title={t("widget.healthMonitoring.cluster.resource.storage.name")}
|
||||
title={t("cluster.resource.storage.name")}
|
||||
icon={IconDatabase}
|
||||
badge={addBadgeColor({
|
||||
activeCount: activeStorage,
|
||||
@@ -175,8 +176,6 @@ interface SummaryHeaderProps {
|
||||
}
|
||||
|
||||
const SummaryHeader = ({ cpu, memory, isTiny }: SummaryHeaderProps) => {
|
||||
const t = useI18n();
|
||||
|
||||
if (cpu.hidden && memory.hidden) return null;
|
||||
|
||||
return (
|
||||
@@ -198,7 +197,7 @@ const SummaryHeader = ({ cpu, memory, isTiny }: SummaryHeaderProps) => {
|
||||
<Stack align="center" justify="center" gap={0}>
|
||||
{!isTiny && (
|
||||
<Text fw={500} size="sm">
|
||||
{t("widget.healthMonitoring.cluster.summary.cpu")}
|
||||
{invariantTechnicalLabels.cpu}
|
||||
</Text>
|
||||
)}
|
||||
<Text size="xs">{cpu.value.toFixed(1)}%</Text>
|
||||
@@ -221,7 +220,7 @@ const SummaryHeader = ({ cpu, memory, isTiny }: SummaryHeaderProps) => {
|
||||
<Stack align="center" justify="center" gap={0}>
|
||||
{!isTiny && (
|
||||
<Text size="sm" fw={500}>
|
||||
{t("widget.healthMonitoring.cluster.summary.memory")}
|
||||
{invariantTechnicalLabels.ram}
|
||||
</Text>
|
||||
)}
|
||||
<Text size="xs">{memory.value.toFixed(1)}%</Text>
|
||||
|
||||
@@ -18,7 +18,7 @@ import duration from "dayjs/plugin/duration";
|
||||
|
||||
import { capitalize, formatBytes, formatBytesPair } from "@homarr/common";
|
||||
import type { ComputeResource, Resource, StorageResource } from "@homarr/integrations/types";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
dayjs.extend(duration);
|
||||
|
||||
@@ -46,7 +46,7 @@ export const ResourcePopover = ({ item, children }: PropsWithChildren<ResourcePo
|
||||
};
|
||||
|
||||
export const ResourceTypeEntryDetails = ({ item }: { item: Resource }) => {
|
||||
const t = useScopedI18n("widget.healthMonitoring.cluster.popover");
|
||||
const t = useI18n("widget.healthMonitoring.cluster.popover");
|
||||
return (
|
||||
<Stack gap={0}>
|
||||
<Group wrap="nowrap" align="start" justify="apart">
|
||||
@@ -92,7 +92,7 @@ const RightSection = ({ label, value }: RightSectionProps) => {
|
||||
};
|
||||
|
||||
const ComputeResourceDetails = ({ item }: { item: ComputeResource }) => {
|
||||
const t = useScopedI18n("widget.healthMonitoring.cluster.popover.detail");
|
||||
const t = useI18n("widget.healthMonitoring.cluster.popover.detail");
|
||||
const memory = formatBytesPair(item.memory.used, item.memory.total);
|
||||
const storage = formatBytesPair(item.storage.used, item.storage.total);
|
||||
return (
|
||||
@@ -121,7 +121,7 @@ const ComputeResourceDetails = ({ item }: { item: ComputeResource }) => {
|
||||
};
|
||||
|
||||
const StorageResourceDetails = ({ item }: { item: StorageResource }) => {
|
||||
const t = useScopedI18n("widget.healthMonitoring.cluster.popover.detail");
|
||||
const t = useI18n("widget.healthMonitoring.cluster.popover.detail");
|
||||
const storagePercent = item.total ? (item.used / item.total) * 100 : 0;
|
||||
const storage = formatBytesPair(item.used, item.total);
|
||||
return (
|
||||
@@ -188,7 +188,7 @@ const NetStats = ({ item }: { item: ComputeResource }) => {
|
||||
};
|
||||
|
||||
const StorageType = ({ item }: { item: StorageResource }) => {
|
||||
const t = useScopedI18n("widget.healthMonitoring.cluster.popover.detail.storageType");
|
||||
const t = useI18n("widget.healthMonitoring.cluster.popover.detail.storageType");
|
||||
if (item.isShared) {
|
||||
return <Badge color="blue">{t("shared")}</Badge>;
|
||||
} else {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
|
||||
import { invariantTechnicalLabels } from "@homarr/definitions";
|
||||
import type { Resource } from "@homarr/integrations/types";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
@@ -23,27 +24,27 @@ interface ResourceTableProps {
|
||||
}
|
||||
|
||||
export const ResourceTable = ({ type, data, isTiny }: ResourceTableProps) => {
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.healthMonitoring");
|
||||
return (
|
||||
<Table highlightOnHover>
|
||||
<TableThead>
|
||||
<TableTr fz="xs">
|
||||
<TableTh ta="start" p={0}>
|
||||
{t("widget.healthMonitoring.cluster.table.header.name")}
|
||||
{t("cluster.table.header.name")}
|
||||
</TableTh>
|
||||
{!isTiny && type !== "storage" ? (
|
||||
<TableTh ta="start" p={0}>
|
||||
{t("widget.healthMonitoring.cluster.table.header.cpu")}
|
||||
{invariantTechnicalLabels.cpu}
|
||||
</TableTh>
|
||||
) : null}
|
||||
{!isTiny && type !== "storage" ? (
|
||||
<TableTh ta="start" p={0}>
|
||||
{t("widget.healthMonitoring.cluster.table.header.memory")}
|
||||
{invariantTechnicalLabels.ram}
|
||||
</TableTh>
|
||||
) : null}
|
||||
{!isTiny && type === "storage" ? (
|
||||
<TableTh ta="start" p={0}>
|
||||
{t("widget.healthMonitoring.cluster.table.header.node")}
|
||||
{t("cluster.table.header.node")}
|
||||
</TableTh>
|
||||
) : null}
|
||||
</TableTr>
|
||||
|
||||
@@ -17,7 +17,7 @@ dayjs.extend(duration);
|
||||
|
||||
export default function HealthMonitoringWidget(props: WidgetComponentProps<"healthMonitoring">) {
|
||||
const integrations = getUsableWidgetQueryData(clientApi.integration.byIds.useQuery(props.integrationIds)) ?? [];
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.healthMonitoring");
|
||||
|
||||
const { clusterIntegrationIds, systemIntegrationIds } = partitionHealthMonitoringIntegrations(integrations);
|
||||
|
||||
@@ -68,10 +68,10 @@ export default function HealthMonitoringWidget(props: WidgetComponentProps<"heal
|
||||
<Tabs defaultValue={props.options.defaultTab} variant="outline">
|
||||
<Tabs.List grow>
|
||||
<Tabs.Tab value="system" fz="xs">
|
||||
<b>{t("widget.healthMonitoring.tab.system")}</b>
|
||||
<b>{t("tab.system")}</b>
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="cluster" fz="xs">
|
||||
<b>{t("widget.healthMonitoring.tab.cluster")}</b>
|
||||
<b>{t("tab.cluster")}</b>
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value="system">
|
||||
|
||||
@@ -38,7 +38,7 @@ import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { formatBytes } from "@homarr/common";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import type { ScopedTranslationFunction } from "@homarr/translation";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { filterStorageVolumes, normalizeStorageDeviceName } from "../filter-storage-volumes";
|
||||
@@ -63,7 +63,7 @@ export const SystemHealthMonitoring = ({
|
||||
displayMode,
|
||||
withScrollArea = true,
|
||||
}: WidgetComponentProps<"healthMonitoring"> & { withScrollArea?: boolean }) => {
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.healthMonitoring");
|
||||
const healthQuery = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery({ integrationIds });
|
||||
const healthResults = getUsableWidgetQueryData(healthQuery) ?? [];
|
||||
const healthData = healthResults.filter(
|
||||
@@ -81,7 +81,7 @@ export const SystemHealthMonitoring = ({
|
||||
const queryIndicators = (
|
||||
<Group gap={0}>
|
||||
<IntegrationErrorIndicator results={healthResults} />
|
||||
<WidgetQueryErrorIndicator error={healthQuery.error} label={t("widget.healthMonitoring.name")} />
|
||||
<WidgetQueryErrorIndicator error={healthQuery.error} label={t("name")} />
|
||||
</Group>
|
||||
);
|
||||
|
||||
@@ -155,7 +155,7 @@ export const SystemHealthMonitoring = ({
|
||||
size="sm"
|
||||
radius={board.itemRadius}
|
||||
onClick={() => setOpenedIntegrationId(integrationId)}
|
||||
aria-label={t("widget.healthMonitoring.popover.information")}
|
||||
aria-label={t("popover.information")}
|
||||
>
|
||||
<IconInfoCircle className="health-monitoring-information-icon" size={30} />
|
||||
</ActionIcon>
|
||||
@@ -164,7 +164,7 @@ export const SystemHealthMonitoring = ({
|
||||
opened={openedIntegrationId === integrationId}
|
||||
onClose={() => setOpenedIntegrationId(null)}
|
||||
size="auto"
|
||||
title={t("widget.healthMonitoring.popover.information")}
|
||||
title={t("popover.information")}
|
||||
centered
|
||||
>
|
||||
<Stack gap="10px" className="health-monitoring-modal-stack">
|
||||
@@ -253,10 +253,10 @@ export const SystemHealthMonitoring = ({
|
||||
</Progress.Root>
|
||||
<Group justify="space-between" gap={8} wrap="nowrap">
|
||||
<Text className="health-monitoring-disk-use-value" size="xs" c="dimmed">
|
||||
{t("widget.healthMonitoring.popover.used")} {formatFileSize(disk.used)}
|
||||
{t("popover.used")} {formatFileSize(disk.used)}
|
||||
</Text>
|
||||
<Text className="health-monitoring-disk-available-value" size="xs" c="dimmed">
|
||||
{formatFileSize(disk.available)} {t("widget.healthMonitoring.popover.available")}
|
||||
{formatFileSize(disk.available)} {t("popover.available")}
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
@@ -283,7 +283,7 @@ const SystemInformationList = ({
|
||||
}: {
|
||||
healthInfo: HealthInfo;
|
||||
memoryUsage: ReturnType<typeof formatMemoryUsage>;
|
||||
t: TranslationFunction;
|
||||
t: ScopedTranslationFunction<"widget.healthMonitoring">;
|
||||
compact?: boolean;
|
||||
}) => (
|
||||
<List
|
||||
@@ -293,48 +293,46 @@ const SystemInformationList = ({
|
||||
size={compact ? "sm" : undefined}
|
||||
>
|
||||
<List.Item className="health-monitoring-information-processor" icon={<IconCpu2 size={compact ? 18 : 30} />}>
|
||||
{t("widget.healthMonitoring.popover.processor", { cpuModelName: healthInfo.cpuModelName })}
|
||||
{t("popover.processor", { cpuModelName: healthInfo.cpuModelName })}
|
||||
</List.Item>
|
||||
<List.Item className="health-monitoring-information-memory" icon={<IconBrain size={compact ? 18 : 30} />}>
|
||||
{t("widget.healthMonitoring.popover.memory", { memory: memoryUsage.memTotal.GB })}
|
||||
{t("popover.memory", { memory: memoryUsage.memTotal.GB })}
|
||||
</List.Item>
|
||||
<List.Item className="health-monitoring-information-memory" icon={<IconBrain size={compact ? 18 : 30} />}>
|
||||
{t("widget.healthMonitoring.popover.memoryAvailable", {
|
||||
{t("popover.memoryAvailable", {
|
||||
memoryAvailable: memoryUsage.memFree.GB,
|
||||
percent: String(memoryUsage.memFree.percent),
|
||||
})}
|
||||
</List.Item>
|
||||
<List.Item className="health-monitoring-information-version" icon={<IconVersions size={compact ? 18 : 30} />}>
|
||||
{t("widget.healthMonitoring.popover.version", { version: healthInfo.version })}
|
||||
{t("popover.version", { version: healthInfo.version })}
|
||||
</List.Item>
|
||||
<List.Item className="health-monitoring-information-uptime" icon={<IconClock size={compact ? 18 : 30} />}>
|
||||
{formatUptime(healthInfo.uptime, t)}
|
||||
</List.Item>
|
||||
{healthInfo.loadAverage && (
|
||||
<List.Item className="health-monitoring-information-load-average" icon={<IconCpu size={compact ? 18 : 30} />}>
|
||||
{t("widget.healthMonitoring.popover.loadAverage")}: {healthInfo.loadAverage["1min"]}% /{" "}
|
||||
{healthInfo.loadAverage["5min"]}% / {healthInfo.loadAverage["15min"]}%
|
||||
{t("popover.loadAverage")}: {healthInfo.loadAverage["1min"]}% / {healthInfo.loadAverage["5min"]}% /{" "}
|
||||
{healthInfo.loadAverage["15min"]}%
|
||||
</List.Item>
|
||||
)}
|
||||
<List.Item className="health-monitoring-information-updates" icon={<IconPackages size={compact ? 18 : 30} />}>
|
||||
{t("widget.healthMonitoring.popover.updatesAvailable", { count: healthInfo.availablePkgUpdates })}
|
||||
{t("popover.updatesAvailable", { count: healthInfo.availablePkgUpdates })}
|
||||
</List.Item>
|
||||
<List.Item className="health-monitoring-information-reboot" icon={<IconRefreshAlert size={compact ? 18 : 30} />}>
|
||||
{healthInfo.rebootRequired
|
||||
? t("widget.healthMonitoring.popover.rebootRequired")
|
||||
: t("widget.healthMonitoring.popover.rebootNotRequired")}
|
||||
{healthInfo.rebootRequired ? t("popover.rebootRequired") : t("popover.rebootNotRequired")}
|
||||
</List.Item>
|
||||
</List>
|
||||
);
|
||||
|
||||
export const formatUptime = (uptimeInSeconds: number, t: TranslationFunction) => {
|
||||
export const formatUptime = (uptimeInSeconds: number, t: ScopedTranslationFunction<"widget.healthMonitoring">) => {
|
||||
const uptimeDuration = dayjs.duration(uptimeInSeconds, "seconds");
|
||||
const months = uptimeDuration.months();
|
||||
const days = uptimeDuration.days();
|
||||
const hours = uptimeDuration.hours();
|
||||
const minutes = uptimeDuration.minutes();
|
||||
|
||||
return t("widget.healthMonitoring.popover.uptime", {
|
||||
return t("popover.uptime", {
|
||||
months: String(months),
|
||||
days: String(days),
|
||||
hours: String(hours),
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getSafeApplicationUrl } from "../common/application-url";
|
||||
import classes from "./component.module.css";
|
||||
|
||||
export default function IFrameWidget({ options, isEditMode }: WidgetComponentProps<"iframe">) {
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.iframe");
|
||||
const { embedUrl, allowScrolling, ...permissions } = options;
|
||||
const safeEmbedUrl = getSafeApplicationUrl(embedUrl);
|
||||
const allowedPermissions = getAllowedPermissions(permissions);
|
||||
@@ -35,7 +35,7 @@ export default function IFrameWidget({ options, isEditMode }: WidgetComponentPro
|
||||
scrolling={allowScrolling ? "yes" : "no"}
|
||||
sandbox={sandboxFlags.join(" ")}
|
||||
>
|
||||
<Text>{t("widget.iframe.error.noBrowerSupport")}</Text>
|
||||
<Text>{t("error.noBrowerSupport")}</Text>
|
||||
</iframe>
|
||||
</Box>
|
||||
</Stack>
|
||||
@@ -49,24 +49,24 @@ export const isSupportedProtocol = (url: string) => {
|
||||
};
|
||||
|
||||
const NoUrl = () => {
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.iframe");
|
||||
|
||||
return (
|
||||
<Stack align="center" justify="center" h="100%">
|
||||
<IconBrowserOff />
|
||||
<Title order={4}>{t("widget.iframe.error.noUrl")}</Title>
|
||||
<Title order={4}>{t("error.noUrl")}</Title>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const UnsupportedProtocol = () => {
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.iframe");
|
||||
|
||||
return (
|
||||
<Stack align="center" justify="center" h="100%">
|
||||
<IconProtocol />
|
||||
<Title order={4} ta="center">
|
||||
{t("widget.iframe.error.unsupportedProtocol", {
|
||||
{t("error.unsupportedProtocol", {
|
||||
supportedProtocols: supportedProtocols.map((protocol) => protocol).join(", "),
|
||||
})}
|
||||
</Title>
|
||||
|
||||
@@ -28,7 +28,7 @@ export const { definition, componentLoader } = createWidgetDefinition("iframe",
|
||||
? [
|
||||
{
|
||||
key: "open-iframe",
|
||||
label: "widget.app.option.openInNewTab.label",
|
||||
label: "widget.common.openInNewTab.label",
|
||||
icon: IconExternalLink,
|
||||
onClick: () => window.open(embedUrl, "_blank", "noopener,noreferrer"),
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useCurrentIntlLocale, useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../../common/empty-state";
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
@@ -32,7 +32,7 @@ export default function ImmichAlbumCarouselWidget({
|
||||
}: WidgetComponentProps<"immich-albumCarousel">) {
|
||||
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
|
||||
const [paused, setPaused] = useState(false);
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.immich-albumCarousel");
|
||||
|
||||
const albumQuery = clientApi.widget.immich.getAlbum.useQuery(
|
||||
{
|
||||
@@ -72,7 +72,7 @@ export default function ImmichAlbumCarouselWidget({
|
||||
return (
|
||||
<Box h="100%" pos="relative">
|
||||
<Box pos="absolute" top={4} right={8} style={{ zIndex: 3 }}>
|
||||
<WidgetQueryErrorIndicator error={albumQuery.error} label={t("widget.immich-albumCarousel.name")} />
|
||||
<WidgetQueryErrorIndicator error={albumQuery.error} label={t("name")} />
|
||||
</Box>
|
||||
{album.assets.length === 0 || photoAssets.length === 0 ? (
|
||||
<NoPhotosInAlbum />
|
||||
@@ -133,7 +133,7 @@ function Carousel({
|
||||
paused,
|
||||
setPaused,
|
||||
}: CarouselProps) {
|
||||
const t = useScopedI18n("widget.immich-albumCarousel");
|
||||
const t = useI18n("widget.immich-albumCarousel");
|
||||
const locale = useCurrentIntlLocale();
|
||||
const reduceMotion = useReducedMotion();
|
||||
|
||||
@@ -189,7 +189,11 @@ function Carousel({
|
||||
size={advanced ? 40 : 32}
|
||||
onClick={() => setPaused((value) => !value)}
|
||||
>
|
||||
{paused ? <IconPlayerPlay size="var(--mantine-font-size-lg)" /> : <IconPlayerPause size="var(--mantine-font-size-lg)" />}
|
||||
{paused ? (
|
||||
<IconPlayerPlay size="var(--mantine-font-size-lg)" />
|
||||
) : (
|
||||
<IconPlayerPause size="var(--mantine-font-size-lg)" />
|
||||
)}
|
||||
</ActionIcon>
|
||||
{advanced && (
|
||||
<ActionIcon
|
||||
@@ -254,13 +258,13 @@ function Carousel({
|
||||
}
|
||||
|
||||
function NoPhotosInAlbum() {
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.immich-albumCarousel");
|
||||
return (
|
||||
<Center h="100%">
|
||||
<Stack align="center" gap="xs">
|
||||
<IconAlertCircle size={32} />
|
||||
<Text size="sm" fw={500}>
|
||||
{t("widget.immich-albumCarousel.noPhotos")}
|
||||
{t("noPhotos")}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { IconChevronLeft, IconChevronRight, IconPhoto, IconPlayerPause } from "@
|
||||
import z from "zod";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { createWidgetDefinition, widgetQueryInputMatches } from "../../definition";
|
||||
import { optionsBuilder } from "../../options";
|
||||
@@ -15,7 +15,7 @@ const createOptions = () =>
|
||||
withDescription: true,
|
||||
clearable: true,
|
||||
useOptions: (integrationIds: string[]) => {
|
||||
const t = useScopedI18n("widget.immich-albumCarousel");
|
||||
const t = useI18n("widget.immich-albumCarousel");
|
||||
const {
|
||||
data = [],
|
||||
isPending,
|
||||
|
||||
@@ -34,7 +34,6 @@ vi.mock("@homarr/api/client", () => ({
|
||||
vi.mock("@homarr/translation/client", () => ({
|
||||
useCurrentIntlLocale: () => "en",
|
||||
useI18n: () => (key: string) => key,
|
||||
useScopedI18n: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
describe("Immich album queries", () => {
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function ImmichServerStatsWidget({
|
||||
height,
|
||||
widgetRuntimeRef,
|
||||
}: WidgetComponentProps<"immich-serverStats">) {
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.immich-serverStats");
|
||||
const locale = useCurrentIntlLocale();
|
||||
const input = { integrationId: integrationIds[0] ?? "" };
|
||||
const albumsInput = { ...input, limit: MAX_ADVANCED_ALBUMS };
|
||||
@@ -60,7 +60,7 @@ export default function ImmichServerStatsWidget({
|
||||
{statVisibility.showUsers && (
|
||||
<StatItem
|
||||
icon={<IconUsers size="var(--mantine-font-size-xl)" />}
|
||||
label={t("widget.immich-serverStats.users")}
|
||||
label={t("users")}
|
||||
value={stats.userCount.toLocaleString(locale)}
|
||||
dense={statsLayout.dense}
|
||||
/>
|
||||
@@ -68,7 +68,7 @@ export default function ImmichServerStatsWidget({
|
||||
{statVisibility.showPhotos && (
|
||||
<StatItem
|
||||
icon={<IconPhoto size="var(--mantine-font-size-xl)" />}
|
||||
label={t("widget.immich-serverStats.photos")}
|
||||
label={t("photos")}
|
||||
value={stats.photoCount.toLocaleString(locale)}
|
||||
dense={statsLayout.dense}
|
||||
/>
|
||||
@@ -76,7 +76,7 @@ export default function ImmichServerStatsWidget({
|
||||
{statVisibility.showVideos && (
|
||||
<StatItem
|
||||
icon={<IconVideo size="var(--mantine-font-size-xl)" />}
|
||||
label={t("widget.immich-serverStats.videos")}
|
||||
label={t("videos")}
|
||||
value={stats.videoCount.toLocaleString(locale)}
|
||||
dense={statsLayout.dense}
|
||||
/>
|
||||
@@ -84,7 +84,7 @@ export default function ImmichServerStatsWidget({
|
||||
{statVisibility.showStorage && (
|
||||
<StatItem
|
||||
icon={<IconDatabase size="var(--mantine-font-size-xl)" />}
|
||||
label={t("widget.immich-serverStats.storage")}
|
||||
label={t("storage")}
|
||||
value={formatBytes(stats.totalLibraryUsageInBytes)}
|
||||
dense={statsLayout.dense}
|
||||
/>
|
||||
@@ -98,7 +98,7 @@ export default function ImmichServerStatsWidget({
|
||||
{statsContent}
|
||||
{statsQuery.error && (
|
||||
<Box pos="absolute" top={4} right={4}>
|
||||
<WidgetQueryErrorIndicator error={statsQuery.error} label={t("widget.immich-serverStats.name")} />
|
||||
<WidgetQueryErrorIndicator error={statsQuery.error} label={t("name")} />
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -112,19 +112,16 @@ export default function ImmichServerStatsWidget({
|
||||
{statsContent}
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("widget.immich-serverStats.albumLimit", { count: MAX_ADVANCED_ALBUMS })}
|
||||
{t("albumLimit", { count: MAX_ADVANCED_ALBUMS })}
|
||||
</Text>
|
||||
<WidgetQueryErrorIndicator
|
||||
error={statsQuery.error ?? albumsQuery.error}
|
||||
label={t("widget.immich-serverStats.name")}
|
||||
/>
|
||||
<WidgetQueryErrorIndicator error={statsQuery.error ?? albumsQuery.error} label={t("name")} />
|
||||
</Group>
|
||||
<ScrollArea style={{ flex: 1, minHeight: 0 }}>
|
||||
{isInitialWidgetQueryPending(albumsQuery) ? (
|
||||
<WidgetQueryLoadingState />
|
||||
) : albumsQuery.error && albumsQuery.data === undefined ? (
|
||||
<Center h="100%" p="md">
|
||||
<WidgetQueryErrorIndicator error={albumsQuery.error} label={t("widget.immich-serverStats.name")} />
|
||||
<WidgetQueryErrorIndicator error={albumsQuery.error} label={t("name")} />
|
||||
</Center>
|
||||
) : albums.length === 0 ? (
|
||||
<WidgetEmptyState />
|
||||
|
||||
@@ -49,7 +49,8 @@ export default function IndexerManagerWidget({
|
||||
widgetRuntimeRef,
|
||||
displayMode,
|
||||
}: WidgetComponentProps<"indexerManager">) {
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.indexerManager");
|
||||
const tCommon = useI18n("common");
|
||||
const indexersQuery = clientApi.widget.indexerManager.getIndexersStatus.useQuery({ integrationIds });
|
||||
const isInitialPending = isInitialWidgetQueryPending(indexersQuery);
|
||||
const indexersData = getUsableWidgetQueryData(indexersQuery) ?? [];
|
||||
@@ -92,8 +93,8 @@ export default function IndexerManagerWidget({
|
||||
p={isDense ? "xs" : "sm"}
|
||||
>
|
||||
<Group className="indexer-manager-title" align="center" gap="xs" wrap="nowrap" w="100%">
|
||||
{testAllError && <VisuallyHidden role="alert">{t("common.error")}</VisuallyHidden>}
|
||||
<Tooltip label={t("widget.indexerManager.title")} disabled={!hasSmallWidth}>
|
||||
{testAllError && <VisuallyHidden role="alert">{tCommon("error")}</VisuallyHidden>}
|
||||
<Tooltip label={t("title")} disabled={!hasSmallWidth}>
|
||||
<IconReportSearch
|
||||
className="indexer-manager-title-icon"
|
||||
size={hasSmallWidth ? 16 : 20}
|
||||
@@ -102,7 +103,7 @@ export default function IndexerManagerWidget({
|
||||
</Tooltip>
|
||||
{!hasSmallWidth && (
|
||||
<Text size={isDense ? "xs" : "sm"} fw={600} truncate="end">
|
||||
{t("widget.indexerManager.title")}
|
||||
{t("title")}
|
||||
</Text>
|
||||
)}
|
||||
{showHealthCounts && (
|
||||
@@ -117,8 +118,8 @@ export default function IndexerManagerWidget({
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
<WidgetQueryErrorIndicator error={indexersQuery.error} label={t("widget.indexerManager.title")} />
|
||||
<Tooltip label={testAllError ? t("common.error") : t("widget.indexerManager.testAll")}>
|
||||
<WidgetQueryErrorIndicator error={indexersQuery.error} label={t("title")} />
|
||||
<Tooltip label={testAllError ? tCommon("error") : t("testAll")}>
|
||||
<ActionIcon
|
||||
className={combineClasses("indexer-manager-test-action-icon", classes.testAction, actionTargetClasses.root)}
|
||||
size="sm"
|
||||
@@ -131,7 +132,7 @@ export default function IndexerManagerWidget({
|
||||
onClick={() => {
|
||||
testAllIndexers();
|
||||
}}
|
||||
aria-label={t("widget.indexerManager.testAll")}
|
||||
aria-label={t("testAll")}
|
||||
>
|
||||
<IconTestPipe size="var(--mantine-font-size-sm)" />
|
||||
</ActionIcon>
|
||||
@@ -161,7 +162,7 @@ export default function IndexerManagerWidget({
|
||||
</Text>
|
||||
{error && (
|
||||
<Badge size="xs" color="red" variant="light">
|
||||
{t("common.error")}
|
||||
{tCommon("error")}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
@@ -197,7 +198,7 @@ export default function IndexerManagerWidget({
|
||||
</Anchor>
|
||||
{isAdvanced ? (
|
||||
<Badge size="xs" color={presentation.color} variant="light">
|
||||
{t(`widget.indexerManager.status.${displayStatus}`)}
|
||||
{t(`status.${displayStatus}` as never)}
|
||||
</Badge>
|
||||
) : displayStatus === "healthy" ? (
|
||||
<IconCircleCheck
|
||||
|
||||
@@ -23,7 +23,7 @@ import { getQueryKey } from "@trpc/react-query";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { MissingMediaItem, QueuedMediaItem } from "@homarr/integrations/types";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../common/application-url";
|
||||
@@ -44,7 +44,7 @@ export default function MediaMissingWidget({
|
||||
displayMode,
|
||||
widgetRuntimeRef,
|
||||
}: WidgetComponentProps<"mediaMissing">) {
|
||||
const t = useScopedI18n("widget.mediaMissing");
|
||||
const t = useI18n("widget.mediaMissing");
|
||||
const isAdvanced = displayMode === "advanced";
|
||||
const showMissing = isAdvanced || options.showMissing;
|
||||
const showQueued = isAdvanced || options.showQueued;
|
||||
@@ -181,12 +181,20 @@ export default function MediaMissingWidget({
|
||||
</Group>
|
||||
<Tabs.List grow>
|
||||
{showMissing && (
|
||||
<Tabs.Tab value="missing" px={isThin ? 6 : undefined} leftSection={<IconQuestionMark size="var(--mantine-font-size-sm)" />}>
|
||||
<Tabs.Tab
|
||||
value="missing"
|
||||
px={isThin ? 6 : undefined}
|
||||
leftSection={<IconQuestionMark size="var(--mantine-font-size-sm)" />}
|
||||
>
|
||||
{tabLabel(t("tab.missing"), missing.length, missingCount)}
|
||||
</Tabs.Tab>
|
||||
)}
|
||||
{showQueued && (
|
||||
<Tabs.Tab value="queued" px={isThin ? 6 : undefined} leftSection={<IconDownload size="var(--mantine-font-size-sm)" />}>
|
||||
<Tabs.Tab
|
||||
value="queued"
|
||||
px={isThin ? 6 : undefined}
|
||||
leftSection={<IconDownload size="var(--mantine-font-size-sm)" />}
|
||||
>
|
||||
{tabLabel(t("tab.queued"), queued.length, queuedCount)}
|
||||
</Tabs.Tab>
|
||||
)}
|
||||
@@ -240,7 +248,7 @@ const episodeCode = (item: MissingMediaItem | QueuedMediaItem) =>
|
||||
: null;
|
||||
|
||||
const TypeBadge = ({ item, density }: { item: MissingMediaItem | QueuedMediaItem; density: Density }) => {
|
||||
const t = useScopedI18n("widget.mediaMissing");
|
||||
const t = useI18n("widget.mediaMissing");
|
||||
const color = item.type === "movie" ? "yellow" : "blue";
|
||||
const code = episodeCode(item);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import { getMantineColor, toValidDate } from "@homarr/common";
|
||||
import { getIconUrl } from "@homarr/definitions";
|
||||
import type { MediaRelease } from "@homarr/integrations/types";
|
||||
import { mediaTypeConfigurations } from "@homarr/integrations/types";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import type { ScopedTranslationFunction } from "@homarr/translation";
|
||||
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
import { OverflowBadge } from "@homarr/ui";
|
||||
@@ -43,7 +43,7 @@ export default function MediaReleasesWidget({
|
||||
height,
|
||||
displayMode,
|
||||
}: WidgetComponentProps<"mediaReleases">) {
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.mediaReleases");
|
||||
const releasesQuery = clientApi.widget.mediaRelease.getMediaReleases.useQuery({ integrationIds });
|
||||
const response = getUsableWidgetQueryData(releasesQuery);
|
||||
|
||||
@@ -73,7 +73,7 @@ export default function MediaReleasesWidget({
|
||||
{(failedIntegrations.length > 0 || releasesQuery.error) && (
|
||||
<Group pos="absolute" top={4} right={4} gap={0}>
|
||||
<IntegrationErrorIndicator results={failedIntegrations} />
|
||||
<WidgetQueryErrorIndicator error={releasesQuery.error} label={t("widget.mediaReleases.name")} />
|
||||
<WidgetQueryErrorIndicator error={releasesQuery.error} label={t("name")} />
|
||||
</Group>
|
||||
)}
|
||||
</Box>
|
||||
@@ -101,7 +101,7 @@ const formatReleaseDate = (value: unknown, locale: string, compact: boolean) =>
|
||||
|
||||
const Item = ({ item, options, isAdvanced, width, height }: ItemProps) => {
|
||||
const locale = useCurrentIntlLocale();
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.mediaReleases");
|
||||
const length = formatLength(item.length, item.type, t);
|
||||
const isCompact = !isAdvanced && (width < 340 || height < 180);
|
||||
const isTiny = !isAdvanced && (width < 220 || height < 105);
|
||||
@@ -281,12 +281,16 @@ const Info = ({ icon: Icon, label }: IconAndLabelProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const formatLength = (length: number | undefined, type: MediaRelease["type"], t: TranslationFunction) => {
|
||||
const formatLength = (
|
||||
length: number | undefined,
|
||||
type: MediaRelease["type"],
|
||||
t: ScopedTranslationFunction<"widget.mediaReleases">,
|
||||
) => {
|
||||
if (!length) return undefined;
|
||||
if (type === "movie" || type === "tv" || type === "video" || type === "music" || type === "article") {
|
||||
return {
|
||||
type: "duration" as const,
|
||||
label: t("widget.mediaReleases.length.duration", {
|
||||
label: t("length.duration", {
|
||||
length: Math.round(length / 60).toString(),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import { toValidDate } from "@homarr/common";
|
||||
import type { MediaRequestStatus } from "@homarr/integrations/types";
|
||||
import { mediaAvailabilityConfiguration, mediaRequestStatusConfiguration } from "@homarr/integrations/types";
|
||||
import { openMediaRequestSearch } from "@homarr/spotlight";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../../common/empty-state";
|
||||
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../../common/application-url";
|
||||
@@ -83,7 +83,7 @@ export default function MediaServerWidget({
|
||||
}
|
||||
|
||||
const MediaRequestSearchButton = ({ integrationIds }: { integrationIds: string[] }) => {
|
||||
const t = useScopedI18n("search.mode.media");
|
||||
const t = useI18n("search.mode.media");
|
||||
|
||||
return (
|
||||
<Tooltip label={t("action.search.label")}>
|
||||
@@ -118,7 +118,7 @@ const MediaRequestCard = ({
|
||||
options,
|
||||
}: MediaRequestCardProps) => {
|
||||
const board = useRequiredBoard();
|
||||
const t = useScopedI18n("widget.mediaRequests-requestList");
|
||||
const t = useI18n("widget.mediaRequests-requestList");
|
||||
const requestHref = getSafeApplicationUrl(request.href);
|
||||
const requestedByHref = getSafeApplicationUrl(request.requestedBy?.link);
|
||||
|
||||
@@ -252,7 +252,7 @@ const DecisionButtons = ({ requestId, integrationId, canInteract, alwaysVisible
|
||||
} = clientApi.widget.mediaRequests.answerRequest.useMutation({
|
||||
onSettled: () => void utils.widget.mediaRequests.invalidate(),
|
||||
});
|
||||
const t = useScopedI18n("widget.mediaRequests-requestList");
|
||||
const t = useI18n("widget.mediaRequests-requestList");
|
||||
const handleDecision = (answer: RouterInputs["widget"]["mediaRequests"]["answerRequest"]["answer"]) => {
|
||||
if (!canInteract || isPending) return;
|
||||
mutateRequestAnswer({
|
||||
@@ -308,7 +308,7 @@ interface StatusBadgeProps {
|
||||
}
|
||||
|
||||
const StatusBadge = ({ status }: StatusBadgeProps) => {
|
||||
const tStatus = useScopedI18n("widget.mediaRequests-requestList.status");
|
||||
const tStatus = useI18n("widget.mediaRequests-requestList.status");
|
||||
|
||||
return (
|
||||
<Badge size="xs" color={mediaRequestStatusConfiguration[status].color} variant="light">
|
||||
|
||||
@@ -19,7 +19,7 @@ import { clientApi } from "@homarr/api/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import type { RequestStats } from "@homarr/integrations/types";
|
||||
import { openMediaRequestSearch } from "@homarr/spotlight";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../../common/empty-state";
|
||||
import { getSafeApplicationUrl, SAFE_NEW_TAB_REL } from "../../common/application-url";
|
||||
@@ -40,7 +40,8 @@ export default function MediaServerWidget({
|
||||
width,
|
||||
height,
|
||||
}: WidgetComponentProps<"mediaRequests-requestStats">) {
|
||||
const t = useScopedI18n("widget.mediaRequests-requestStats");
|
||||
const t = useI18n("widget.mediaRequests-requestStats");
|
||||
const tCommon = useI18n("common");
|
||||
const requestStats = getUsableWidgetQueryData(clientApi.widget.mediaRequests.getStats.useQuery({ integrationIds }));
|
||||
|
||||
const board = useRequiredBoard();
|
||||
@@ -129,7 +130,7 @@ export default function MediaServerWidget({
|
||||
key={stat.name}
|
||||
span={isTiny ? 6 : 3}
|
||||
>
|
||||
<Tooltip label={t(`titles.stats.${stat.name}`)}>
|
||||
<Tooltip label={stat.name === "total" ? tCommon("total") : t(`titles.stats.${stat.name}`)}>
|
||||
<Card p={0} radius={board.itemRadius} className={classes.card}>
|
||||
<Group className="mediaRequests-stats-stat-stack" justify="center" align="center" gap="xs" w="100%">
|
||||
<stat.icon className="mediaRequests-stats-stat-icon" size="var(--mantine-font-size-md)" />
|
||||
@@ -215,7 +216,7 @@ export default function MediaServerWidget({
|
||||
}
|
||||
|
||||
const MediaRequestSearchButton = ({ integrationIds }: { integrationIds: string[] }) => {
|
||||
const t = useScopedI18n("search.mode.media");
|
||||
const t = useI18n("search.mode.media");
|
||||
|
||||
return (
|
||||
<Tooltip label={t("action.search.label")}>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "./component";
|
||||
|
||||
vi.mock("@homarr/translation/client", () => ({
|
||||
useScopedI18n: () => (key: string) => key,
|
||||
useI18n: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
const session = {
|
||||
|
||||
@@ -34,7 +34,7 @@ import { clientApi } from "@homarr/api/client";
|
||||
import { objectEntries } from "@homarr/common";
|
||||
import { getIconUrl } from "@homarr/definitions";
|
||||
import type { StreamSession } from "@homarr/integrations";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
@@ -183,8 +183,8 @@ export default function MediaServerWidget({
|
||||
const currentStreamsData = getUsableWidgetQueryData(currentStreamsQuery);
|
||||
const currentStreams = useMemo(() => currentStreamsData ?? [], [currentStreamsData]);
|
||||
|
||||
const t = useScopedI18n("widget.mediaServer");
|
||||
const tGlobal = useI18n();
|
||||
const t = useI18n("widget.mediaServer");
|
||||
const tSearch = useI18n("search");
|
||||
const isAdvanced = displayMode === "advanced";
|
||||
const showLocation = isAdvanced || options.showLocation;
|
||||
const showBitrate = isAdvanced || options.showBitrate;
|
||||
@@ -258,8 +258,8 @@ export default function MediaServerWidget({
|
||||
size="xs"
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.currentTarget.value)}
|
||||
placeholder={tGlobal("search.placeholder")}
|
||||
aria-label={tGlobal("search.placeholder")}
|
||||
placeholder={tSearch("placeholder")}
|
||||
aria-label={tSearch("placeholder")}
|
||||
leftSection={<IconSearch size="var(--mantine-font-size-xs)" aria-hidden />}
|
||||
/>
|
||||
<WidgetQueryErrorIndicator error={currentStreamsQuery.error} label={t("name")} />
|
||||
@@ -474,7 +474,7 @@ export default function MediaServerWidget({
|
||||
}
|
||||
|
||||
function CurrentlyPlaying({ item }: { item: StreamSession }) {
|
||||
const t = useScopedI18n("widget.mediaServer");
|
||||
const t = useI18n("widget.mediaServer");
|
||||
const currentlyPlaying = item.currentlyPlaying;
|
||||
if (!currentlyPlaying) {
|
||||
return (
|
||||
@@ -580,7 +580,7 @@ export function SessionDetailsPopover({
|
||||
}
|
||||
|
||||
function SessionDetails({ item }: { item: StreamSession }) {
|
||||
const t = useScopedI18n("widget.mediaServer.items");
|
||||
const t = useI18n("widget.mediaServer.items");
|
||||
const Icon = item.currentlyPlaying ? mediaTypeIconMap[item.currentlyPlaying.type] : null;
|
||||
|
||||
const metadata = item.currentlyPlaying?.metadata ? constructMetadata(item.currentlyPlaying.metadata) : null;
|
||||
|
||||
@@ -15,6 +15,7 @@ interface HealthCheckStatusProps {
|
||||
export function HealthCheckStatus(props: HealthCheckStatusProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const t = useI18n("widget.mediaTranscoding.healthCheck");
|
||||
const tCommon = useI18n("widget.mediaTranscoding.panel.common");
|
||||
|
||||
const indicatorColor = props.statistics.failedHealthCheckCount
|
||||
? "red"
|
||||
@@ -25,7 +26,7 @@ export function HealthCheckStatus(props: HealthCheckStatusProps) {
|
||||
return (
|
||||
<Popover position="bottom" width={250} shadow="sm" withArrow>
|
||||
<Popover.Target>
|
||||
<ActionIcon className={actionTargetClasses.root} variant="subtle" size="sm" aria-label={t("title")}>
|
||||
<ActionIcon className={actionTargetClasses.root} variant="subtle" size="sm" aria-label={tCommon("healthCheck")}>
|
||||
<Indicator color={textColor(indicatorColor, colorScheme)} size={6} display="flex">
|
||||
<IconHeartbeat size="var(--mantine-font-size-md)" />
|
||||
</Indicator>
|
||||
@@ -39,7 +40,7 @@ export function HealthCheckStatus(props: HealthCheckStatusProps) {
|
||||
<Stack gap="sm" align="center">
|
||||
<Group gap="xs">
|
||||
<IconHeartbeat size="var(--mantine-font-size-lg)" />
|
||||
<Text size="sm">{t("title")}</Text>
|
||||
<Text size="sm">{tCommon("healthCheck")}</Text>
|
||||
</Group>
|
||||
<Divider
|
||||
style={{
|
||||
|
||||
@@ -26,11 +26,12 @@ export function QueuePanel(props: QueuePanelProps) {
|
||||
const { queue } = props;
|
||||
|
||||
const t = useI18n("widget.mediaTranscoding.panel.queue");
|
||||
const tCommon = useI18n("widget.mediaTranscoding.panel.common");
|
||||
|
||||
if (queue.array.length === 0) {
|
||||
return (
|
||||
<Center style={{ flex: "1" }}>
|
||||
<Title order={6}>{t("empty")}</Title>
|
||||
<Title order={6}>{tCommon("empty")}</Title>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
@@ -42,7 +43,7 @@ export function QueuePanel(props: QueuePanelProps) {
|
||||
<TableTr>
|
||||
<TableTh ta="start" py={4}>
|
||||
<Text size="xs" fw="bold">
|
||||
{t("table.file")}
|
||||
{tCommon("file")}
|
||||
</Text>
|
||||
</TableTh>
|
||||
<TableTh ta="start" py={4}>
|
||||
@@ -58,11 +59,11 @@ export function QueuePanel(props: QueuePanelProps) {
|
||||
<TableTd py={2}>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
{item.type === "transcode" ? (
|
||||
<Tooltip label={t("table.transcode")}>
|
||||
<Tooltip label={tCommon("transcode")}>
|
||||
<IconTransform size="var(--mantine-font-size-xs)" style={{ flexShrink: 0 }} />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip label={t("table.healthCheck")}>
|
||||
<Tooltip label={tCommon("healthCheck")}>
|
||||
<IconHeartbeat size="var(--mantine-font-size-xs)" style={{ flexShrink: 0 }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -16,6 +16,7 @@ interface StatisticsPanelProps {
|
||||
|
||||
export function StatisticsPanel(props: StatisticsPanelProps) {
|
||||
const t = useI18n("widget.mediaTranscoding.panel.statistics");
|
||||
const tCommon = useI18n("widget.mediaTranscoding.panel.common");
|
||||
|
||||
const allLibs = props.statistics;
|
||||
|
||||
@@ -23,7 +24,7 @@ export function StatisticsPanel(props: StatisticsPanelProps) {
|
||||
if (!(allLibs.totalFileCount > 0)) {
|
||||
return (
|
||||
<Center style={{ flex: "1" }}>
|
||||
<Title order={6}>{t("empty")}</Title>
|
||||
<Title order={6}>{tCommon("empty")}</Title>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
@@ -31,10 +32,14 @@ export function StatisticsPanel(props: StatisticsPanelProps) {
|
||||
return (
|
||||
<ScrollArea h="100%">
|
||||
<Group wrap="wrap" justify="center" p={4} w="100%" gap="xs">
|
||||
<StatisticItem icon={IconTransform} label={t("transcodesCount")} value={props.statistics.totalTranscodeCount} />
|
||||
<StatisticItem
|
||||
icon={IconTransform}
|
||||
label={tCommon("transcodes")}
|
||||
value={props.statistics.totalTranscodeCount}
|
||||
/>
|
||||
<StatisticItem
|
||||
icon={IconHeartbeat}
|
||||
label={t("healthChecksCount")}
|
||||
label={tCommon("healthChecks")}
|
||||
value={props.statistics.totalHealthCheckCount}
|
||||
/>
|
||||
<StatisticItem icon={IconFileDescription} label={t("filesCount")} value={props.statistics.totalFileCount} />
|
||||
@@ -45,8 +50,8 @@ export function StatisticsPanel(props: StatisticsPanelProps) {
|
||||
/>
|
||||
</Group>
|
||||
<Group justify="center" wrap="wrap" grow>
|
||||
<StatisticRingProgress items={allLibs.transcodeStatus} label={t("transcodes")} />
|
||||
<StatisticRingProgress items={allLibs.healthCheckStatus} label={t("healthChecks")} />
|
||||
<StatisticRingProgress items={allLibs.transcodeStatus} label={tCommon("transcodes")} />
|
||||
<StatisticRingProgress items={allLibs.healthCheckStatus} label={tCommon("healthChecks")} />
|
||||
<StatisticRingProgress items={allLibs.videoCodecs} label={t("videoCodecs")} />
|
||||
<StatisticRingProgress items={allLibs.videoContainers} label={t("videoContainers")} />
|
||||
<StatisticRingProgress items={allLibs.videoResolutions} label={t("videoResolutions")} />
|
||||
|
||||
@@ -25,11 +25,12 @@ interface WorkersPanelProps {
|
||||
|
||||
export function WorkersPanel(props: WorkersPanelProps) {
|
||||
const t = useI18n("widget.mediaTranscoding.panel.workers");
|
||||
const tCommon = useI18n("widget.mediaTranscoding.panel.common");
|
||||
|
||||
if (props.workers.length === 0) {
|
||||
return (
|
||||
<Center style={{ flex: "1" }}>
|
||||
<Title order={6}>{t("empty")}</Title>
|
||||
<Title order={6}>{tCommon("empty")}</Title>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
@@ -41,7 +42,7 @@ export function WorkersPanel(props: WorkersPanelProps) {
|
||||
<TableTr>
|
||||
<TableTh ta="start" py={4}>
|
||||
<Text size="xs" fw="bold">
|
||||
{t("table.file")}
|
||||
{tCommon("file")}
|
||||
</Text>
|
||||
</TableTh>
|
||||
<TableTh ta="start" py={4} w={50}>
|
||||
@@ -65,11 +66,11 @@ export function WorkersPanel(props: WorkersPanelProps) {
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<div>
|
||||
{worker.jobType === "transcode" ? (
|
||||
<Tooltip label={t("table.transcode")}>
|
||||
<Tooltip label={tCommon("transcode")}>
|
||||
<IconTransform size="var(--mantine-font-size-sm)" style={{ flexShrink: 0 }} />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip label={t("table.healthCheck")}>
|
||||
<Tooltip label={tCommon("healthCheck")}>
|
||||
<IconHeartbeat size="var(--mantine-font-size-sm)" style={{ flexShrink: 0 }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -16,13 +16,14 @@ export default function MinecraftServerStatusWidget({
|
||||
height,
|
||||
}: WidgetComponentProps<"minecraftServerStatus">) {
|
||||
const { data: result, isPending, error } = clientApi.widget.minecraft.getServerStatus.useQuery(options);
|
||||
const t = useI18n();
|
||||
const t = useI18n("widget.minecraftServerStatus");
|
||||
const tCommon = useI18n("common");
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<Flex align="center" justify="center" h="100%">
|
||||
<Text c="dimmed" size="sm">
|
||||
{t("common.action.loading")}
|
||||
{tCommon("action.loading")}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
@@ -50,20 +51,10 @@ export default function MinecraftServerStatusWidget({
|
||||
align="center"
|
||||
>
|
||||
<Group gap="xs" wrap="nowrap" align="center" maw="100%">
|
||||
<Tooltip
|
||||
label={
|
||||
data.online
|
||||
? t("widget.minecraftServerStatus.status.online")
|
||||
: t("widget.minecraftServerStatus.status.offline")
|
||||
}
|
||||
>
|
||||
<Tooltip label={data.online ? t("status.online") : t("status.offline")}>
|
||||
<Box aria-hidden miw="md" h="md" bg={data.online ? "teal" : "red"} style={{ borderRadius: "100%" }} />
|
||||
</Tooltip>
|
||||
<VisuallyHidden>
|
||||
{data.online
|
||||
? t("widget.minecraftServerStatus.status.online")
|
||||
: t("widget.minecraftServerStatus.status.offline")}
|
||||
</VisuallyHidden>
|
||||
<VisuallyHidden>{data.online ? t("status.online") : t("status.offline")}</VisuallyHidden>
|
||||
<Text size={showMetadata ? "lg" : "md"} fw="bold" truncate="end">
|
||||
{title}
|
||||
</Text>
|
||||
@@ -71,9 +62,7 @@ export default function MinecraftServerStatusWidget({
|
||||
{showMetadata && (
|
||||
<Group gap="xs" mt="xs" wrap="wrap" justify="center">
|
||||
<Badge variant="light">{options.domain}</Badge>
|
||||
{options.isBedrockServer && (
|
||||
<Badge variant="outline">{t("widget.minecraftServerStatus.option.isBedrockServer.label")}</Badge>
|
||||
)}
|
||||
{options.isBedrockServer && <Badge variant="outline">{t("option.isBedrockServer.label")}</Badge>}
|
||||
</Group>
|
||||
)}
|
||||
{data.online && (
|
||||
|
||||
@@ -20,7 +20,10 @@ interface EmbeddedAppEditFormProps {
|
||||
}
|
||||
|
||||
export const EmbeddedAppEditForm = ({ appId, handleRef }: EmbeddedAppEditFormProps) => {
|
||||
const t = useI18n();
|
||||
const tCommon = useI18n("common");
|
||||
const tAppEdit = useI18n("app.page.edit");
|
||||
const tAppSelect = useI18n("app.action.select");
|
||||
const tItemApp = useI18n("item.edit.app");
|
||||
const utils = clientApi.useUtils();
|
||||
const appFormRef = useRef<AppFormHandle>(null);
|
||||
const { data: app, isPending: isLoadingApp, isError, refetch } = clientApi.app.byId.useQuery({ id: appId });
|
||||
@@ -29,14 +32,14 @@ export const EmbeddedAppEditForm = ({ appId, handleRef }: EmbeddedAppEditFormPro
|
||||
onSuccess: async () => {
|
||||
await utils.app.invalidate();
|
||||
showSuccessNotification({
|
||||
title: t("app.page.edit.notification.success.title"),
|
||||
message: t("app.page.edit.notification.success.message"),
|
||||
title: tCommon("notification.update.success"),
|
||||
message: tAppEdit("notification.success.message"),
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
showErrorNotification({
|
||||
title: t("app.page.edit.notification.error.title"),
|
||||
message: t("app.page.edit.notification.error.message"),
|
||||
title: tCommon("notification.update.error"),
|
||||
message: tAppEdit("notification.error.message"),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -66,9 +69,9 @@ export const EmbeddedAppEditForm = ({ appId, handleRef }: EmbeddedAppEditFormPro
|
||||
if (isError || !app) {
|
||||
return (
|
||||
<Stack align="center" gap="md" py="xl">
|
||||
<Text c="dimmed">{t("app.action.select.notFound")}</Text>
|
||||
<Text c="dimmed">{tAppSelect("notFound")}</Text>
|
||||
<Button variant="light" onClick={() => void refetch()}>
|
||||
{t("common.action.tryAgain")}
|
||||
{tCommon("action.tryAgain")}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
@@ -77,7 +80,7 @@ export const EmbeddedAppEditForm = ({ appId, handleRef }: EmbeddedAppEditFormPro
|
||||
return (
|
||||
<Stack>
|
||||
<Alert icon={<IconInfoCircle size="var(--mantine-font-size-md)" />} color="blue" variant="light">
|
||||
{t("item.edit.app.propagationNotice")}
|
||||
{tItemApp("propagationNotice")}
|
||||
</Alert>
|
||||
<AppForm
|
||||
formRef={appFormRef}
|
||||
|
||||
@@ -14,7 +14,7 @@ interface InnerProps {
|
||||
}
|
||||
|
||||
export const WidgetAdvancedOptionsModal = createModal<InnerProps>(({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const t = useI18n("item.edit");
|
||||
const theme = useMantineTheme();
|
||||
const form = useForm({
|
||||
initialValues: innerProps.advancedOptions,
|
||||
@@ -33,17 +33,14 @@ export const WidgetAdvancedOptionsModal = createModal<InnerProps>(({ actions, in
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label={t("item.edit.field.title.label")}
|
||||
label={t("field.title.label")}
|
||||
data-autofocus
|
||||
{...form.getInputProps("title")}
|
||||
rightSection={<Input.ClearButton onClick={() => form.setFieldValue("title", "")} />}
|
||||
/>
|
||||
<TextMultiSelect
|
||||
label={t("item.edit.field.customCssClasses.label")}
|
||||
{...form.getInputProps("customCssClasses")}
|
||||
/>
|
||||
<TextMultiSelect label={t("field.customCssClasses.label")} {...form.getInputProps("customCssClasses")} />
|
||||
<ColorInput
|
||||
label={t("item.edit.field.borderColor.label")}
|
||||
label={t("field.borderColor.label")}
|
||||
format="hex"
|
||||
swatches={Object.values(theme.colors).map((color) => color[6])}
|
||||
rightSection={
|
||||
|
||||
@@ -68,6 +68,8 @@ export const getSelectedWidgetIntegrations = (
|
||||
|
||||
export const WidgetEditModal = createModal<WidgetEditModalProps<WidgetKind>>(({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const tItem = useI18n("item.edit");
|
||||
const tCommon = useI18n("common");
|
||||
const board = useOptionalBoard();
|
||||
const { data: session } = useSession();
|
||||
const [advancedOptions, setAdvancedOptions] = useState<BoardItemAdvancedOptions>(innerProps.value.advancedOptions);
|
||||
@@ -233,7 +235,7 @@ export const WidgetEditModal = createModal<WidgetEditModalProps<WidgetKind>>(({
|
||||
<Stack>
|
||||
{canConfigureWidget && innerProps.integrationSupport && (
|
||||
<WidgetIntegrationSelect
|
||||
label={t("item.edit.field.integrations.label")}
|
||||
label={tItem("field.integrations.label")}
|
||||
data={innerProps.integrationData}
|
||||
canSelectMultiple={maxIntegrations > 1}
|
||||
withAsterisk={integrationsRequired}
|
||||
@@ -280,7 +282,7 @@ export const WidgetEditModal = createModal<WidgetEditModalProps<WidgetKind>>(({
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("item.edit.advancedOptions.label")}
|
||||
{tItem("advancedOptions.label")}
|
||||
</Button>
|
||||
) : (
|
||||
<ModalFormFooter
|
||||
@@ -296,7 +298,7 @@ export const WidgetEditModal = createModal<WidgetEditModalProps<WidgetKind>>(({
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("item.edit.advancedOptions.label")}
|
||||
{tItem("advancedOptions.label")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
@@ -311,9 +313,9 @@ export const WidgetEditModal = createModal<WidgetEditModalProps<WidgetKind>>(({
|
||||
<Stack>
|
||||
<Tabs value={activeTab} onChange={setActiveTab}>
|
||||
<Tabs.List grow>
|
||||
<Tabs.Tab value="widget">{t("item.edit.tab.widget")}</Tabs.Tab>
|
||||
{showAppTab && <Tabs.Tab value="app">{t("item.edit.tab.app")}</Tabs.Tab>}
|
||||
{showIntegrationTab && <Tabs.Tab value="integration">{t("item.edit.tab.integration")}</Tabs.Tab>}
|
||||
<Tabs.Tab value="widget">{tItem("tab.widget")}</Tabs.Tab>
|
||||
{showAppTab && <Tabs.Tab value="app">{tItem("tab.app")}</Tabs.Tab>}
|
||||
{showIntegrationTab && <Tabs.Tab value="integration">{tItem("tab.integration")}</Tabs.Tab>}
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value="widget" pt="md">
|
||||
{widgetFormContent}
|
||||
@@ -329,7 +331,7 @@ export const WidgetEditModal = createModal<WidgetEditModalProps<WidgetKind>>(({
|
||||
{displayedIntegrationId === null && (
|
||||
<>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("item.edit.integration.description")}
|
||||
{tItem("integration.description")}
|
||||
</Text>
|
||||
{selectedIntegrations.map((integration) => {
|
||||
const canEdit = editableIntegrationIds.includes(integration.id);
|
||||
@@ -352,13 +354,13 @@ export const WidgetEditModal = createModal<WidgetEditModalProps<WidgetKind>>(({
|
||||
variant="light"
|
||||
leftSection={<IconPencil size={16} />}
|
||||
onClick={() => beginEditingIntegration(integration.id)}
|
||||
aria-label={t("item.edit.integration.editLabel", { name: integration.name })}
|
||||
aria-label={tItem("integration.editLabel", { name: integration.name })}
|
||||
>
|
||||
{t("item.edit.integration.action")}
|
||||
{tItem("integration.action")}
|
||||
</Button>
|
||||
) : (
|
||||
<Text size="xs" c="dimmed" ta="end">
|
||||
{t("item.edit.integration.fullAccessRequired")}
|
||||
{tItem("integration.fullAccessRequired")}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
@@ -374,7 +376,7 @@ export const WidgetEditModal = createModal<WidgetEditModalProps<WidgetKind>>(({
|
||||
onClick={() => setActiveIntegrationId(null)}
|
||||
style={{ alignSelf: "start" }}
|
||||
>
|
||||
{t("common.action.previous")}
|
||||
{tCommon("action.previous")}
|
||||
</Button>
|
||||
)}
|
||||
{integrationIdsToRender.map((integrationId) => (
|
||||
@@ -406,10 +408,10 @@ export const WidgetEditModal = createModal<WidgetEditModalProps<WidgetKind>>(({
|
||||
>
|
||||
<Group justify="end">
|
||||
<Button onClick={actions.closeModal} variant="subtle" color="gray">
|
||||
{t("common.action.cancel")}
|
||||
{tCommon("action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
{t("common.action.saveChanges")}
|
||||
{tCommon("action.saveChanges")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useMemo } from "react";
|
||||
import { Badge, Card, Center, Group, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
import { IntegrationErrorIndicator } from "../../common/integration-error-indicator";
|
||||
@@ -32,8 +32,9 @@ export default function NetworkControllerNetworkStatusWidget({
|
||||
),
|
||||
[results],
|
||||
);
|
||||
const t = useScopedI18n("widget.networkControllerStatus");
|
||||
const tCommon = useScopedI18n("common");
|
||||
const t = useI18n("widget.networkControllerStatus");
|
||||
const tWidgetCommon = useI18n("widget.common");
|
||||
const tCommon = useI18n("common");
|
||||
const locale = useCurrentIntlLocale();
|
||||
|
||||
const countWifiGuests = summaries.reduce((sum, { summary }) => sum + summary.wifi.guests, 0);
|
||||
@@ -55,7 +56,7 @@ export default function NetworkControllerNetworkStatusWidget({
|
||||
{queryIndicators}
|
||||
<Center p="sm" style={{ flex: 1 }}>
|
||||
<Text c="dimmed" size="sm" ta="center">
|
||||
{summaryQuery.isPending ? tCommon("action.loading") : t("error.integrationsDisconnected")}
|
||||
{summaryQuery.isPending ? tCommon("action.loading") : tWidgetCommon("integrationDisconnected")}
|
||||
</Text>
|
||||
</Center>
|
||||
</Stack>
|
||||
@@ -77,7 +78,7 @@ export default function NetworkControllerNetworkStatusWidget({
|
||||
</Text>
|
||||
{updatedAt && (
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("advanced.updatedAt", { date: new Date(updatedAt).toLocaleString(locale) })}
|
||||
{tWidgetCommon("updatedAt", { date: new Date(updatedAt).toLocaleString(locale) })}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
Loaded 100 of 449 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user