mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 22:25:24 +00:00
feat: finish the homepage plugin, move service links and canvas out of core (B19)
This commit is contained in:
1 parent
21937a6b02
commit
d4ec444558
256 files changed
+27365
-9077
No files matched your search
@@ -22,7 +22,7 @@ function DockerWidget({
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full gap-2 text-muted-foreground/60">
|
||||
<Box size={20} />
|
||||
<span className="text-xs">{t("homepage.widgetNoHostSelected")}</span>
|
||||
<span className="text-xs">{t("common.noHostConfigured")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export function DockerWidgetEditForm({
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.host")}
|
||||
{t("common.host")}
|
||||
</label>
|
||||
<Select2
|
||||
value={config.hostId || ""}
|
||||
@@ -25,7 +25,7 @@ export function DockerWidgetEditForm({
|
||||
}
|
||||
className="h-8 text-xs border border-border bg-background px-2"
|
||||
>
|
||||
<option value="">{t("homepage.selectHost")}</option>
|
||||
<option value="">{t("common.selectHost")}</option>
|
||||
{options.map((host) => (
|
||||
<option key={host.id} value={host.id}>
|
||||
{host.name || host.ip}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { FolderSearch } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHost } from "@termix/plugin-sdk/frontend";
|
||||
import { WidgetTitle } from "@termix/plugin-sdk/ui";
|
||||
import type {
|
||||
FileManagerWidgetConfig,
|
||||
WidgetComponentProps,
|
||||
} from "@/types/homepage-types";
|
||||
import { getSSHHosts } from "@/api/ssh-host-management-api";
|
||||
import type { SSHHostWithStatus } from "@/main-axios";
|
||||
} from "./homepage.js";
|
||||
import type { SSHHost } from "@/types/index";
|
||||
import { WidgetTitle } from "@/features/homepage/widgets/WidgetTitle";
|
||||
import { FileManager } from "../FileManager";
|
||||
|
||||
export function FileManagerWidget({
|
||||
@@ -16,36 +14,13 @@ export function FileManagerWidget({
|
||||
config,
|
||||
}: WidgetComponentProps<FileManagerWidgetConfig>) {
|
||||
const { t } = useTranslation();
|
||||
const [host, setHost] = useState<SSHHostWithStatus | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!config.hostId) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
getSSHHosts()
|
||||
.then((hosts) => {
|
||||
const found = hosts.find((h) => h.id === config.hostId);
|
||||
setHost(found ?? null);
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false));
|
||||
}, [config.hostId]);
|
||||
const host = useHost(config.hostId || undefined);
|
||||
|
||||
if (!config.hostId) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full gap-2 text-muted-foreground/60">
|
||||
<FolderSearch size={20} />
|
||||
<span className="text-xs">{t("homepage.widgetNoHostSelected")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full h-full text-xs text-muted-foreground/60">
|
||||
{t("homepage.loading")}
|
||||
<span className="text-xs">{t("common.noHostConfigured")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -53,7 +28,7 @@ export function FileManagerWidget({
|
||||
if (!host) {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full h-full text-xs text-muted-foreground/60">
|
||||
{t("homepage.widgetNoHostSelected")}
|
||||
{t("common.noHostConfigured")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,39 @@
|
||||
import { useHosts, useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Select2 } from "@termix/plugin-sdk/ui";
|
||||
import type {
|
||||
FileManagerWidgetConfig,
|
||||
WidgetEditFormProps,
|
||||
} from "@/types/homepage-types";
|
||||
import { SingleHostEditForm } from "@/features/homepage/dialogs/SingleHostEditForm";
|
||||
} from "./homepage.js";
|
||||
|
||||
export function FileManagerWidgetEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<FileManagerWidgetConfig>) {
|
||||
const { t } = useTranslation();
|
||||
const { hosts } = useHosts();
|
||||
const options = hosts.filter(
|
||||
(host) => host.enableSsh !== false && host.enableFileManager !== false,
|
||||
);
|
||||
|
||||
return (
|
||||
<SingleHostEditForm
|
||||
hostId={config.hostId}
|
||||
onChange={(hostId) => onChange({ ...config, hostId })}
|
||||
filter={(h) => !!(h.enableSsh && h.enableFileManager)}
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("common.host")}
|
||||
</label>
|
||||
<Select2
|
||||
value={config.hostId || ""}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, hostId: Number(e.target.value) })
|
||||
}
|
||||
className="h-8 text-xs border border-border bg-background px-2"
|
||||
>
|
||||
<option value="">{t("common.selectHost")}</option>
|
||||
{options.map((host) => (
|
||||
<option key={host.id} value={host.id}>
|
||||
{host.name || host.ip}
|
||||
</option>
|
||||
))}
|
||||
</Select2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { HomepageWidgetContribution } from "@termix/plugin-sdk/frontend";
|
||||
|
||||
/** The homepage's grid step, in pixels. */
|
||||
export const GRID_SIZE = 30;
|
||||
|
||||
export interface FileManagerWidgetConfig {
|
||||
hostId: number;
|
||||
}
|
||||
|
||||
/** What the homepage hands a widget; only the fields this plugin reads. */
|
||||
export interface WidgetComponentProps<C> {
|
||||
widget: { title?: string | null };
|
||||
config: C;
|
||||
}
|
||||
|
||||
export interface WidgetEditFormProps<C> {
|
||||
config: C;
|
||||
onChange: (config: C) => void;
|
||||
}
|
||||
|
||||
export type WidgetDefinition<C> = Omit<
|
||||
HomepageWidgetContribution<C>,
|
||||
"defaultConfig"
|
||||
> & { defaultConfig: C };
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type TabProps,
|
||||
type TermixApp,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { GRID_SIZE } from "@/types/homepage-types";
|
||||
import { GRID_SIZE } from "./homepage/homepage.js";
|
||||
import { FileManager } from "./FileManager.tsx";
|
||||
import FileManagerApp from "./FileManagerApp.tsx";
|
||||
import { SftpTransferTab } from "./SftpTransferTab.tsx";
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
## 1.0.0
|
||||
|
||||
Bundled with Termix 2.9.0.
|
||||
|
||||
- Homepage and the dashboard's service links moved out of core's own
|
||||
servers on ports 30006 (dashboard) and 30012 (homepage), and now serve
|
||||
under `/plugin-api/homepage`.
|
||||
- Owns its data: core's `homepage_items`, `homepage_layouts` and
|
||||
`dashboard_service_links` tables are renamed to `p_homepage_*` on first
|
||||
activation.
|
||||
- The favicon, RSS, ping and custom-API outbound requests now go through
|
||||
`ctx.fetch` instead of a raw fetch, keeping the same SSRF guard.
|
||||
- New `homepage.use` permission, given to the admin and user roles.
|
||||
- Offers `homepage.items` v1 to other plugins; the AI assistant reads it
|
||||
optionally.
|
||||
- The dashboard's "Dashboard / Homepage" toggle is now a contribution to
|
||||
core's generic `dashboard.secondaryView` slot rather than a hardcoded
|
||||
homepage case.
|
||||
- Core's uptime, recent-activity and database-health routes stayed on the
|
||||
main server, now under `/dashboard` rather than a dedicated port.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Homepage
|
||||
|
||||
A customizable widget canvas (clock, notes, host status, RSS feeds, an
|
||||
embedded terminal, and more) plus the dashboard's service link buttons.
|
||||
|
||||
Bundled with Termix and enabled by default. It imports only
|
||||
`@termix/plugin-sdk`.
|
||||
|
||||
## What it does
|
||||
|
||||
- **The canvas**: a pannable, zoomable grid of widgets, opened as its own
|
||||
tab (singleton, `?view=homepage`) or from the command palette. Widgets
|
||||
save their own layout and config.
|
||||
- **Widget types**: clock, notes, markdown notes, folders, host status,
|
||||
host grid, bookmarks, weather, iframe embed, RSS feed, ping status,
|
||||
recent activity, Termix uptime, system overview, an embedded SSH terminal,
|
||||
quick connect, calendar, countdown, search bar, text banner, image,
|
||||
custom API, service grid, dashboard links, search shortcuts and link
|
||||
trees. Other plugins (docker, tunnels, file-manager, host-metrics) add
|
||||
their own widget types through `app.registerHomepageWidget`; the widget
|
||||
type registry itself lives in core
|
||||
(`src/ui/plugin-host/homepage-widget-registry.ts`) so those widgets keep
|
||||
working with this plugin disabled.
|
||||
- **Dashboard cards**: `service_links` (clickable service URL buttons) and
|
||||
`homepage_preview` (a read-only scaled preview of the canvas), both
|
||||
offered in the dashboard's card tray. The dashboard's "Dashboard /
|
||||
Homepage" toggle is a contribution to core's generic
|
||||
`dashboard.secondaryView` slot.
|
||||
- **Outbound requests**: the favicon, RSS, ping and custom-API widgets fetch
|
||||
through `ctx.fetch`, which keeps the SSRF guard (private and loopback
|
||||
addresses refused, no redirects, DNS pinned) every other plugin's outbound
|
||||
call gets.
|
||||
- **`homepage.items`**: a service the AI assistant reads optionally, to list
|
||||
a user's homepage tiles.
|
||||
|
||||
## Layout
|
||||
|
||||
manifest.json id, capabilities and what this plugin contributes
|
||||
src/backend/ activate(ctx), routes, the outbound proxy routes
|
||||
src/frontend/ the canvas, widgets, dialogs and dashboard cards
|
||||
migrations/ one .sql per dialect, generated from src/backend/tables.ts
|
||||
locales/en.json its strings (English only; the rest are translated)
|
||||
tests/backend/ vitest, node
|
||||
tests/frontend/ vitest, jsdom
|
||||
|
||||
## Data
|
||||
|
||||
- `p_homepage_homepage_items`, `p_homepage_homepage_layouts` and
|
||||
`p_homepage_dashboard_service_links`, adopted from core's `homepage_items`,
|
||||
`homepage_layouts` and `dashboard_service_links`.
|
||||
|
||||
Core's own uptime, recent-activity and database-health routes are unrelated
|
||||
to the canvas and stay on the main server
|
||||
(`src/backend/database/routes/dashboard-routes.ts`, mounted at
|
||||
`/dashboard`).
|
||||
|
||||
See packages/plugin-sdk/ARCHITECTURE.md for the contract this follows.
|
||||
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Homepage"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Service Links",
|
||||
"homepagePreview": "Homepage Preview"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Service Links",
|
||||
"serviceLinksEmpty": "No service links yet",
|
||||
"serviceLinksAdd": "Add",
|
||||
"serviceLinksLabelPlaceholder": "My Service",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Enter a valid web address",
|
||||
"serviceLinksAddFailed": "Failed to add service link"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Homepage",
|
||||
"addWidget": "Add Widget",
|
||||
"editWidget": "Edit Widget",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out",
|
||||
"resetView": "Reset View",
|
||||
"lockLayout": "Lock Layout",
|
||||
"unlockLayout": "Unlock Layout",
|
||||
"noWidgets": "Right-click or click + to add your first widget",
|
||||
"openFullView": "Open Full View",
|
||||
"previewTitle": "Homepage Preview",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"title_label": "Title",
|
||||
"widgetTitlePlaceholder": "Widget title (optional)",
|
||||
"url": "URL",
|
||||
"imageUrl": "Custom Image URL",
|
||||
"imageUrlHint": "Leave blank to use the site favicon automatically",
|
||||
"showImage": "Show Image",
|
||||
"description": "Description",
|
||||
"color": "Color",
|
||||
"timezone": "Timezone",
|
||||
"invalidTimezone": "Invalid timezone. Use an IANA name such as America/New_York",
|
||||
"showSeconds": "Show seconds",
|
||||
"format12h": "12-hour",
|
||||
"content": "Content",
|
||||
"backgroundColor": "Background Color",
|
||||
"links": "Links",
|
||||
"addLink": "Add Link",
|
||||
"linkLabel": "Label",
|
||||
"categoryLinks": "Links",
|
||||
"categoryInfo": "Info",
|
||||
"categorySystem": "System",
|
||||
"copyLink": "Copy Link",
|
||||
"linkCopied": "Link copied!",
|
||||
"location": "Location",
|
||||
"temperatureUnit": "Temperature Unit",
|
||||
"showForecast": "Show 3-day forecast",
|
||||
"scrolling": "Allow Scrolling",
|
||||
"feedUrl": "Feed URL",
|
||||
"maxItems": "Max Items",
|
||||
"showDescription": "Show Description",
|
||||
"widgetRssName": "RSS Feed",
|
||||
"rssNoFeedUrl": "Configure a feed URL in widget settings",
|
||||
"rssLoadError": "Could not load feed",
|
||||
"rssNoItems": "No items",
|
||||
"noHostSelected": "No host selected",
|
||||
"displayedMetrics": "Displayed Metrics",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "Memory",
|
||||
"metricDisk": "Disk",
|
||||
"metricUptime": "Uptime",
|
||||
"metricSystem": "System",
|
||||
"metricNetwork": "Network",
|
||||
"metricProcesses": "Processes",
|
||||
"categoryMonitoring": "Monitoring",
|
||||
"noPingUrls": "No URLs configured",
|
||||
"pingLabel": "Label",
|
||||
"addPingUrl": "Add URL",
|
||||
"showLatency": "Show Latency",
|
||||
"refreshInterval": "Refresh Interval",
|
||||
"seconds": "seconds",
|
||||
"filterActivityTypes": "Filter Types",
|
||||
"filterTypesHint": "Leave empty to show all",
|
||||
"showTimestamp": "Show Timestamp",
|
||||
"uptimeUnavailable": "Uptime unavailable",
|
||||
"uptimeLabel": "Uptime",
|
||||
"overviewVersion": "Version",
|
||||
"overviewUpdateLabel": "Update",
|
||||
"overviewUpdateAvailable": "Update available",
|
||||
"overviewDatabase": "Database",
|
||||
"overviewUptime": "Uptime",
|
||||
"noHosts": "No hosts configured",
|
||||
"hostGridHosts": "Hosts",
|
||||
"hostGridAllHint": "Leave empty to show all hosts",
|
||||
"columns": "Columns",
|
||||
"showIp": "Show IP Address",
|
||||
"layout": "Layout",
|
||||
"showStatus": "Show Status",
|
||||
"noActivity": "No activity",
|
||||
"widgetPingStatusName": "Ping Status",
|
||||
"widgetRecentActivityName": "Recent Activity",
|
||||
"sshTerminalNoHost": "No host configured",
|
||||
"sshTerminalConnect": "Connect",
|
||||
"sshTerminalAutoConnect": "Auto-connect on load",
|
||||
"connectionTypes": "Connection Types",
|
||||
"calMon": "Mo",
|
||||
"calTue": "Tu",
|
||||
"calWed": "We",
|
||||
"calThu": "Th",
|
||||
"calFri": "Fr",
|
||||
"calSat": "Sa",
|
||||
"calSun": "Su",
|
||||
"startOnMonday": "Start week on Monday",
|
||||
"countdownNoDate": "No target date set",
|
||||
"countdownPast": "Event has passed",
|
||||
"countdownDays": "days",
|
||||
"countdownHours": "hrs",
|
||||
"countdownMinutes": "min",
|
||||
"countdownSeconds": "sec",
|
||||
"countdownLabel": "Label",
|
||||
"countdownLabelPlaceholder": "e.g. Launch Day",
|
||||
"countdownShowDays": "Show Days",
|
||||
"countdownShowHours": "Show Hours",
|
||||
"targetDate": "Target Date",
|
||||
"searchEngine": "Search Engine",
|
||||
"customSearchUrl": "Custom Search URL",
|
||||
"searchPlaceholder": "Search...",
|
||||
"searchGo": "Go",
|
||||
"searchPlaceholderLabel": "Placeholder Text",
|
||||
"searchPlaceholderHint": "Text shown inside the search input",
|
||||
"openInNewTab": "Open in New Tab",
|
||||
"searchQueryPlaceholder": "Enter query...",
|
||||
"noSearchShortcuts": "No shortcuts configured",
|
||||
"addSearchShortcut": "Add Shortcut",
|
||||
"fontSize": "Font Size",
|
||||
"textAlign": "Text Align",
|
||||
"fontWeight": "Font Weight",
|
||||
"clearColor": "Clear Color",
|
||||
"imageFit": "Image Fit",
|
||||
"imageLinkUrl": "Link URL",
|
||||
"noImage": "No image URL set",
|
||||
"altText": "Alt Text",
|
||||
"altTextPlaceholder": "Describe the image",
|
||||
"renderMarkdown": "Render Markdown",
|
||||
"displayMode": "Display Mode",
|
||||
"displayField": "Display Field",
|
||||
"jsonPath": "JSON Path",
|
||||
"customApiLabel": "Label",
|
||||
"customApiLabelPlaceholder": "e.g. Temperature",
|
||||
"customApiUnit": "Unit",
|
||||
"customApiNoUrl": "No API URL configured",
|
||||
"customApiError": "Failed to fetch",
|
||||
"customApiNotArray": "Response is not an array",
|
||||
"addService": "Add Service",
|
||||
"showLabels": "Show Labels",
|
||||
"iconSize": "Icon Size",
|
||||
"noDashboardLinks": "No dashboard links configured",
|
||||
"noLimit": "No limit",
|
||||
"sectionHeading": "Section Heading",
|
||||
"addSection": "Add Section",
|
||||
"compactMode": "Compact Mode",
|
||||
"selectHosts": "Select Hosts",
|
||||
"listLayout": "List",
|
||||
"gridLayout": "Grid"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Use the homepage",
|
||||
"description": "View and edit the homepage canvas and dashboard service links."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Tuisblad"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Diens skakels",
|
||||
"homepagePreview": "Tuisbladvoorskou"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Diens skakels",
|
||||
"serviceLinksEmpty": "Geen diens skakels nog nie",
|
||||
"serviceLinksAdd": "Voeg by",
|
||||
"serviceLinksLabelPlaceholder": "My Diens",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Voer 'n geldige webadres in",
|
||||
"serviceLinksAddFailed": "Kon nie diensskakel byvoeg nie"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Tuisblad",
|
||||
"addWidget": "Voeg Widget by",
|
||||
"editWidget": "Wysig Widget",
|
||||
"zoomIn": "Inzoem",
|
||||
"zoomOut": "Uitzoom",
|
||||
"resetView": "Herstel aansig",
|
||||
"lockLayout": "Sluituitleg",
|
||||
"unlockLayout": "Ontsluit uitleg",
|
||||
"noWidgets": "Regskliek of klik + om jou eerste widget by te voeg",
|
||||
"openFullView": "Maak volle aansig oop",
|
||||
"previewTitle": "Tuisbladvoorskou",
|
||||
"cancel": "Kanselleer",
|
||||
"save": "Stoor",
|
||||
"title_label": "Titel",
|
||||
"widgetTitlePlaceholder": "Widgettitel (opsioneel)",
|
||||
"url": "URL",
|
||||
"imageUrl": "Pasgemaakte beeld-URL",
|
||||
"imageUrlHint": "Los leeg om die webwerf se gunstelingikoon outomaties te gebruik",
|
||||
"showImage": "Wys Beeld",
|
||||
"description": "Beskrywing",
|
||||
"color": "Kleur",
|
||||
"timezone": "Tydsone",
|
||||
"invalidTimezone": "Ongeldige tydsone. Gebruik 'n IANA-naam soos Amerika/New_York",
|
||||
"showSeconds": "Wys sekondes",
|
||||
"format12h": "12-uur",
|
||||
"content": "Inhoud",
|
||||
"backgroundColor": "Agtergrondkleur",
|
||||
"links": "Skakels",
|
||||
"addLink": "Voeg skakel by",
|
||||
"linkLabel": "Etiket",
|
||||
"categoryLinks": "Skakels",
|
||||
"categoryInfo": "Inligting",
|
||||
"categorySystem": "Stelsel",
|
||||
"copyLink": "Kopieer skakel",
|
||||
"linkCopied": "Skakel gekopieer!",
|
||||
"location": "Ligging",
|
||||
"temperatureUnit": "Temperatuureenheid",
|
||||
"showForecast": "Wys 3-dae voorspelling",
|
||||
"scrolling": "Laat blaai toe",
|
||||
"feedUrl": "Voer-URL",
|
||||
"maxItems": "Maksimum items",
|
||||
"showDescription": "Wys Beskrywing",
|
||||
"widgetRssName": "RSS-voer",
|
||||
"noHostSelected": "Geen gasheer gekies nie",
|
||||
"displayedMetrics": "Vertoonde Metrieke",
|
||||
"metricCpu": "SVE",
|
||||
"metricMemory": "Geheue",
|
||||
"metricDisk": "Skyf",
|
||||
"metricUptime": "Optyd",
|
||||
"metricSystem": "Stelsel",
|
||||
"metricNetwork": "Netwerk",
|
||||
"metricProcesses": "Prosesse",
|
||||
"categoryMonitoring": "Monitering",
|
||||
"noPingUrls": "Geen URL'e gekonfigureer nie",
|
||||
"pingLabel": "Etiket",
|
||||
"addPingUrl": "Voeg URL by",
|
||||
"showLatency": "Wys Latensie",
|
||||
"refreshInterval": "Verversingsinterval",
|
||||
"seconds": "sekondes",
|
||||
"filterActivityTypes": "Filtertipes",
|
||||
"filterTypesHint": "Los leeg om alles te wys",
|
||||
"showTimestamp": "Wys Tydstempel",
|
||||
"uptimeUnavailable": "Beskikbaar vir bedryfstyd",
|
||||
"uptimeLabel": "Optyd",
|
||||
"overviewVersion": "Weergawe",
|
||||
"overviewUpdateLabel": "Opdatering",
|
||||
"overviewUpdateAvailable": "Opdatering beskikbaar",
|
||||
"overviewDatabase": "Databasis",
|
||||
"overviewUptime": "Optyd",
|
||||
"noHosts": "Geen gashere gekonfigureer nie",
|
||||
"hostGridHosts": "Gashere",
|
||||
"hostGridAllHint": "Los leeg om alle gashere te wys",
|
||||
"columns": "Kolomme",
|
||||
"showIp": "Wys IP-adres",
|
||||
"layout": "Uitleg",
|
||||
"showStatus": "Wys Status",
|
||||
"noActivity": "Geen aktiwiteit nie",
|
||||
"widgetPingStatusName": "Ping-status",
|
||||
"widgetRecentActivityName": "Onlangse Aktiwiteit",
|
||||
"sshTerminalNoHost": "Geen gasheer gekonfigureer nie",
|
||||
"sshTerminalConnect": "Verbind",
|
||||
"sshTerminalAutoConnect": "Koppel outomaties tydens laai",
|
||||
"connectionTypes": "Verbindingstipes",
|
||||
"calMon": "Ma",
|
||||
"calTue": "Di",
|
||||
"calWed": "Ons",
|
||||
"calThu": "Do",
|
||||
"calFri": "Vr",
|
||||
"calSat": "Sa",
|
||||
"calSun": "So",
|
||||
"startOnMonday": "Begin week op Maandag",
|
||||
"countdownNoDate": "Geen teikendatum gestel nie",
|
||||
"countdownPast": "Gebeurtenis het verbygegaan",
|
||||
"countdownDays": "dae",
|
||||
"countdownHours": "ure",
|
||||
"countdownMinutes": "min",
|
||||
"countdownSeconds": "sek",
|
||||
"countdownLabel": "Etiket",
|
||||
"countdownLabelPlaceholder": "bv. Bekendstellingsdag",
|
||||
"countdownShowDays": "Skou Dae",
|
||||
"countdownShowHours": "Wys Ure",
|
||||
"targetDate": "Teikendatum",
|
||||
"searchEngine": "Soekenjin",
|
||||
"customSearchUrl": "Pasgemaakte soektog-URL",
|
||||
"searchPlaceholder": "Soek...",
|
||||
"searchGo": "Gaan",
|
||||
"searchPlaceholderLabel": "Plekhouerteks",
|
||||
"searchPlaceholderHint": "Teks wat binne die soekinvoer gewys word",
|
||||
"openInNewTab": "Maak oop in Nuwe Oortjie",
|
||||
"searchQueryPlaceholder": "Voer navraag in...",
|
||||
"noSearchShortcuts": "Geen kortpaaie gekonfigureer nie",
|
||||
"addSearchShortcut": "Voeg kortpad by",
|
||||
"fontSize": "Lettergrootte",
|
||||
"textAlign": "Teksbelyning",
|
||||
"fontWeight": "Lettertipe Gewig",
|
||||
"clearColor": "Duidelike Kleur",
|
||||
"imageFit": "Beeldpassing",
|
||||
"imageLinkUrl": "Skakel-URL",
|
||||
"noImage": "Geen beeld-URL gestel nie",
|
||||
"altText": "Alt-teks",
|
||||
"altTextPlaceholder": "Beskryf die beeld",
|
||||
"renderMarkdown": "Lewer Markdown",
|
||||
"displayMode": "Vertoonmodus",
|
||||
"displayField": "Vertoonveld",
|
||||
"jsonPath": "JSON-pad",
|
||||
"customApiLabel": "Etiket",
|
||||
"customApiLabelPlaceholder": "bv. Temperatuur",
|
||||
"customApiUnit": "Eenheid",
|
||||
"customApiNoUrl": "Geen API-URL gekonfigureer nie",
|
||||
"customApiError": "Kon nie haal nie",
|
||||
"customApiNotArray": "Antwoord is nie 'n skikking nie",
|
||||
"addService": "Voeg Diens by",
|
||||
"showLabels": "Wys etikette",
|
||||
"iconSize": "Ikoongrootte",
|
||||
"noDashboardLinks": "Geen dashboard-skakels gekonfigureer nie",
|
||||
"noLimit": "Geen limiet nie",
|
||||
"sectionHeading": "Afdelingsopskrif",
|
||||
"addSection": "Voeg Afdeling by",
|
||||
"compactMode": "Kompakte modus",
|
||||
"selectHosts": "Kies gashere",
|
||||
"listLayout": "Lys",
|
||||
"gridLayout": "Rooster"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "الصفحة الرئيسية"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "روابط الخدمات",
|
||||
"homepagePreview": "معاينة الصفحة الرئيسية"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "روابط الخدمات",
|
||||
"serviceLinksEmpty": "لا توجد روابط خدمات بعد",
|
||||
"serviceLinksAdd": "إضافة",
|
||||
"serviceLinksLabelPlaceholder": "خدمتي",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "أدخل عنوان ويب صالح",
|
||||
"serviceLinksAddFailed": "فشل في إضافة رابط الخدمة"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "الصفحة الرئيسية",
|
||||
"addWidget": "إضافة ويدجت",
|
||||
"editWidget": "تعديل ويدجت",
|
||||
"zoomIn": "تكبير",
|
||||
"zoomOut": "تصغير",
|
||||
"resetView": "إعادة تعيين العرض",
|
||||
"lockLayout": "قفل التخطيط",
|
||||
"unlockLayout": "إلغاء قفل التخطيط",
|
||||
"noWidgets": "انقر بزر الماوس الأيمن أو انقر على + لإضافة أداتك الأولى",
|
||||
"openFullView": "فتح العرض الكامل",
|
||||
"previewTitle": "معاينة الصفحة الرئيسية",
|
||||
"cancel": "إلغاء",
|
||||
"save": "حفظ",
|
||||
"title_label": "العنوان",
|
||||
"widgetTitlePlaceholder": "عنوان الأداة (اختياري)",
|
||||
"url": "URL",
|
||||
"imageUrl": "URL صورة مخصصة",
|
||||
"imageUrlHint": "اتركه فارغًا لاستخدام أيقونة الموقع تلقائيًا",
|
||||
"showImage": "إظهار الصورة",
|
||||
"description": "الوصف",
|
||||
"color": "اللون",
|
||||
"timezone": "المنطقة الزمنية",
|
||||
"invalidTimezone": "المنطقة الزمنية غير صالحة. استخدم اسمًا من IANA مثل America/New_York",
|
||||
"showSeconds": "عرض الثواني",
|
||||
"format12h": "12 ساعة",
|
||||
"content": "المحتوى",
|
||||
"backgroundColor": "لون الخلفية",
|
||||
"links": "روابط",
|
||||
"addLink": "إضافة رابط",
|
||||
"linkLabel": "التسمية",
|
||||
"categoryLinks": "روابط",
|
||||
"categoryInfo": "معلومات",
|
||||
"categorySystem": "النظام",
|
||||
"copyLink": "نسخ الرابط",
|
||||
"linkCopied": "تم نسخ الرابط!",
|
||||
"location": "الموقع",
|
||||
"temperatureUnit": "وحدة درجة الحرارة",
|
||||
"showForecast": "إظهار توقعات 3 أيام",
|
||||
"scrolling": "السماح بالتمرير",
|
||||
"feedUrl": "رابط التغذية",
|
||||
"maxItems": "الحد الأقصى للعناصر",
|
||||
"showDescription": "إظهار الوصف",
|
||||
"widgetRssName": "تغذية RSS",
|
||||
"noHostSelected": "لم يتم تحديد أي مضيف",
|
||||
"displayedMetrics": "المقاييس المعروضة",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "الذاكرة",
|
||||
"metricDisk": "القرص",
|
||||
"metricUptime": "وقت التشغيل",
|
||||
"metricSystem": "النظام",
|
||||
"metricNetwork": "الشبكة",
|
||||
"metricProcesses": "العمليات",
|
||||
"categoryMonitoring": "المراقبة",
|
||||
"noPingUrls": "لم يتم تكوين عناوين URL",
|
||||
"pingLabel": "التسمية",
|
||||
"addPingUrl": "إضافة URL",
|
||||
"showLatency": "إظهار زمن الانتقال",
|
||||
"refreshInterval": "فترة التحديث",
|
||||
"seconds": "ثانية",
|
||||
"filterActivityTypes": "أنواع التصفية",
|
||||
"filterTypesHint": "اترك فارغاً لإظهار الكل",
|
||||
"showTimestamp": "إظهار الطابع الزمني",
|
||||
"uptimeUnavailable": "وقت التشغيل غير متوفر",
|
||||
"uptimeLabel": "وقت التشغيل",
|
||||
"overviewVersion": "الإصدار",
|
||||
"overviewUpdateLabel": "تحديث",
|
||||
"overviewUpdateAvailable": "تحديث متوفر",
|
||||
"overviewDatabase": "قاعدة البيانات",
|
||||
"overviewUptime": "وقت التشغيل",
|
||||
"noHosts": "لا توجد مضيفات مُهيأة",
|
||||
"hostGridHosts": "المضيفات",
|
||||
"hostGridAllHint": "اتركه فارغًا لعرض جميع المضيفات",
|
||||
"columns": "الأعمدة",
|
||||
"showIp": "إظهار عنوان IP",
|
||||
"layout": "التخطيط",
|
||||
"showStatus": "إظهار الحالة",
|
||||
"noActivity": "لا يوجد نشاط",
|
||||
"widgetPingStatusName": "حالة Ping",
|
||||
"widgetRecentActivityName": "النشاط الأخير",
|
||||
"sshTerminalNoHost": "لم يتم إعداد مضيف",
|
||||
"sshTerminalConnect": "اتصل",
|
||||
"sshTerminalAutoConnect": "اتصال تلقائي عند التحميل",
|
||||
"connectionTypes": "أنواع الاتصال",
|
||||
"calMon": "إثنين",
|
||||
"calTue": "ثلاثاء",
|
||||
"calWed": "أربعاء",
|
||||
"calThu": "خميس",
|
||||
"calFri": "جمعة",
|
||||
"calSat": "سبت",
|
||||
"calSun": "أحد",
|
||||
"startOnMonday": "بدء الأسبوع يوم الإثنين",
|
||||
"countdownNoDate": "لم يتم تحديد تاريخ مستهدف",
|
||||
"countdownPast": "انقضى الحدث",
|
||||
"countdownDays": "أيام",
|
||||
"countdownHours": "ساعات",
|
||||
"countdownMinutes": "دقائق",
|
||||
"countdownSeconds": "ثوانٍ",
|
||||
"countdownLabel": "تسمية",
|
||||
"countdownLabelPlaceholder": "مثال: يوم الإطلاق",
|
||||
"countdownShowDays": "إظهار الأيام",
|
||||
"countdownShowHours": "إظهار الساعات",
|
||||
"targetDate": "التاريخ المستهدف",
|
||||
"searchEngine": "محرك البحث",
|
||||
"customSearchUrl": "عنوان URL بحث مخصص",
|
||||
"searchPlaceholder": "بحث...",
|
||||
"searchGo": "تنفيذ",
|
||||
"searchPlaceholderLabel": "نص العنصر النائب",
|
||||
"searchPlaceholderHint": "النص المعروض داخل حقل البحث",
|
||||
"openInNewTab": "فتح في علامة تبويب جديدة",
|
||||
"searchQueryPlaceholder": "أدخل الاستعلام...",
|
||||
"noSearchShortcuts": "لا توجد اختصارات مكونة",
|
||||
"addSearchShortcut": "إضافة اختصار",
|
||||
"fontSize": "حجم الخط",
|
||||
"textAlign": "محاذاة النص",
|
||||
"fontWeight": "وزن الخط",
|
||||
"clearColor": "مسح اللون",
|
||||
"imageFit": "ملاءمة الصورة",
|
||||
"imageLinkUrl": "رابط URL",
|
||||
"noImage": "لم يتم تعيين عنوان URL للصورة",
|
||||
"altText": "نص بديل",
|
||||
"altTextPlaceholder": "وصف الصورة",
|
||||
"renderMarkdown": "عرض Markdown",
|
||||
"displayMode": "وضع العرض",
|
||||
"displayField": "حقل العرض",
|
||||
"jsonPath": "مسار JSON",
|
||||
"customApiLabel": "تسمية",
|
||||
"customApiLabelPlaceholder": "مثال: درجة الحرارة",
|
||||
"customApiUnit": "وحدة",
|
||||
"customApiNoUrl": "لم يتم تكوين عنوان URL API",
|
||||
"customApiError": "فشل في الجلب",
|
||||
"customApiNotArray": "الاستجابة ليست مصفوفة",
|
||||
"addService": "إضافة خدمة",
|
||||
"showLabels": "إظهار التسميات",
|
||||
"iconSize": "حجم الأيقونة",
|
||||
"noDashboardLinks": "لا توجد روابط لوحة تحكم مُعدة",
|
||||
"noLimit": "غير محدود",
|
||||
"sectionHeading": "عنوان القسم",
|
||||
"addSection": "إضافة قسم",
|
||||
"compactMode": "الوضع المضغوط",
|
||||
"selectHosts": "اختيار المضيفين",
|
||||
"listLayout": "قائمة",
|
||||
"gridLayout": "شبكة"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Начална страница"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Връзки към услуги",
|
||||
"homepagePreview": "Преглед на началната страница"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Връзки към услуги",
|
||||
"serviceLinksEmpty": "Все още няма връзки към услуги",
|
||||
"serviceLinksAdd": "Добавяне",
|
||||
"serviceLinksLabelPlaceholder": "Моята услуга",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Въведете валиден уеб адрес",
|
||||
"serviceLinksAddFailed": "Добавянето на линк към услугата не бе успешно"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Начална страница",
|
||||
"addWidget": "Добавяне на джаджа",
|
||||
"editWidget": "Редактиране на уиджет",
|
||||
"zoomIn": "Увеличаване на мащаба",
|
||||
"zoomOut": "Намаляване на мащаба",
|
||||
"resetView": "Нулиране на изгледа",
|
||||
"lockLayout": "Заключване на оформлението",
|
||||
"unlockLayout": "Отключване на оформлението",
|
||||
"noWidgets": "Щракнете с десния бутон или щракнете върху +, за да добавите първия си уиджет",
|
||||
"openFullView": "Отваряне на пълен изглед",
|
||||
"previewTitle": "Преглед на началната страница",
|
||||
"cancel": "Отказ",
|
||||
"save": "Запазване",
|
||||
"title_label": "Заглавие",
|
||||
"widgetTitlePlaceholder": "Заглавие на уиджета (по избор)",
|
||||
"url": "URL адрес",
|
||||
"imageUrl": "URL адрес на персонализирано изображение",
|
||||
"imageUrlHint": "Оставете празно, за да използвате автоматично фавиконката на сайта",
|
||||
"showImage": "Показване на изображение",
|
||||
"description": "Описание",
|
||||
"color": "Цвят",
|
||||
"timezone": "Часова зона",
|
||||
"invalidTimezone": "Невалидна часова зона. Използвайте име от IANA, като например America/New_York.",
|
||||
"showSeconds": "Показване на секунди",
|
||||
"format12h": "12-часов",
|
||||
"content": "Съдържание",
|
||||
"backgroundColor": "Цвят на фона",
|
||||
"links": "Връзки",
|
||||
"addLink": "Добавяне на връзка",
|
||||
"linkLabel": "Етикет",
|
||||
"categoryLinks": "Връзки",
|
||||
"categoryInfo": "Информация",
|
||||
"categorySystem": "Система",
|
||||
"copyLink": "Копиране на връзката",
|
||||
"linkCopied": "Линкът е копиран!",
|
||||
"location": "Местоположение",
|
||||
"temperatureUnit": "Температурна единица",
|
||||
"showForecast": "Показване на 3-дневна прогноза",
|
||||
"scrolling": "Разрешаване на превъртане",
|
||||
"feedUrl": "URL адрес на емисията",
|
||||
"maxItems": "Максимален брой елементи",
|
||||
"showDescription": "Покажи описание",
|
||||
"widgetRssName": "RSS емисия",
|
||||
"noHostSelected": "Няма избран хост",
|
||||
"displayedMetrics": "Показани показатели",
|
||||
"metricCpu": "Процесор",
|
||||
"metricMemory": "Памет",
|
||||
"metricDisk": "Диск",
|
||||
"metricUptime": "Време на работа",
|
||||
"metricSystem": "Система",
|
||||
"metricNetwork": "Мрежа",
|
||||
"metricProcesses": "Процеси",
|
||||
"categoryMonitoring": "Мониторинг",
|
||||
"noPingUrls": "Няма конфигурирани URL адреси",
|
||||
"pingLabel": "Етикет",
|
||||
"addPingUrl": "Добавяне на URL адрес",
|
||||
"showLatency": "Покажи латентност",
|
||||
"refreshInterval": "Интервал на обновяване",
|
||||
"seconds": "секунди",
|
||||
"filterActivityTypes": "Видове филтри",
|
||||
"filterTypesHint": "Оставете празно, за да се покажат всички",
|
||||
"showTimestamp": "Показване на времева маркировка",
|
||||
"uptimeUnavailable": "Не е налично време за работа",
|
||||
"uptimeLabel": "Време на работа",
|
||||
"overviewVersion": "Версия",
|
||||
"overviewUpdateLabel": "Актуализация",
|
||||
"overviewUpdateAvailable": "Налична е актуализация",
|
||||
"overviewDatabase": "База данни",
|
||||
"overviewUptime": "Време на работа",
|
||||
"noHosts": "Няма конфигурирани хостове",
|
||||
"hostGridHosts": "Домакини",
|
||||
"hostGridAllHint": "Оставете празно, за да се покажат всички хостове",
|
||||
"columns": "Колони",
|
||||
"showIp": "Показване на IP адрес",
|
||||
"layout": "Оформление",
|
||||
"showStatus": "Покажи състоянието",
|
||||
"noActivity": "Няма активност",
|
||||
"widgetPingStatusName": "Състояние на пинг",
|
||||
"widgetRecentActivityName": "Последна активност",
|
||||
"sshTerminalNoHost": "Няма конфигуриран хост",
|
||||
"sshTerminalConnect": "Свържете се",
|
||||
"sshTerminalAutoConnect": "Автоматично свързване при зареждане",
|
||||
"connectionTypes": "Видове връзки",
|
||||
"calMon": "Мо",
|
||||
"calTue": "Вт",
|
||||
"calWed": "Ние",
|
||||
"calThu": "Четвъртък",
|
||||
"calFri": "Пт",
|
||||
"calSat": "Съ",
|
||||
"calSun": "Су",
|
||||
"startOnMonday": "Започнете седмицата в понеделник",
|
||||
"countdownNoDate": "Няма определена целева дата",
|
||||
"countdownPast": "Събитието е отминало",
|
||||
"countdownDays": "дни",
|
||||
"countdownHours": "часа",
|
||||
"countdownMinutes": "мин.",
|
||||
"countdownSeconds": "сек",
|
||||
"countdownLabel": "Етикет",
|
||||
"countdownLabelPlaceholder": "напр. Ден на стартиране",
|
||||
"countdownShowDays": "Дни на шоуто",
|
||||
"countdownShowHours": "Работно време на шоуто",
|
||||
"targetDate": "Целева дата",
|
||||
"searchEngine": "Търсачка",
|
||||
"customSearchUrl": "URL адрес за персонализирано търсене",
|
||||
"searchPlaceholder": "Търсене...",
|
||||
"searchGo": "Отиди",
|
||||
"searchPlaceholderLabel": "Текст на заместител",
|
||||
"searchPlaceholderHint": "Текст, показан във полето за търсене",
|
||||
"openInNewTab": "Отваряне в нов раздел",
|
||||
"searchQueryPlaceholder": "Въведете заявка...",
|
||||
"noSearchShortcuts": "Няма конфигурирани преки пътища",
|
||||
"addSearchShortcut": "Добавяне на пряк път",
|
||||
"fontSize": "Размер на шрифта",
|
||||
"textAlign": "Подравняване на текста",
|
||||
"fontWeight": "Тегло на шрифта",
|
||||
"clearColor": "Ясен цвят",
|
||||
"imageFit": "Напасване на изображението",
|
||||
"imageLinkUrl": "URL адрес на връзката",
|
||||
"noImage": "Няма зададен URL адрес на изображението",
|
||||
"altText": "Алтернативен текст",
|
||||
"altTextPlaceholder": "Опишете изображението",
|
||||
"renderMarkdown": "Рендиране на маркиране",
|
||||
"displayMode": "Режим на показване",
|
||||
"displayField": "Поле за показване",
|
||||
"jsonPath": "JSON път",
|
||||
"customApiLabel": "Етикет",
|
||||
"customApiLabelPlaceholder": "напр. Температура",
|
||||
"customApiUnit": "Единица",
|
||||
"customApiNoUrl": "Няма конфигуриран URL адрес на API",
|
||||
"customApiError": "Неуспешно извличане",
|
||||
"customApiNotArray": "Отговорът не е масив",
|
||||
"addService": "Добавяне на услуга",
|
||||
"showLabels": "Показване на етикети",
|
||||
"iconSize": "Размер на иконата",
|
||||
"noDashboardLinks": "Няма конфигурирани връзки към таблото за управление",
|
||||
"noLimit": "Без ограничение",
|
||||
"sectionHeading": "Заглавие на раздел",
|
||||
"addSection": "Добавяне на раздел",
|
||||
"compactMode": "Компактен режим",
|
||||
"selectHosts": "Изберете хостове",
|
||||
"listLayout": "Списък",
|
||||
"gridLayout": "Решетка"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "হোমপেজ"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "পরিষেবা লিঙ্ক",
|
||||
"homepagePreview": "হোমপেজ প্রিভিউ"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "পরিষেবা লিঙ্ক",
|
||||
"serviceLinksEmpty": "এখনও কোনো পরিষেবা লিঙ্ক নেই",
|
||||
"serviceLinksAdd": "যোগ করুন",
|
||||
"serviceLinksLabelPlaceholder": "আমার পরিষেবা",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "একটি বৈধ ওয়েব ঠিকানা লিখুন",
|
||||
"serviceLinksAddFailed": "পরিষেবা লিঙ্ক যোগ করতে ব্যর্থ হয়েছে"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "হোমপেজ",
|
||||
"addWidget": "উইজেট যোগ করুন",
|
||||
"editWidget": "উইজেট সম্পাদনা করুন",
|
||||
"zoomIn": "জুম ইন",
|
||||
"zoomOut": "জুম আউট",
|
||||
"resetView": "দৃশ্য রিসেট করুন",
|
||||
"lockLayout": "লক লেআউট",
|
||||
"unlockLayout": "লেআউট আনলক করুন",
|
||||
"noWidgets": "আপনার প্রথম উইজেট যোগ করতে ডান-ক্লিক করুন অথবা + এ ক্লিক করুন।",
|
||||
"openFullView": "সম্পূর্ণ দৃশ্য খুলুন",
|
||||
"previewTitle": "হোমপেজ প্রিভিউ",
|
||||
"cancel": "বাতিল করুন",
|
||||
"save": "সংরক্ষণ করুন",
|
||||
"title_label": "শিরোনাম",
|
||||
"widgetTitlePlaceholder": "উইজেটের শিরোনাম (ঐচ্ছিক)",
|
||||
"url": "ইউআরএল",
|
||||
"imageUrl": "কাস্টম ইমেজ ইউআরএল",
|
||||
"imageUrlHint": "সাইটের ফেভিকন স্বয়ংক্রিয়ভাবে ব্যবহার করতে খালি রাখুন।",
|
||||
"showImage": "ছবি দেখান",
|
||||
"description": "বর্ণনা",
|
||||
"color": "রঙ",
|
||||
"timezone": "সময় অঞ্চল",
|
||||
"invalidTimezone": "অবৈধ টাইমজোন। America/New_York-এর মতো একটি IANA নাম ব্যবহার করুন।",
|
||||
"showSeconds": "সেকেন্ড দেখান",
|
||||
"format12h": "১২ ঘন্টা",
|
||||
"content": "বিষয়বস্তু",
|
||||
"backgroundColor": "পটভূমির রঙ",
|
||||
"links": "লিঙ্ক",
|
||||
"addLink": "লিঙ্ক যোগ করুন",
|
||||
"linkLabel": "লেবেল",
|
||||
"categoryLinks": "লিঙ্ক",
|
||||
"categoryInfo": "তথ্য",
|
||||
"categorySystem": "সিস্টেম",
|
||||
"copyLink": "লিঙ্ক কপি করুন",
|
||||
"linkCopied": "লিঙ্ক কপি করা হয়েছে!",
|
||||
"location": "অবস্থান",
|
||||
"temperatureUnit": "তাপমাত্রা ইউনিট",
|
||||
"showForecast": "তিন দিনের পূর্বাভাস দেখান",
|
||||
"scrolling": "স্ক্রোল করার অনুমতি দিন",
|
||||
"feedUrl": "ফিড ইউআরএল",
|
||||
"maxItems": "সর্বোচ্চ আইটেম",
|
||||
"showDescription": "বিবরণ দেখান",
|
||||
"widgetRssName": "আরএসএস ফিড",
|
||||
"noHostSelected": "কোন হোস্ট নির্বাচন করা হয়নি",
|
||||
"displayedMetrics": "প্রদর্শিত মেট্রিক্স",
|
||||
"metricCpu": "সিপিইউ",
|
||||
"metricMemory": "স্মৃতি",
|
||||
"metricDisk": "ডিস্ক",
|
||||
"metricUptime": "আপটাইম",
|
||||
"metricSystem": "সিস্টেম",
|
||||
"metricNetwork": "নেটওয়ার্ক",
|
||||
"metricProcesses": "প্রক্রিয়া",
|
||||
"categoryMonitoring": "পর্যবেক্ষণ",
|
||||
"noPingUrls": "কোন URL কনফিগার করা নেই",
|
||||
"pingLabel": "লেবেল",
|
||||
"addPingUrl": "URL যোগ করুন",
|
||||
"showLatency": "লেটেন্সি দেখান",
|
||||
"refreshInterval": "রিফ্রেশ ব্যবধান",
|
||||
"seconds": "সেকেন্ড",
|
||||
"filterActivityTypes": "ফিল্টারের প্রকারভেদ",
|
||||
"filterTypesHint": "সবগুলো দেখাতে খালি রাখুন",
|
||||
"showTimestamp": "টাইমস্ট্যাম্প দেখান",
|
||||
"uptimeUnavailable": "আপটাইম অনুপলব্ধ",
|
||||
"uptimeLabel": "আপটাইম",
|
||||
"overviewVersion": "সংস্করণ",
|
||||
"overviewUpdateLabel": "আপডেট",
|
||||
"overviewUpdateAvailable": "আপডেট উপলব্ধ",
|
||||
"overviewDatabase": "ডাটাবেস",
|
||||
"overviewUptime": "আপটাইম",
|
||||
"noHosts": "কোন হোস্ট কনফিগার করা নেই",
|
||||
"hostGridHosts": "হোস্টরা",
|
||||
"hostGridAllHint": "সমস্ত হোস্ট দেখানোর জন্য খালি রাখুন",
|
||||
"columns": "কলাম",
|
||||
"showIp": "আইপি ঠিকানা দেখান",
|
||||
"layout": "লেআউট",
|
||||
"showStatus": "অবস্থা দেখান",
|
||||
"noActivity": "কোন কার্যকলাপ নেই",
|
||||
"widgetPingStatusName": "পিং স্ট্যাটাস",
|
||||
"widgetRecentActivityName": "সাম্প্রতিক কার্যকলাপ",
|
||||
"sshTerminalNoHost": "কোন হোস্ট কনফিগার করা নেই",
|
||||
"sshTerminalConnect": "সংযোগ করুন",
|
||||
"sshTerminalAutoConnect": "লোড হলে স্বয়ংক্রিয়ভাবে সংযোগ স্থাপন",
|
||||
"connectionTypes": "সংযোগের প্রকারভেদ",
|
||||
"calMon": "মো",
|
||||
"calTue": "তু",
|
||||
"calWed": "আমরা",
|
||||
"calThu": "থ",
|
||||
"calFri": "ফ্র",
|
||||
"calSat": "সা",
|
||||
"calSun": "সু",
|
||||
"startOnMonday": "সোমবার থেকে সপ্তাহ শুরু করুন",
|
||||
"countdownNoDate": "কোনো নির্দিষ্ট তারিখ নির্ধারণ করা হয়নি।",
|
||||
"countdownPast": "অনুষ্ঠানটি শেষ হয়ে গেছে।",
|
||||
"countdownDays": "দিন",
|
||||
"countdownHours": "ঘন্টা",
|
||||
"countdownMinutes": "মিনিট",
|
||||
"countdownSeconds": "সেকেন্ড",
|
||||
"countdownLabel": "লেবেল",
|
||||
"countdownLabelPlaceholder": "যেমন লঞ্চের দিন",
|
||||
"countdownShowDays": "শো ডেজ",
|
||||
"countdownShowHours": "শো আওয়ার্স",
|
||||
"targetDate": "লক্ষ্য তারিখ",
|
||||
"searchEngine": "সার্চ ইঞ্জিন",
|
||||
"customSearchUrl": "কাস্টম সার্চ ইউআরএল",
|
||||
"searchPlaceholder": "অনুসন্ধান করুন...",
|
||||
"searchGo": "যান",
|
||||
"searchPlaceholderLabel": "স্থানধারক পাঠ্য",
|
||||
"searchPlaceholderHint": "সার্চ ইনপুটের ভিতরে দেখানো টেক্সট",
|
||||
"openInNewTab": "নতুন ট্যাবে খুলুন",
|
||||
"searchQueryPlaceholder": "প্রশ্ন লিখুন...",
|
||||
"noSearchShortcuts": "কোন শর্টকাট কনফিগার করা নেই",
|
||||
"addSearchShortcut": "শর্টকাট যোগ করুন",
|
||||
"fontSize": "ফন্ট সাইজ",
|
||||
"textAlign": "টেক্সট অ্যালাইন",
|
||||
"fontWeight": "ফন্ট ওজন",
|
||||
"clearColor": "পরিষ্কার রঙ",
|
||||
"imageFit": "ইমেজ ফিট",
|
||||
"imageLinkUrl": "লিঙ্ক ইউআরএল",
|
||||
"noImage": "কোন ছবির ইউআরএল সেট করা হয়নি",
|
||||
"altText": "বিকল্প পাঠ্য",
|
||||
"altTextPlaceholder": "ছবিটি বর্ণনা করুন",
|
||||
"renderMarkdown": "রেন্ডার মার্কডাউন",
|
||||
"displayMode": "ডিসপ্লে মোড",
|
||||
"displayField": "ডিসপ্লে ফিল্ড",
|
||||
"jsonPath": "JSON পাথ",
|
||||
"customApiLabel": "লেবেল",
|
||||
"customApiLabelPlaceholder": "যেমন তাপমাত্রা",
|
||||
"customApiUnit": "ইউনিট",
|
||||
"customApiNoUrl": "কোন API URL কনফিগার করা নেই",
|
||||
"customApiError": "আনতে ব্যর্থ হয়েছে",
|
||||
"customApiNotArray": "প্রতিক্রিয়া একটি অ্যারে নয়",
|
||||
"addService": "পরিষেবা যোগ করুন",
|
||||
"showLabels": "লেবেল দেখান",
|
||||
"iconSize": "আইকনের আকার",
|
||||
"noDashboardLinks": "কোন ড্যাশবোর্ড লিঙ্ক কনফিগার করা নেই",
|
||||
"noLimit": "কোনো সীমা নেই",
|
||||
"sectionHeading": "বিভাগ শিরোনাম",
|
||||
"addSection": "বিভাগ যোগ করুন",
|
||||
"compactMode": "কমপ্যাক্ট মোড",
|
||||
"selectHosts": "হোস্ট নির্বাচন করুন",
|
||||
"listLayout": "তালিকা",
|
||||
"gridLayout": "গ্রিড"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Pàgina d'inici"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Enllaços de servei",
|
||||
"homepagePreview": "Vista prèvia de la pàgina d'inici"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Enllaços de servei",
|
||||
"serviceLinksEmpty": "Encara no hi ha enllaços de servei",
|
||||
"serviceLinksAdd": "Afegeix",
|
||||
"serviceLinksLabelPlaceholder": "El meu servei",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Introduïu una adreça web vàlida",
|
||||
"serviceLinksAddFailed": "No s'ha pogut afegir l'enllaç del servei"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Pàgina d'inici",
|
||||
"addWidget": "Afegeix un widget",
|
||||
"editWidget": "Edita el widget",
|
||||
"zoomIn": "Ampliar zoom",
|
||||
"zoomOut": "Allunyar",
|
||||
"resetView": "Restableix la vista",
|
||||
"lockLayout": "Disseny de bloqueig",
|
||||
"unlockLayout": "Desbloqueja el disseny",
|
||||
"noWidgets": "Feu clic amb el botó dret o feu clic a + per afegir el primer widget",
|
||||
"openFullView": "Obre la vista completa",
|
||||
"previewTitle": "Vista prèvia de la pàgina d'inici",
|
||||
"cancel": "Cancel·la",
|
||||
"save": "Desa",
|
||||
"title_label": "Títol",
|
||||
"widgetTitlePlaceholder": "Títol del widget (opcional)",
|
||||
"url": "URL",
|
||||
"imageUrl": "URL d'imatge personalitzada",
|
||||
"imageUrlHint": "Deixeu-ho en blanc per utilitzar la icona de favorits del lloc web automàticament",
|
||||
"showImage": "Mostra la imatge",
|
||||
"description": "Descripció",
|
||||
"color": "Color",
|
||||
"timezone": "Zona horària",
|
||||
"invalidTimezone": "Fus horari no vàlid. Feu servir un nom IANA com ara Amèrica/Nova_York.",
|
||||
"showSeconds": "Mostra els segons",
|
||||
"format12h": "12 hores",
|
||||
"content": "Contingut",
|
||||
"backgroundColor": "Color de fons",
|
||||
"links": "Enllaços",
|
||||
"addLink": "Afegeix un enllaç",
|
||||
"linkLabel": "Etiqueta",
|
||||
"categoryLinks": "Enllaços",
|
||||
"categoryInfo": "Informació",
|
||||
"categorySystem": "Sistema",
|
||||
"copyLink": "Copia l'enllaç",
|
||||
"linkCopied": "Enllaç copiat!",
|
||||
"location": "Ubicació",
|
||||
"temperatureUnit": "Unitat de temperatura",
|
||||
"showForecast": "Mostra la previsió de 3 dies",
|
||||
"scrolling": "Permet el desplaçament",
|
||||
"feedUrl": "URL del canal",
|
||||
"maxItems": "Màxim d'elements",
|
||||
"showDescription": "Mostra la descripció",
|
||||
"widgetRssName": "Canal RSS",
|
||||
"noHostSelected": "No s'ha seleccionat cap amfitrió",
|
||||
"displayedMetrics": "Mètriques mostrades",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "Memòria",
|
||||
"metricDisk": "Disc",
|
||||
"metricUptime": "Temps de funcionament",
|
||||
"metricSystem": "Sistema",
|
||||
"metricNetwork": "Xarxa",
|
||||
"metricProcesses": "Processos",
|
||||
"categoryMonitoring": "Monitorització",
|
||||
"noPingUrls": "No s'han configurat URL",
|
||||
"pingLabel": "Etiqueta",
|
||||
"addPingUrl": "Afegeix URL",
|
||||
"showLatency": "Mostra la latència",
|
||||
"refreshInterval": "Interval d'actualització",
|
||||
"seconds": "segons",
|
||||
"filterActivityTypes": "Tipus de filtre",
|
||||
"filterTypesHint": "Deixeu-ho en blanc per mostrar-ho tot",
|
||||
"showTimestamp": "Mostra la marca de temps",
|
||||
"uptimeUnavailable": "Temps de funcionament no disponible",
|
||||
"uptimeLabel": "Temps de funcionament",
|
||||
"overviewVersion": "Versió",
|
||||
"overviewUpdateLabel": "Actualització",
|
||||
"overviewUpdateAvailable": "Actualització disponible",
|
||||
"overviewDatabase": "Base de dades",
|
||||
"overviewUptime": "Temps de funcionament",
|
||||
"noHosts": "No hi ha cap amfitrió configurat",
|
||||
"hostGridHosts": "Amfitrions",
|
||||
"hostGridAllHint": "Deixeu-ho en blanc per mostrar tots els amfitrions",
|
||||
"columns": "Columnes",
|
||||
"showIp": "Mostra l'adreça IP",
|
||||
"layout": "Disseny",
|
||||
"showStatus": "Mostra l'estat",
|
||||
"noActivity": "Sense activitat",
|
||||
"widgetPingStatusName": "Estat del ping",
|
||||
"widgetRecentActivityName": "Activitat recent",
|
||||
"sshTerminalNoHost": "No hi ha cap amfitrió configurat",
|
||||
"sshTerminalConnect": "Connecta",
|
||||
"sshTerminalAutoConnect": "Connexió automàtica en càrrega",
|
||||
"connectionTypes": "Tipus de connexió",
|
||||
"calMon": "Mo",
|
||||
"calTue": "Dimarts",
|
||||
"calWed": "Nosaltres",
|
||||
"calThu": "Dj.",
|
||||
"calFri": "Div.",
|
||||
"calSat": "Sa",
|
||||
"calSun": "Dg.",
|
||||
"startOnMonday": "Comença la setmana dilluns",
|
||||
"countdownNoDate": "No hi ha cap data objectiu fixada",
|
||||
"countdownPast": "L'esdeveniment ha passat",
|
||||
"countdownDays": "dies",
|
||||
"countdownHours": "hores",
|
||||
"countdownMinutes": "min",
|
||||
"countdownSeconds": "segons",
|
||||
"countdownLabel": "Etiqueta",
|
||||
"countdownLabelPlaceholder": "p. ex. Dia del llançament",
|
||||
"countdownShowDays": "Dies d'espectacle",
|
||||
"countdownShowHours": "Horari de la mostra",
|
||||
"targetDate": "Data objectiu",
|
||||
"searchEngine": "Motor de cerca",
|
||||
"customSearchUrl": "URL de cerca personalitzada",
|
||||
"searchPlaceholder": "Cerca...",
|
||||
"searchGo": "Anar",
|
||||
"searchPlaceholderLabel": "Text de marcador de posició",
|
||||
"searchPlaceholderHint": "Text que es mostra dins del camp de cerca",
|
||||
"openInNewTab": "Obre en una pestanya nova",
|
||||
"searchQueryPlaceholder": "Introduïu la consulta...",
|
||||
"noSearchShortcuts": "No hi ha dreceres configurades",
|
||||
"addSearchShortcut": "Afegeix una drecera",
|
||||
"fontSize": "Mida de la lletra",
|
||||
"textAlign": "Alineació del text",
|
||||
"fontWeight": "Pes de la lletra",
|
||||
"clearColor": "Color clar",
|
||||
"imageFit": "Ajust d'imatge",
|
||||
"imageLinkUrl": "URL de l'enllaç",
|
||||
"noImage": "No s'ha definit cap URL d'imatge",
|
||||
"altText": "Text alternatiu",
|
||||
"altTextPlaceholder": "Descriu la imatge",
|
||||
"renderMarkdown": "Renderització Markdown",
|
||||
"displayMode": "Mode de visualització",
|
||||
"displayField": "Camp de visualització",
|
||||
"jsonPath": "Camí JSON",
|
||||
"customApiLabel": "Etiqueta",
|
||||
"customApiLabelPlaceholder": "p. ex. Temperatura",
|
||||
"customApiUnit": "Unitat",
|
||||
"customApiNoUrl": "No s'ha configurat cap URL de l'API",
|
||||
"customApiError": "No s'ha pogut obtenir",
|
||||
"customApiNotArray": "La resposta no és una matriu",
|
||||
"addService": "Afegeix un servei",
|
||||
"showLabels": "Mostra les etiquetes",
|
||||
"iconSize": "Mida de la icona",
|
||||
"noDashboardLinks": "No hi ha cap enllaç configurat al tauler de control",
|
||||
"noLimit": "Sense límit",
|
||||
"sectionHeading": "Encapçalament de secció",
|
||||
"addSection": "Afegeix secció",
|
||||
"compactMode": "Mode compacte",
|
||||
"selectHosts": "Selecciona els amfitrions",
|
||||
"listLayout": "Llista",
|
||||
"gridLayout": "Graella"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Domovská stránka"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Odkazy na služby",
|
||||
"homepagePreview": "Náhled domovské stránky"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Odkazy na služby",
|
||||
"serviceLinksEmpty": "Zatím žádné odkazy na služby",
|
||||
"serviceLinksAdd": "Přidat",
|
||||
"serviceLinksLabelPlaceholder": "Moje služba",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Zadejte platnou webovou adresu",
|
||||
"serviceLinksAddFailed": "Nepodařilo se přidat odkaz na službu"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Domovská stránka",
|
||||
"addWidget": "Přidat widget",
|
||||
"editWidget": "Upravit widget",
|
||||
"zoomIn": "Přiblížit",
|
||||
"zoomOut": "Oddálit",
|
||||
"resetView": "Obnovit zobrazení",
|
||||
"lockLayout": "Rozložení zámku",
|
||||
"unlockLayout": "Odemknout rozvržení",
|
||||
"noWidgets": "Klikněte pravým tlačítkem myši nebo kliknutím na + přidejte svůj první widget",
|
||||
"openFullView": "Otevřít celé zobrazení",
|
||||
"previewTitle": "Náhled domovské stránky",
|
||||
"cancel": "Zrušit",
|
||||
"save": "Uložit",
|
||||
"title_label": "Titul",
|
||||
"widgetTitlePlaceholder": "Název widgetu (volitelné)",
|
||||
"url": "URL",
|
||||
"imageUrl": "URL vlastního obrázku",
|
||||
"imageUrlHint": "Nechte pole prázdné, pokud chcete favicon webu používat automaticky.",
|
||||
"showImage": "Zobrazit obrázek",
|
||||
"description": "Popis",
|
||||
"color": "Barva",
|
||||
"timezone": "Časové pásmo",
|
||||
"invalidTimezone": "Neplatné časové pásmo. Použijte název IANA, například America/New_York.",
|
||||
"showSeconds": "Zobrazit sekundy",
|
||||
"format12h": "12 hodin",
|
||||
"content": "Obsah",
|
||||
"backgroundColor": "Barva pozadí",
|
||||
"links": "Odkazy",
|
||||
"addLink": "Přidat odkaz",
|
||||
"linkLabel": "Označení",
|
||||
"categoryLinks": "Odkazy",
|
||||
"categoryInfo": "Informace",
|
||||
"categorySystem": "Systém",
|
||||
"copyLink": "Kopírovat odkaz",
|
||||
"linkCopied": "Odkaz zkopírován!",
|
||||
"location": "Umístění",
|
||||
"temperatureUnit": "Jednotka teploty",
|
||||
"showForecast": "Zobrazit třídenní předpověď",
|
||||
"scrolling": "Povolit rolování",
|
||||
"feedUrl": "URL kanálu",
|
||||
"maxItems": "Maximální počet položek",
|
||||
"showDescription": "Zobrazit popis",
|
||||
"widgetRssName": "RSS kanál",
|
||||
"noHostSelected": "Nebyl vybrán žádný hostitel",
|
||||
"displayedMetrics": "Zobrazené metriky",
|
||||
"metricCpu": "Procesor",
|
||||
"metricMemory": "Paměť",
|
||||
"metricDisk": "Disk",
|
||||
"metricUptime": "Doba provozuschopnosti",
|
||||
"metricSystem": "Systém",
|
||||
"metricNetwork": "Síť",
|
||||
"metricProcesses": "Procesy",
|
||||
"categoryMonitoring": "Monitorování",
|
||||
"noPingUrls": "Žádné adresy URL nejsou nakonfigurovány",
|
||||
"pingLabel": "Označení",
|
||||
"addPingUrl": "Přidat URL",
|
||||
"showLatency": "Zobrazit latenci",
|
||||
"refreshInterval": "Interval aktualizace",
|
||||
"seconds": "sekundy",
|
||||
"filterActivityTypes": "Typy filtrů",
|
||||
"filterTypesHint": "Pro zobrazení všech ponechte prázdné",
|
||||
"showTimestamp": "Zobrazit časové razítko",
|
||||
"uptimeUnavailable": "Dostupnost není k dispozici",
|
||||
"uptimeLabel": "Doba provozuschopnosti",
|
||||
"overviewVersion": "Verze",
|
||||
"overviewUpdateLabel": "Aktualizovat",
|
||||
"overviewUpdateAvailable": "Aktualizace k dispozici",
|
||||
"overviewDatabase": "Databáze",
|
||||
"overviewUptime": "Doba provozuschopnosti",
|
||||
"noHosts": "Žádní hostitelé nakonfigurováni",
|
||||
"hostGridHosts": "Hostitelé",
|
||||
"hostGridAllHint": "Nechte prázdné pro zobrazení všech hostitelů",
|
||||
"columns": "Sloupce",
|
||||
"showIp": "Zobrazit IP adresu",
|
||||
"layout": "Rozvržení",
|
||||
"showStatus": "Zobrazit stav",
|
||||
"noActivity": "Žádná aktivita",
|
||||
"widgetPingStatusName": "Stav pingu",
|
||||
"widgetRecentActivityName": "Nedávná aktivita",
|
||||
"sshTerminalNoHost": "Žádný hostitel není nakonfigurován",
|
||||
"sshTerminalConnect": "Připojit",
|
||||
"sshTerminalAutoConnect": "Automatické připojení při načtení",
|
||||
"connectionTypes": "Typy připojení",
|
||||
"calMon": "Po",
|
||||
"calTue": "Út",
|
||||
"calWed": "My",
|
||||
"calThu": "Čt",
|
||||
"calFri": "Pá",
|
||||
"calSat": "So",
|
||||
"calSun": "Ne",
|
||||
"startOnMonday": "Začátek týdne v pondělí",
|
||||
"countdownNoDate": "Není stanoveno žádné cílové datum",
|
||||
"countdownPast": "Událost proběhla",
|
||||
"countdownDays": "dny",
|
||||
"countdownHours": "hodin",
|
||||
"countdownMinutes": "min.",
|
||||
"countdownSeconds": "sekunda",
|
||||
"countdownLabel": "Označení",
|
||||
"countdownLabelPlaceholder": "např. den spuštění",
|
||||
"countdownShowDays": "Dny výstav",
|
||||
"countdownShowHours": "Otevírací doba",
|
||||
"targetDate": "Cílové datum",
|
||||
"searchEngine": "Vyhledávač",
|
||||
"customSearchUrl": "URL pro vlastní vyhledávání",
|
||||
"searchPlaceholder": "Vyhledávání...",
|
||||
"searchGo": "Jít",
|
||||
"searchPlaceholderLabel": "Zástupný text",
|
||||
"searchPlaceholderHint": "Text zobrazený uvnitř vyhledávacího pole",
|
||||
"openInNewTab": "Otevřít v nové kartě",
|
||||
"searchQueryPlaceholder": "Zadejte dotaz...",
|
||||
"noSearchShortcuts": "Žádné zkratky nakonfigurované",
|
||||
"addSearchShortcut": "Přidat zkratku",
|
||||
"fontSize": "Velikost písma",
|
||||
"textAlign": "Zarovnání textu",
|
||||
"fontWeight": "Tloušťka písma",
|
||||
"clearColor": "Jasná barva",
|
||||
"imageFit": "Přizpůsobení obrazu",
|
||||
"imageLinkUrl": "URL odkazu",
|
||||
"noImage": "Není nastavena adresa URL obrázku",
|
||||
"altText": "Alternativní text",
|
||||
"altTextPlaceholder": "Popište obrázek",
|
||||
"renderMarkdown": "Renderování Markdownu",
|
||||
"displayMode": "Režim zobrazení",
|
||||
"displayField": "Pole zobrazení",
|
||||
"jsonPath": "Cesta JSON",
|
||||
"customApiLabel": "Označení",
|
||||
"customApiLabelPlaceholder": "např. teplota",
|
||||
"customApiUnit": "Jednotka",
|
||||
"customApiNoUrl": "Není nakonfigurována žádná adresa URL API",
|
||||
"customApiError": "Načtení se nezdařilo",
|
||||
"customApiNotArray": "Odpověď není pole",
|
||||
"addService": "Přidat službu",
|
||||
"showLabels": "Zobrazit štítky",
|
||||
"iconSize": "Velikost ikony",
|
||||
"noDashboardLinks": "Nejsou nakonfigurovány žádné odkazy na řídicí panel",
|
||||
"noLimit": "Bez omezení",
|
||||
"sectionHeading": "Nadpis sekce",
|
||||
"addSection": "Přidat sekci",
|
||||
"compactMode": "Kompaktní režim",
|
||||
"selectHosts": "Vyberte hostitele",
|
||||
"listLayout": "Seznam",
|
||||
"gridLayout": "Mřížka"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Hjemmeside"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Servicelinks",
|
||||
"homepagePreview": "Forhåndsvisning af hjemmeside"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Servicelinks",
|
||||
"serviceLinksEmpty": "Ingen servicelinks endnu",
|
||||
"serviceLinksAdd": "Tilføje",
|
||||
"serviceLinksLabelPlaceholder": "Min tjeneste",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Indtast en gyldig webadresse",
|
||||
"serviceLinksAddFailed": "Kunne ikke tilføje tjenestelink"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Hjemmeside",
|
||||
"addWidget": "Tilføj widget",
|
||||
"editWidget": "Rediger widget",
|
||||
"zoomIn": "Zoom ind",
|
||||
"zoomOut": "Zoom ud",
|
||||
"resetView": "Nulstil visning",
|
||||
"lockLayout": "Låselayout",
|
||||
"unlockLayout": "Lås layout op",
|
||||
"noWidgets": "Højreklik eller klik på + for at tilføje din første widget",
|
||||
"openFullView": "Åbn fuld visning",
|
||||
"previewTitle": "Forhåndsvisning af hjemmeside",
|
||||
"cancel": "Ophæve",
|
||||
"save": "Spare",
|
||||
"title_label": "Titel",
|
||||
"widgetTitlePlaceholder": "Widgettitel (valgfrit)",
|
||||
"url": "URL",
|
||||
"imageUrl": "Brugerdefineret billed-URL",
|
||||
"imageUrlHint": "Lad feltet stå tomt for at bruge webstedets favicon automatisk",
|
||||
"showImage": "Vis billede",
|
||||
"description": "Beskrivelse",
|
||||
"color": "Farve",
|
||||
"timezone": "Tidszone",
|
||||
"invalidTimezone": "Ugyldig tidszone. Brug et IANA-navn, f.eks. America/New_York.",
|
||||
"showSeconds": "Vis sekunder",
|
||||
"format12h": "12-timers",
|
||||
"content": "Tilfreds",
|
||||
"backgroundColor": "Baggrundsfarve",
|
||||
"links": "Links",
|
||||
"addLink": "Tilføj link",
|
||||
"linkLabel": "Mærke",
|
||||
"categoryLinks": "Links",
|
||||
"categoryInfo": "Info",
|
||||
"categorySystem": "System",
|
||||
"copyLink": "Kopiér link",
|
||||
"linkCopied": "Link kopieret!",
|
||||
"location": "Beliggenhed",
|
||||
"temperatureUnit": "Temperaturenhed",
|
||||
"showForecast": "Vis 3-dages vejrudsigt",
|
||||
"scrolling": "Tillad rulning",
|
||||
"feedUrl": "Feed-URL",
|
||||
"maxItems": "Maks. antal varer",
|
||||
"showDescription": "Vis beskrivelse",
|
||||
"widgetRssName": "RSS-feed",
|
||||
"noHostSelected": "Ingen vært valgt",
|
||||
"displayedMetrics": "Viste målinger",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "Hukommelse",
|
||||
"metricDisk": "Disk",
|
||||
"metricUptime": "Oppetid",
|
||||
"metricSystem": "System",
|
||||
"metricNetwork": "Netværk",
|
||||
"metricProcesses": "Processer",
|
||||
"categoryMonitoring": "Overvågning",
|
||||
"noPingUrls": "Ingen URL'er konfigureret",
|
||||
"pingLabel": "Mærke",
|
||||
"addPingUrl": "Tilføj URL",
|
||||
"showLatency": "Vis latenstid",
|
||||
"refreshInterval": "Opdateringsinterval",
|
||||
"seconds": "sekunder",
|
||||
"filterActivityTypes": "Filtertyper",
|
||||
"filterTypesHint": "Lad stå tomt for at vise alle",
|
||||
"showTimestamp": "Vis tidsstempel",
|
||||
"uptimeUnavailable": "Oppetid utilgængelig",
|
||||
"uptimeLabel": "Oppetid",
|
||||
"overviewVersion": "Version",
|
||||
"overviewUpdateLabel": "Opdatering",
|
||||
"overviewUpdateAvailable": "Opdatering tilgængelig",
|
||||
"overviewDatabase": "Database",
|
||||
"overviewUptime": "Oppetid",
|
||||
"noHosts": "Ingen værter konfigureret",
|
||||
"hostGridHosts": "Værter",
|
||||
"hostGridAllHint": "Lad stå tomt for at vise alle værter",
|
||||
"columns": "Kolonner",
|
||||
"showIp": "Vis IP-adresse",
|
||||
"layout": "Layout",
|
||||
"showStatus": "Vis status",
|
||||
"noActivity": "Ingen aktivitet",
|
||||
"widgetPingStatusName": "Ping-status",
|
||||
"widgetRecentActivityName": "Seneste aktivitet",
|
||||
"sshTerminalNoHost": "Ingen vært konfigureret",
|
||||
"sshTerminalConnect": "Forbinde",
|
||||
"sshTerminalAutoConnect": "Automatisk forbindelse ved indlæsning",
|
||||
"connectionTypes": "Forbindelsestyper",
|
||||
"calMon": "Må",
|
||||
"calTue": "Ti",
|
||||
"calWed": "Vi",
|
||||
"calThu": "To",
|
||||
"calFri": "Fr",
|
||||
"calSat": "Lør",
|
||||
"calSun": "Sø",
|
||||
"startOnMonday": "Start ugen på mandag",
|
||||
"countdownNoDate": "Ingen måldato fastsat",
|
||||
"countdownPast": "Begivenheden er overstået",
|
||||
"countdownDays": "dage",
|
||||
"countdownHours": "timer",
|
||||
"countdownMinutes": "min.",
|
||||
"countdownSeconds": "sek.",
|
||||
"countdownLabel": "Mærke",
|
||||
"countdownLabelPlaceholder": "f.eks. lanceringsdag",
|
||||
"countdownShowDays": "Vis dage",
|
||||
"countdownShowHours": "Åbningstider",
|
||||
"targetDate": "Måldato",
|
||||
"searchEngine": "Søgemaskine",
|
||||
"customSearchUrl": "Tilpasset søge-URL",
|
||||
"searchPlaceholder": "Søge...",
|
||||
"searchGo": "Gå",
|
||||
"searchPlaceholderLabel": "Pladsholdertekst",
|
||||
"searchPlaceholderHint": "Tekst vist i søgefeltet",
|
||||
"openInNewTab": "Åbn i ny fane",
|
||||
"searchQueryPlaceholder": "Indtast forespørgsel...",
|
||||
"noSearchShortcuts": "Ingen genveje konfigureret",
|
||||
"addSearchShortcut": "Tilføj genvej",
|
||||
"fontSize": "Skriftstørrelse",
|
||||
"textAlign": "Tekstjustering",
|
||||
"fontWeight": "Skriftvægt",
|
||||
"clearColor": "Klar farve",
|
||||
"imageFit": "Billedtilpasning",
|
||||
"imageLinkUrl": "Link-URL",
|
||||
"noImage": "Ingen billed-URL angivet",
|
||||
"altText": "Alternativ tekst",
|
||||
"altTextPlaceholder": "Beskriv billedet",
|
||||
"renderMarkdown": "Render Markdown",
|
||||
"displayMode": "Visningstilstand",
|
||||
"displayField": "Visningsfelt",
|
||||
"jsonPath": "JSON-sti",
|
||||
"customApiLabel": "Mærke",
|
||||
"customApiLabelPlaceholder": "f.eks. Temperatur",
|
||||
"customApiUnit": "Enhed",
|
||||
"customApiNoUrl": "Ingen API-URL konfigureret",
|
||||
"customApiError": "Kunne ikke hente",
|
||||
"customApiNotArray": "Svaret er ikke et array",
|
||||
"addService": "Tilføj tjeneste",
|
||||
"showLabels": "Vis etiketter",
|
||||
"iconSize": "Ikonstørrelse",
|
||||
"noDashboardLinks": "Ingen dashboard-links konfigureret",
|
||||
"noLimit": "Ingen grænse",
|
||||
"sectionHeading": "Sektionsoverskrift",
|
||||
"addSection": "Tilføj sektion",
|
||||
"compactMode": "Kompakt tilstand",
|
||||
"selectHosts": "Vælg værter",
|
||||
"listLayout": "Liste",
|
||||
"gridLayout": "Gitter"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Startseite"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Service-Links",
|
||||
"homepagePreview": "Homepage-Vorschau"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Service-Links",
|
||||
"serviceLinksEmpty": "Noch keine Service-Links",
|
||||
"serviceLinksAdd": "Hinzufügen",
|
||||
"serviceLinksLabelPlaceholder": "Mein Dienst",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Geben Sie eine gültige Webadresse ein",
|
||||
"serviceLinksAddFailed": "Hinzufügen des Service-Links fehlgeschlagen"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Startseite",
|
||||
"addWidget": "Widget hinzufügen",
|
||||
"editWidget": "Widget bearbeiten",
|
||||
"zoomIn": "Vergrößern",
|
||||
"zoomOut": "Verkleinern",
|
||||
"resetView": "Ansicht zurücksetzen",
|
||||
"lockLayout": "Layout sperren",
|
||||
"unlockLayout": "Layout entsperren",
|
||||
"noWidgets": "Klicken Sie mit der rechten Maustaste oder auf +, um Ihr erstes Widget hinzuzufügen",
|
||||
"openFullView": "Vollansicht öffnen",
|
||||
"previewTitle": "Homepage-Vorschau",
|
||||
"cancel": "Abbrechen",
|
||||
"save": "Speichern",
|
||||
"title_label": "Titel",
|
||||
"widgetTitlePlaceholder": "Widget-Titel (optional)",
|
||||
"url": "URL",
|
||||
"imageUrl": "Benutzerdefinierte Bild-URL",
|
||||
"imageUrlHint": "Leer lassen, um automatisch das Favicon der Website zu verwenden",
|
||||
"showImage": "Bild anzeigen",
|
||||
"description": "Beschreibung",
|
||||
"color": "Farbe",
|
||||
"timezone": "Zeitzone",
|
||||
"invalidTimezone": "Ungültige Zeitzone. Verwenden Sie einen IANA-Namen wie z. B. America/New_York.",
|
||||
"showSeconds": "Sekunden anzeigen",
|
||||
"format12h": "12-Stunden",
|
||||
"content": "Inhalt",
|
||||
"backgroundColor": "Hintergrundfarbe",
|
||||
"links": "Links",
|
||||
"addLink": "Link hinzufügen",
|
||||
"linkLabel": "Bezeichnung",
|
||||
"categoryLinks": "Links",
|
||||
"categoryInfo": "Info",
|
||||
"categorySystem": "System",
|
||||
"copyLink": "Link kopieren",
|
||||
"linkCopied": "Link kopiert!",
|
||||
"location": "Standort",
|
||||
"temperatureUnit": "Temperatureinheit",
|
||||
"showForecast": "3-Tage-Vorhersage anzeigen",
|
||||
"scrolling": "Scrollen erlauben",
|
||||
"feedUrl": "Feed-URL",
|
||||
"maxItems": "Max. Einträge",
|
||||
"showDescription": "Beschreibung anzeigen",
|
||||
"widgetRssName": "RSS-Feed",
|
||||
"noHostSelected": "Kein Host ausgewählt",
|
||||
"displayedMetrics": "Angezeigte Metriken",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "Arbeitsspeicher",
|
||||
"metricDisk": "Festplatte",
|
||||
"metricUptime": "Betriebszeit",
|
||||
"metricSystem": "System",
|
||||
"metricNetwork": "Netzwerk",
|
||||
"metricProcesses": "Prozesse",
|
||||
"categoryMonitoring": "Überwachung",
|
||||
"noPingUrls": "Keine URLs konfiguriert",
|
||||
"pingLabel": "Bezeichnung",
|
||||
"addPingUrl": "URL hinzufügen",
|
||||
"showLatency": "Latenz anzeigen",
|
||||
"refreshInterval": "Aktualisierungsintervall",
|
||||
"seconds": "Sekunden",
|
||||
"filterActivityTypes": "Filtertypen",
|
||||
"filterTypesHint": "Leer lassen, um alle anzuzeigen",
|
||||
"showTimestamp": "Zeitstempel anzeigen",
|
||||
"uptimeUnavailable": "Betriebszeit nicht verfügbar",
|
||||
"uptimeLabel": "Betriebszeit",
|
||||
"overviewVersion": "Version",
|
||||
"overviewUpdateLabel": "Aktualisieren",
|
||||
"overviewUpdateAvailable": "Update verfügbar",
|
||||
"overviewDatabase": "Datenbank",
|
||||
"overviewUptime": "Betriebszeit",
|
||||
"noHosts": "Keine Hosts konfiguriert",
|
||||
"hostGridHosts": "Hosts",
|
||||
"hostGridAllHint": "Leer lassen, um alle Hosts anzuzeigen",
|
||||
"columns": "Spalten",
|
||||
"showIp": "IP-Adresse anzeigen",
|
||||
"layout": "Layout",
|
||||
"showStatus": "Status anzeigen",
|
||||
"noActivity": "Keine Aktivität",
|
||||
"widgetPingStatusName": "Ping-Status",
|
||||
"widgetRecentActivityName": "Letzte Aktivität",
|
||||
"sshTerminalNoHost": "Kein Host konfiguriert",
|
||||
"sshTerminalConnect": "Verbinden",
|
||||
"sshTerminalAutoConnect": "Beim Laden automatisch verbinden",
|
||||
"connectionTypes": "Verbindungstypen",
|
||||
"calMon": "Mo",
|
||||
"calTue": "Di",
|
||||
"calWed": "Mi",
|
||||
"calThu": "Do",
|
||||
"calFri": "Fr",
|
||||
"calSat": "Sa",
|
||||
"calSun": "So",
|
||||
"startOnMonday": "Woche am Montag beginnen",
|
||||
"countdownNoDate": "Kein Zieldatum festgelegt",
|
||||
"countdownPast": "Ereignis ist vorbei",
|
||||
"countdownDays": "Tage",
|
||||
"countdownHours": "Std.",
|
||||
"countdownMinutes": "Min.",
|
||||
"countdownSeconds": "Sek.",
|
||||
"countdownLabel": "Label",
|
||||
"countdownLabelPlaceholder": "z.B. Starttag",
|
||||
"countdownShowDays": "Tage anzeigen",
|
||||
"countdownShowHours": "Stunden anzeigen",
|
||||
"targetDate": "Zieldatum",
|
||||
"searchEngine": "Suchmaschine",
|
||||
"customSearchUrl": "Benutzerdefinierte Such-URL",
|
||||
"searchPlaceholder": "Suchen...",
|
||||
"searchGo": "Los",
|
||||
"searchPlaceholderLabel": "Platzhaltertext",
|
||||
"searchPlaceholderHint": "Text, der im Suchfeld angezeigt wird",
|
||||
"openInNewTab": "In neuem Tab öffnen",
|
||||
"searchQueryPlaceholder": "Suchbegriff eingeben...",
|
||||
"noSearchShortcuts": "Keine Shortcuts konfiguriert",
|
||||
"addSearchShortcut": "Shortcut hinzufügen",
|
||||
"fontSize": "Schriftgröße",
|
||||
"textAlign": "Textausrichtung",
|
||||
"fontWeight": "Schriftstärke",
|
||||
"clearColor": "Farbe löschen",
|
||||
"imageFit": "Bildanpassung",
|
||||
"imageLinkUrl": "Link-URL",
|
||||
"noImage": "Keine Bild-URL festgelegt",
|
||||
"altText": "Alternativtext",
|
||||
"altTextPlaceholder": "Bild beschreiben",
|
||||
"renderMarkdown": "Markdown rendern",
|
||||
"displayMode": "Anzeigemodus",
|
||||
"displayField": "Anzeigefeld",
|
||||
"jsonPath": "JSON-Pfad",
|
||||
"customApiLabel": "Bezeichnung",
|
||||
"customApiLabelPlaceholder": "z.B. Temperatur",
|
||||
"customApiUnit": "Einheit",
|
||||
"customApiNoUrl": "Keine API-URL konfiguriert",
|
||||
"customApiError": "Fehler beim Abrufen",
|
||||
"customApiNotArray": "Antwort ist kein Array",
|
||||
"addService": "Dienst hinzufügen",
|
||||
"showLabels": "Beschriftungen anzeigen",
|
||||
"iconSize": "Symbolgröße",
|
||||
"noDashboardLinks": "Keine Dashboard-Links konfiguriert",
|
||||
"noLimit": "Keine Begrenzung",
|
||||
"sectionHeading": "Abschnittsüberschrift",
|
||||
"addSection": "Abschnitt hinzufügen",
|
||||
"compactMode": "Kompaktmodus",
|
||||
"selectHosts": "Hosts auswählen",
|
||||
"listLayout": "Liste",
|
||||
"gridLayout": "Raster"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Αρχική σελίδα"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Σύνδεσμοι υπηρεσιών",
|
||||
"homepagePreview": "Προεπισκόπηση αρχικής σελίδας"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Σύνδεσμοι υπηρεσιών",
|
||||
"serviceLinksEmpty": "Δεν υπάρχουν ακόμη σύνδεσμοι υπηρεσίας",
|
||||
"serviceLinksAdd": "Προσθέτω",
|
||||
"serviceLinksLabelPlaceholder": "Η Υπηρεσία μου",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Εισαγάγετε μια έγκυρη διεύθυνση ιστού",
|
||||
"serviceLinksAddFailed": "Αποτυχία προσθήκης συνδέσμου υπηρεσίας"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Αρχική σελίδα",
|
||||
"addWidget": "Προσθήκη γραφικού στοιχείου",
|
||||
"editWidget": "Επεξεργασία γραφικού στοιχείου",
|
||||
"zoomIn": "Μεγέθυνση",
|
||||
"zoomOut": "Σμίκρυνση",
|
||||
"resetView": "Επαναφορά προβολής",
|
||||
"lockLayout": "Διάταξη κλειδαριάς",
|
||||
"unlockLayout": "Ξεκλείδωμα διάταξης",
|
||||
"noWidgets": "Κάντε δεξί κλικ ή κάντε κλικ στο + για να προσθέσετε το πρώτο σας γραφικό στοιχείο",
|
||||
"openFullView": "Άνοιγμα πλήρους προβολής",
|
||||
"previewTitle": "Προεπισκόπηση αρχικής σελίδας",
|
||||
"cancel": "Ματαίωση",
|
||||
"save": "Εκτός",
|
||||
"title_label": "Τίτλος",
|
||||
"widgetTitlePlaceholder": "Τίτλος γραφικού στοιχείου (προαιρετικό)",
|
||||
"url": "URL",
|
||||
"imageUrl": "URL προσαρμοσμένης εικόνας",
|
||||
"imageUrlHint": "Αφήστε το κενό για να χρησιμοποιήσετε αυτόματα το favicon του ιστότοπου",
|
||||
"showImage": "Εμφάνιση εικόνας",
|
||||
"description": "Περιγραφή",
|
||||
"color": "Χρώμα",
|
||||
"timezone": "Ζώνη ώρας",
|
||||
"invalidTimezone": "Μη έγκυρη ζώνη ώρας. Χρησιμοποιήστε ένα όνομα IANA, όπως America/New_York.",
|
||||
"showSeconds": "Εμφάνιση δευτερολέπτων",
|
||||
"format12h": "12ωρο",
|
||||
"content": "Περιεχόμενο",
|
||||
"backgroundColor": "Χρώμα φόντου",
|
||||
"links": "Εδαφος διά παιγνίδι γκολφ",
|
||||
"addLink": "Προσθήκη συνδέσμου",
|
||||
"linkLabel": "Επιγραφή",
|
||||
"categoryLinks": "Εδαφος διά παιγνίδι γκολφ",
|
||||
"categoryInfo": "Πληροφορίες",
|
||||
"categorySystem": "Σύστημα",
|
||||
"copyLink": "Αντιγραφή συνδέσμου",
|
||||
"linkCopied": "Ο σύνδεσμος αντιγράφηκε!",
|
||||
"location": "Τοποθεσία",
|
||||
"temperatureUnit": "Μονάδα θερμοκρασίας",
|
||||
"showForecast": "Εμφάνιση τριήμερης πρόγνωσης",
|
||||
"scrolling": "Επιτρέπεται η κύλιση",
|
||||
"feedUrl": "URL ροής",
|
||||
"maxItems": "Μέγιστος αριθμός στοιχείων",
|
||||
"showDescription": "Εμφάνιση περιγραφής",
|
||||
"widgetRssName": "Ροή RSS",
|
||||
"noHostSelected": "Δεν έχει επιλεγεί κεντρικός υπολογιστής",
|
||||
"displayedMetrics": "Εμφανιζόμενες μετρήσεις",
|
||||
"metricCpu": "ΚΜΕ",
|
||||
"metricMemory": "Μνήμη",
|
||||
"metricDisk": "Δίσκος",
|
||||
"metricUptime": "Χρόνος λειτουργίας",
|
||||
"metricSystem": "Σύστημα",
|
||||
"metricNetwork": "Δίκτυο",
|
||||
"metricProcesses": "Διαδικασίες",
|
||||
"categoryMonitoring": "Παρακολούθηση",
|
||||
"noPingUrls": "Δεν έχουν ρυθμιστεί διευθύνσεις URL",
|
||||
"pingLabel": "Επιγραφή",
|
||||
"addPingUrl": "Προσθήκη διεύθυνσης URL",
|
||||
"showLatency": "Εμφάνιση καθυστέρησης",
|
||||
"refreshInterval": "Διάστημα ανανέωσης",
|
||||
"seconds": "δευτερόλεπτα",
|
||||
"filterActivityTypes": "Τύποι φίλτρων",
|
||||
"filterTypesHint": "Αφήστε κενό για να εμφανιστούν όλα",
|
||||
"showTimestamp": "Εμφάνιση χρονικής σήμανσης",
|
||||
"uptimeUnavailable": "Ο χρόνος λειτουργίας δεν είναι διαθέσιμος",
|
||||
"uptimeLabel": "Χρόνος λειτουργίας",
|
||||
"overviewVersion": "Εκδοχή",
|
||||
"overviewUpdateLabel": "Εκσυγχρονίζω",
|
||||
"overviewUpdateAvailable": "Διαθέσιμη ενημέρωση",
|
||||
"overviewDatabase": "Βάση δεδομένων",
|
||||
"overviewUptime": "Χρόνος λειτουργίας",
|
||||
"noHosts": "Δεν έχουν ρυθμιστεί κεντρικοί υπολογιστές",
|
||||
"hostGridHosts": "Οικοδεσπότες",
|
||||
"hostGridAllHint": "Αφήστε κενό για να εμφανιστούν όλοι οι κεντρικοί υπολογιστές",
|
||||
"columns": "Στήλες",
|
||||
"showIp": "Εμφάνιση διεύθυνσης IP",
|
||||
"layout": "Σχέδιο",
|
||||
"showStatus": "Εμφάνιση κατάστασης",
|
||||
"noActivity": "Καμία δραστηριότητα",
|
||||
"widgetPingStatusName": "Κατάσταση Ping",
|
||||
"widgetRecentActivityName": "Πρόσφατη δραστηριότητα",
|
||||
"sshTerminalNoHost": "Δεν έχει ρυθμιστεί κεντρικός υπολογιστής",
|
||||
"sshTerminalConnect": "Συνδέω",
|
||||
"sshTerminalAutoConnect": "Αυτόματη σύνδεση κατά τη φόρτωση",
|
||||
"connectionTypes": "Τύποι σύνδεσης",
|
||||
"calMon": "Μο",
|
||||
"calTue": "Τρ",
|
||||
"calWed": "Εμείς",
|
||||
"calThu": "Πέμ",
|
||||
"calFri": "Πατέρας",
|
||||
"calSat": "Σάβ",
|
||||
"calSun": "Κυρ",
|
||||
"startOnMonday": "Έναρξη εβδομάδας τη Δευτέρα",
|
||||
"countdownNoDate": "Δεν έχει οριστεί ημερομηνία-στόχος",
|
||||
"countdownPast": "Η εκδήλωση πέρασε",
|
||||
"countdownDays": "ημέρες",
|
||||
"countdownHours": "ώρες",
|
||||
"countdownMinutes": "λεπτά",
|
||||
"countdownSeconds": "δευτ.",
|
||||
"countdownLabel": "Επιγραφή",
|
||||
"countdownLabelPlaceholder": "π.χ. Ημέρα Εκκίνησης",
|
||||
"countdownShowDays": "Ημέρες προβολής",
|
||||
"countdownShowHours": "Ώρες προβολής",
|
||||
"targetDate": "Ημερομηνία-στόχος",
|
||||
"searchEngine": "Μηχανή αναζήτησης",
|
||||
"customSearchUrl": "URL προσαρμοσμένης αναζήτησης",
|
||||
"searchPlaceholder": "Ερευνα...",
|
||||
"searchGo": "Πάω",
|
||||
"searchPlaceholderLabel": "Κείμενο κράτησης θέσης",
|
||||
"searchPlaceholderHint": "Κείμενο που εμφανίζεται μέσα στην εισαγωγή αναζήτησης",
|
||||
"openInNewTab": "Άνοιγμα σε νέα καρτέλα",
|
||||
"searchQueryPlaceholder": "Εισαγωγή ερωτήματος...",
|
||||
"noSearchShortcuts": "Δεν έχουν ρυθμιστεί συντομεύσεις",
|
||||
"addSearchShortcut": "Προσθήκη συντόμευσης",
|
||||
"fontSize": "Μέγεθος γραμματοσειράς",
|
||||
"textAlign": "Στοίχιση κειμένου",
|
||||
"fontWeight": "Πάχος γραμματοσειράς",
|
||||
"clearColor": "Διαυγές χρώμα",
|
||||
"imageFit": "Προσαρμογή εικόνας",
|
||||
"imageLinkUrl": "URL συνδέσμου",
|
||||
"noImage": "Δεν έχει οριστεί URL εικόνας",
|
||||
"altText": "Εναλλακτικό κείμενο",
|
||||
"altTextPlaceholder": "Περιγράψτε την εικόνα",
|
||||
"renderMarkdown": "Απόδοση Markdown",
|
||||
"displayMode": "Λειτουργία εμφάνισης",
|
||||
"displayField": "Πεδίο εμφάνισης",
|
||||
"jsonPath": "Διαδρομή JSON",
|
||||
"customApiLabel": "Επιγραφή",
|
||||
"customApiLabelPlaceholder": "π.χ. Θερμοκρασία",
|
||||
"customApiUnit": "Μονάδα",
|
||||
"customApiNoUrl": "Δεν έχει ρυθμιστεί URL API",
|
||||
"customApiError": "Η ανάκτηση απέτυχε",
|
||||
"customApiNotArray": "Η απάντηση δεν είναι πίνακας",
|
||||
"addService": "Προσθήκη υπηρεσίας",
|
||||
"showLabels": "Εμφάνιση ετικετών",
|
||||
"iconSize": "Μέγεθος εικονιδίου",
|
||||
"noDashboardLinks": "Δεν έχουν ρυθμιστεί σύνδεσμοι πίνακα ελέγχου",
|
||||
"noLimit": "Χωρίς όριο",
|
||||
"sectionHeading": "Επικεφαλίδα Ενότητας",
|
||||
"addSection": "Προσθήκη ενότητας",
|
||||
"compactMode": "Συμπαγής λειτουργία",
|
||||
"selectHosts": "Επιλέξτε κεντρικούς υπολογιστές",
|
||||
"listLayout": "Λίστα",
|
||||
"gridLayout": "Πλέγμα"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Inicio"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Enlaces de servicio",
|
||||
"homepagePreview": "Vista previa de la página de inicio"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Enlaces de servicio",
|
||||
"serviceLinksEmpty": "Aún no hay enlaces de servicio",
|
||||
"serviceLinksAdd": "Añadir",
|
||||
"serviceLinksLabelPlaceholder": "Mi servicio",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Introduce una dirección web válida",
|
||||
"serviceLinksAddFailed": "Error al añadir el enlace de servicio"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Inicio",
|
||||
"addWidget": "Añadir widget",
|
||||
"editWidget": "Editar widget",
|
||||
"zoomIn": "Acercar",
|
||||
"zoomOut": "Alejar",
|
||||
"resetView": "Restablecer vista",
|
||||
"lockLayout": "Bloquear diseño",
|
||||
"unlockLayout": "Desbloquear diseño",
|
||||
"noWidgets": "Haz clic derecho o pulsa + para añadir tu primer widget",
|
||||
"openFullView": "Abrir vista completa",
|
||||
"previewTitle": "Vista previa de la página de inicio",
|
||||
"cancel": "Cancelar",
|
||||
"save": "Guardar",
|
||||
"title_label": "Título",
|
||||
"widgetTitlePlaceholder": "Título del widget (opcional)",
|
||||
"url": "URL",
|
||||
"imageUrl": "URL de imagen personalizada",
|
||||
"imageUrlHint": "Déjalo en blanco para usar el favicon del sitio automáticamente",
|
||||
"showImage": "Mostrar imagen",
|
||||
"description": "Descripción",
|
||||
"color": "Color",
|
||||
"timezone": "Zona horaria",
|
||||
"invalidTimezone": "Zona horaria no válida. Utilice un nombre IANA como America/New_York.",
|
||||
"showSeconds": "Mostrar segundos",
|
||||
"format12h": "12 horas",
|
||||
"content": "Contenido",
|
||||
"backgroundColor": "Color de fondo",
|
||||
"links": "Enlaces",
|
||||
"addLink": "Añadir enlace",
|
||||
"linkLabel": "Etiqueta",
|
||||
"categoryLinks": "Enlaces",
|
||||
"categoryInfo": "Información",
|
||||
"categorySystem": "Sistema",
|
||||
"copyLink": "Copiar enlace",
|
||||
"linkCopied": "¡Enlace copiado!",
|
||||
"location": "Ubicación",
|
||||
"temperatureUnit": "Unidad de temperatura",
|
||||
"showForecast": "Mostrar pronóstico de 3 días",
|
||||
"scrolling": "Permitir desplazamiento",
|
||||
"feedUrl": "URL del feed",
|
||||
"maxItems": "Máx. de elementos",
|
||||
"showDescription": "Mostrar descripción",
|
||||
"widgetRssName": "Fuente RSS",
|
||||
"noHostSelected": "Ningún host seleccionado",
|
||||
"displayedMetrics": "Métricas mostradas",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "Memoria",
|
||||
"metricDisk": "Disco",
|
||||
"metricUptime": "Tiempo de actividad",
|
||||
"metricSystem": "Sistema",
|
||||
"metricNetwork": "Red",
|
||||
"metricProcesses": "Procesos",
|
||||
"categoryMonitoring": "Monitorización",
|
||||
"noPingUrls": "No hay URLs configuradas",
|
||||
"pingLabel": "Etiqueta",
|
||||
"addPingUrl": "Añadir URL",
|
||||
"showLatency": "Mostrar latencia",
|
||||
"refreshInterval": "Intervalo de actualización",
|
||||
"seconds": "segundos",
|
||||
"filterActivityTypes": "Filtrar tipos",
|
||||
"filterTypesHint": "Dejar vacío para mostrar todo",
|
||||
"showTimestamp": "Mostrar marca de tiempo",
|
||||
"uptimeUnavailable": "Tiempo de actividad no disponible",
|
||||
"uptimeLabel": "Tiempo de actividad",
|
||||
"overviewVersion": "Versión",
|
||||
"overviewUpdateLabel": "Actualizar",
|
||||
"overviewUpdateAvailable": "Actualización disponible",
|
||||
"overviewDatabase": "Base de datos",
|
||||
"overviewUptime": "Tiempo de actividad",
|
||||
"noHosts": "Sin hosts configurados",
|
||||
"hostGridHosts": "Hosts",
|
||||
"hostGridAllHint": "Dejar vacío para mostrar todos los hosts",
|
||||
"columns": "Columnas",
|
||||
"showIp": "Mostrar dirección IP",
|
||||
"layout": "Diseño",
|
||||
"showStatus": "Mostrar estado",
|
||||
"noActivity": "Sin actividad",
|
||||
"widgetPingStatusName": "Estado de ping",
|
||||
"widgetRecentActivityName": "Actividad reciente",
|
||||
"sshTerminalNoHost": "Sin host configurado",
|
||||
"sshTerminalConnect": "Conectar",
|
||||
"sshTerminalAutoConnect": "Conectar automáticamente al cargar",
|
||||
"connectionTypes": "Tipos de conexión",
|
||||
"calMon": "Lu",
|
||||
"calTue": "Ma",
|
||||
"calWed": "Mi",
|
||||
"calThu": "Ju",
|
||||
"calFri": "Vi",
|
||||
"calSat": "Sa",
|
||||
"calSun": "Do",
|
||||
"startOnMonday": "Comenzar la semana el lunes",
|
||||
"countdownNoDate": "No se ha fijado una fecha objetivo",
|
||||
"countdownPast": "El evento ya ha pasado",
|
||||
"countdownDays": "días",
|
||||
"countdownHours": "h",
|
||||
"countdownMinutes": "min",
|
||||
"countdownSeconds": "s",
|
||||
"countdownLabel": "Etiqueta",
|
||||
"countdownLabelPlaceholder": "p. ej. Día de lanzamiento",
|
||||
"countdownShowDays": "Mostrar días",
|
||||
"countdownShowHours": "Mostrar horas",
|
||||
"targetDate": "Fecha objetivo",
|
||||
"searchEngine": "Motor de búsqueda",
|
||||
"customSearchUrl": "URL de búsqueda personalizada",
|
||||
"searchPlaceholder": "Buscar...",
|
||||
"searchGo": "Ir",
|
||||
"searchPlaceholderLabel": "Texto del marcador de posición",
|
||||
"searchPlaceholderHint": "Texto mostrado dentro del campo de búsqueda",
|
||||
"openInNewTab": "Abrir en nueva pestaña",
|
||||
"searchQueryPlaceholder": "Introducir consulta...",
|
||||
"noSearchShortcuts": "No hay accesos directos configurados",
|
||||
"addSearchShortcut": "Añadir acceso directo",
|
||||
"fontSize": "Tamaño de fuente",
|
||||
"textAlign": "Alineación del texto",
|
||||
"fontWeight": "Grosor de la fuente",
|
||||
"clearColor": "Limpiar color",
|
||||
"imageFit": "Ajuste de imagen",
|
||||
"imageLinkUrl": "URL del enlace",
|
||||
"noImage": "No se ha establecido una URL de imagen",
|
||||
"altText": "Texto alternativo",
|
||||
"altTextPlaceholder": "Describir la imagen",
|
||||
"renderMarkdown": "Procesar Markdown",
|
||||
"displayMode": "Modo de visualización",
|
||||
"displayField": "Campo de visualización",
|
||||
"jsonPath": "Ruta JSON",
|
||||
"customApiLabel": "Etiqueta",
|
||||
"customApiLabelPlaceholder": "p. ej. Temperatura",
|
||||
"customApiUnit": "Unidad",
|
||||
"customApiNoUrl": "No se ha configurado ninguna URL de API",
|
||||
"customApiError": "Error al obtener",
|
||||
"customApiNotArray": "La respuesta no es un array",
|
||||
"addService": "Añadir servicio",
|
||||
"showLabels": "Mostrar etiquetas",
|
||||
"iconSize": "Tamaño del icono",
|
||||
"noDashboardLinks": "Sin enlaces de panel configurados",
|
||||
"noLimit": "Sin límite",
|
||||
"sectionHeading": "Encabezado de sección",
|
||||
"addSection": "Añadir sección",
|
||||
"compactMode": "Modo compacto",
|
||||
"selectHosts": "Seleccionar hosts",
|
||||
"listLayout": "Lista",
|
||||
"gridLayout": "Cuadrícula"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Etusivu"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Palvelulinkit",
|
||||
"homepagePreview": "Etusivun esikatselu"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Palvelulinkit",
|
||||
"serviceLinksEmpty": "Ei vielä palvelulinkkejä",
|
||||
"serviceLinksAdd": "Lisätä",
|
||||
"serviceLinksLabelPlaceholder": "Palveluni",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Anna kelvollinen verkko-osoite",
|
||||
"serviceLinksAddFailed": "Palvelulinkin lisääminen epäonnistui"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Etusivu",
|
||||
"addWidget": "Lisää widget",
|
||||
"editWidget": "Muokkaa widgetiä",
|
||||
"zoomIn": "Lähennä",
|
||||
"zoomOut": "Loitonna",
|
||||
"resetView": "Palauta näkymä",
|
||||
"lockLayout": "Lukituksen asettelu",
|
||||
"unlockLayout": "Avaa asettelu",
|
||||
"noWidgets": "Lisää ensimmäinen widget napsauttamalla hiiren kakkospainikkeella tai napsauttamalla +",
|
||||
"openFullView": "Avaa koko näkymä",
|
||||
"previewTitle": "Etusivun esikatselu",
|
||||
"cancel": "Peruuttaa",
|
||||
"save": "Tallentaa",
|
||||
"title_label": "Otsikko",
|
||||
"widgetTitlePlaceholder": "Widgetin otsikko (valinnainen)",
|
||||
"url": "URL-osoite",
|
||||
"imageUrl": "Mukautetun kuvan URL-osoite",
|
||||
"imageUrlHint": "Jätä tyhjäksi, jos haluat käyttää sivuston suosikkikuvaketta automaattisesti",
|
||||
"showImage": "Näytä kuva",
|
||||
"description": "Kuvaus",
|
||||
"color": "Väri",
|
||||
"timezone": "Aikavyöhyke",
|
||||
"invalidTimezone": "Virheellinen aikavyöhyke. Käytä IANA-nimeä, kuten Amerikka/New_York",
|
||||
"showSeconds": "Näytä sekunnit",
|
||||
"format12h": "12 tunnin",
|
||||
"content": "Sisältö",
|
||||
"backgroundColor": "Taustaväri",
|
||||
"links": "Linkit",
|
||||
"addLink": "Lisää linkki",
|
||||
"linkLabel": "Tarra",
|
||||
"categoryLinks": "Linkit",
|
||||
"categoryInfo": "Tiedot",
|
||||
"categorySystem": "Järjestelmä",
|
||||
"copyLink": "Kopioi linkki",
|
||||
"linkCopied": "Linkki kopioitu!",
|
||||
"location": "Sijainti",
|
||||
"temperatureUnit": "Lämpötilayksikkö",
|
||||
"showForecast": "Näytä 3 päivän ennuste",
|
||||
"scrolling": "Salli vieritys",
|
||||
"feedUrl": "Syötteen URL-osoite",
|
||||
"maxItems": "Kohteiden enimmäismäärä",
|
||||
"showDescription": "Näytä kuvaus",
|
||||
"widgetRssName": "RSS-syöte",
|
||||
"noHostSelected": "Ei valittua isäntää",
|
||||
"displayedMetrics": "Näytetyt mittarit",
|
||||
"metricCpu": "Suoritin",
|
||||
"metricMemory": "Muisti",
|
||||
"metricDisk": "Levy",
|
||||
"metricUptime": "Käyttöaika",
|
||||
"metricSystem": "Järjestelmä",
|
||||
"metricNetwork": "Verkko",
|
||||
"metricProcesses": "Prosessit",
|
||||
"categoryMonitoring": "Seuranta",
|
||||
"noPingUrls": "Ei määritettyjä URL-osoitteita",
|
||||
"pingLabel": "Tarra",
|
||||
"addPingUrl": "Lisää URL-osoite",
|
||||
"showLatency": "Näytä latenssi",
|
||||
"refreshInterval": "Päivitysväli",
|
||||
"seconds": "sekuntia",
|
||||
"filterActivityTypes": "Suodatintyypit",
|
||||
"filterTypesHint": "Jätä tyhjäksi näyttääksesi kaikki",
|
||||
"showTimestamp": "Näytä aikaleima",
|
||||
"uptimeUnavailable": "Käyttöaika ei saatavilla",
|
||||
"uptimeLabel": "Käyttöaika",
|
||||
"overviewVersion": "Versio",
|
||||
"overviewUpdateLabel": "Päivittää",
|
||||
"overviewUpdateAvailable": "Päivitys saatavilla",
|
||||
"overviewDatabase": "Tietokanta",
|
||||
"overviewUptime": "Käyttöaika",
|
||||
"noHosts": "Ei määritettyjä isäntiä",
|
||||
"hostGridHosts": "Isännät",
|
||||
"hostGridAllHint": "Jätä tyhjäksi, jos haluat näyttää kaikki isännät",
|
||||
"columns": "Sarakkeet",
|
||||
"showIp": "Näytä IP-osoite",
|
||||
"layout": "Layout",
|
||||
"showStatus": "Näytä tila",
|
||||
"noActivity": "Ei toimintaa",
|
||||
"widgetPingStatusName": "Ping-tila",
|
||||
"widgetRecentActivityName": "Viimeaikaiset toiminnot",
|
||||
"sshTerminalNoHost": "Ei määritettyä isäntää",
|
||||
"sshTerminalConnect": "Yhdistä",
|
||||
"sshTerminalAutoConnect": "Automaattinen yhteys kuormituksen yhteydessä",
|
||||
"connectionTypes": "Yhteystyypit",
|
||||
"calMon": "Mo",
|
||||
"calTue": "Ti",
|
||||
"calWed": "Me",
|
||||
"calThu": "To",
|
||||
"calFri": "Pe",
|
||||
"calSat": "La",
|
||||
"calSun": "Su",
|
||||
"startOnMonday": "Viikko alkaa maanantaina",
|
||||
"countdownNoDate": "Ei asetettua tavoitepäivämäärää",
|
||||
"countdownPast": "Tapahtuma on ohi",
|
||||
"countdownDays": "päivää",
|
||||
"countdownHours": "tuntia",
|
||||
"countdownMinutes": "min",
|
||||
"countdownSeconds": "sek",
|
||||
"countdownLabel": "Tarra",
|
||||
"countdownLabelPlaceholder": "esim. julkaisupäivä",
|
||||
"countdownShowDays": "Näyttelypäivät",
|
||||
"countdownShowHours": "Näytä aukioloajat",
|
||||
"targetDate": "Tavoitepäivämäärä",
|
||||
"searchEngine": "Hakukone",
|
||||
"customSearchUrl": "Mukautetun haun URL-osoite",
|
||||
"searchPlaceholder": "Haku...",
|
||||
"searchGo": "Mennä",
|
||||
"searchPlaceholderLabel": "Paikkamerkkiteksti",
|
||||
"searchPlaceholderHint": "Hakusyötteen sisällä näkyvä teksti",
|
||||
"openInNewTab": "Avaa uudessa välilehdessä",
|
||||
"searchQueryPlaceholder": "Syötä kysely...",
|
||||
"noSearchShortcuts": "Ei määritettyjä pikanäppäimiä",
|
||||
"addSearchShortcut": "Lisää pikakuvake",
|
||||
"fontSize": "Fonttikoko",
|
||||
"textAlign": "Tekstin tasaus",
|
||||
"fontWeight": "Fontin paino",
|
||||
"clearColor": "Kirkas väri",
|
||||
"imageFit": "Kuvan sovitus",
|
||||
"imageLinkUrl": "Linkin URL-osoite",
|
||||
"noImage": "Kuvan URL-osoitetta ei ole asetettu",
|
||||
"altText": "Vaihtoehtoinen teksti",
|
||||
"altTextPlaceholder": "Kuvaile kuvaa",
|
||||
"renderMarkdown": "Renderöi Markdown",
|
||||
"displayMode": "Näyttötila",
|
||||
"displayField": "Näyttökenttä",
|
||||
"jsonPath": "JSON-polku",
|
||||
"customApiLabel": "Tarra",
|
||||
"customApiLabelPlaceholder": "esim. Lämpötila",
|
||||
"customApiUnit": "Yksikkö",
|
||||
"customApiNoUrl": "API-URL-osoitetta ei ole määritetty",
|
||||
"customApiError": "Nouto epäonnistui",
|
||||
"customApiNotArray": "Vastaus ei ole taulukko",
|
||||
"addService": "Lisää palvelu",
|
||||
"showLabels": "Näytä otsikot",
|
||||
"iconSize": "Kuvakkeen koko",
|
||||
"noDashboardLinks": "Ei määritettyjä kojelaudan linkkejä",
|
||||
"noLimit": "Ei rajaa",
|
||||
"sectionHeading": "Osion otsikko",
|
||||
"addSection": "Lisää osio",
|
||||
"compactMode": "Kompakti tila",
|
||||
"selectHosts": "Valitse isännät",
|
||||
"listLayout": "Lista",
|
||||
"gridLayout": "Ruudukko"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Accueil"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Liens de service",
|
||||
"homepagePreview": "Aperçu de la page d'accueil"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Liens de service",
|
||||
"serviceLinksEmpty": "Aucun lien de service pour l’instant",
|
||||
"serviceLinksAdd": "Ajouter",
|
||||
"serviceLinksLabelPlaceholder": "Mon service",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Saisissez une adresse Web valide",
|
||||
"serviceLinksAddFailed": "Échec de l’ajout du lien de service"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Accueil",
|
||||
"addWidget": "Ajouter un widget",
|
||||
"editWidget": "Modifier le widget",
|
||||
"zoomIn": "Zoom avant",
|
||||
"zoomOut": "Zoom arrière",
|
||||
"resetView": "Réinitialiser la vue",
|
||||
"lockLayout": "Verrouiller la disposition",
|
||||
"unlockLayout": "Déverrouiller la disposition",
|
||||
"noWidgets": "Faites un clic droit ou cliquez sur + pour ajouter votre premier widget",
|
||||
"openFullView": "Ouvrir la vue complète",
|
||||
"previewTitle": "Aperçu de la page d'accueil",
|
||||
"cancel": "Annuler",
|
||||
"save": "Enregistrer",
|
||||
"title_label": "Titre",
|
||||
"widgetTitlePlaceholder": "Titre du widget (facultatif)",
|
||||
"url": "URL",
|
||||
"imageUrl": "URL d'image personnalisée",
|
||||
"imageUrlHint": "Laissez vide pour utiliser automatiquement le favicon du site",
|
||||
"showImage": "Afficher l'image",
|
||||
"description": "Description",
|
||||
"color": "Couleur",
|
||||
"timezone": "Fuseau horaire",
|
||||
"invalidTimezone": "Fuseau horaire invalide. Veuillez utiliser un nom IANA tel que America/New_York.",
|
||||
"showSeconds": "Afficher les secondes",
|
||||
"format12h": "Format 12 heures",
|
||||
"content": "Contenu",
|
||||
"backgroundColor": "Couleur d'arrière-plan",
|
||||
"links": "Liens",
|
||||
"addLink": "Ajouter un lien",
|
||||
"linkLabel": "Libellé",
|
||||
"categoryLinks": "Liens",
|
||||
"categoryInfo": "Info",
|
||||
"categorySystem": "Système",
|
||||
"copyLink": "Copier le lien",
|
||||
"linkCopied": "Lien copié !",
|
||||
"location": "Localisation",
|
||||
"temperatureUnit": "Unité de température",
|
||||
"showForecast": "Afficher les prévisions sur 3 jours",
|
||||
"scrolling": "Autoriser le défilement",
|
||||
"feedUrl": "URL du flux",
|
||||
"maxItems": "Nombre max d'éléments",
|
||||
"showDescription": "Afficher la description",
|
||||
"widgetRssName": "Flux RSS",
|
||||
"noHostSelected": "Aucun hôte sélectionné",
|
||||
"displayedMetrics": "Métriques affichées",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "Mémoire",
|
||||
"metricDisk": "Disque",
|
||||
"metricUptime": "Uptime",
|
||||
"metricSystem": "Système",
|
||||
"metricNetwork": "Réseau",
|
||||
"metricProcesses": "Processus",
|
||||
"categoryMonitoring": "Surveillance",
|
||||
"noPingUrls": "Aucune URL configurée",
|
||||
"pingLabel": "Libellé",
|
||||
"addPingUrl": "Ajouter une URL",
|
||||
"showLatency": "Afficher la latence",
|
||||
"refreshInterval": "Intervalle de rafraîchissement",
|
||||
"seconds": "secondes",
|
||||
"filterActivityTypes": "Filtrer les types",
|
||||
"filterTypesHint": "Laisser vide pour tout afficher",
|
||||
"showTimestamp": "Afficher l’horodatage",
|
||||
"uptimeUnavailable": "Uptime non disponible",
|
||||
"uptimeLabel": "Uptime",
|
||||
"overviewVersion": "Version",
|
||||
"overviewUpdateLabel": "Mise à jour",
|
||||
"overviewUpdateAvailable": "Mise à jour disponible",
|
||||
"overviewDatabase": "Base de données",
|
||||
"overviewUptime": "Uptime",
|
||||
"noHosts": "Aucun hôte configuré",
|
||||
"hostGridHosts": "Hôtes",
|
||||
"hostGridAllHint": "Laissez vide pour afficher tous les hôtes",
|
||||
"columns": "Colonnes",
|
||||
"showIp": "Afficher l'adresse IP",
|
||||
"layout": "Disposition",
|
||||
"showStatus": "Afficher le statut",
|
||||
"noActivity": "Aucune activité",
|
||||
"widgetPingStatusName": "Statut du ping",
|
||||
"widgetRecentActivityName": "Activité récente",
|
||||
"sshTerminalNoHost": "Aucun hôte configuré",
|
||||
"sshTerminalConnect": "Connecter",
|
||||
"sshTerminalAutoConnect": "Connexion automatique au chargement",
|
||||
"connectionTypes": "Types de connexion",
|
||||
"calMon": "Lu",
|
||||
"calTue": "Ma",
|
||||
"calWed": "Me",
|
||||
"calThu": "Je",
|
||||
"calFri": "Ve",
|
||||
"calSat": "Sa",
|
||||
"calSun": "Di",
|
||||
"startOnMonday": "Commencer la semaine le lundi",
|
||||
"countdownNoDate": "Aucune date cible définie",
|
||||
"countdownPast": "L'événement est passé",
|
||||
"countdownDays": "jours",
|
||||
"countdownHours": "h",
|
||||
"countdownMinutes": "min",
|
||||
"countdownSeconds": "s",
|
||||
"countdownLabel": "Libellé",
|
||||
"countdownLabelPlaceholder": "ex. : Jour de lancement",
|
||||
"countdownShowDays": "Afficher les jours",
|
||||
"countdownShowHours": "Afficher les heures",
|
||||
"targetDate": "Date cible",
|
||||
"searchEngine": "Moteur de recherche",
|
||||
"customSearchUrl": "URL de recherche personnalisée",
|
||||
"searchPlaceholder": "Rechercher...",
|
||||
"searchGo": "OK",
|
||||
"searchPlaceholderLabel": "Texte indicatif",
|
||||
"searchPlaceholderHint": "Texte affiché dans le champ de recherche",
|
||||
"openInNewTab": "Ouvrir dans un nouvel onglet",
|
||||
"searchQueryPlaceholder": "Saisir une requête...",
|
||||
"noSearchShortcuts": "Aucun raccourci configuré",
|
||||
"addSearchShortcut": "Ajouter un raccourci",
|
||||
"fontSize": "Taille de la police",
|
||||
"textAlign": "Alignement du texte",
|
||||
"fontWeight": "Graisse de la police",
|
||||
"clearColor": "Effacer la couleur",
|
||||
"imageFit": "Ajustement de l'image",
|
||||
"imageLinkUrl": "URL du lien",
|
||||
"noImage": "Aucune URL d'image définie",
|
||||
"altText": "Texte alternatif",
|
||||
"altTextPlaceholder": "Décrire l'image",
|
||||
"renderMarkdown": "Rendu Markdown",
|
||||
"displayMode": "Mode d'affichage",
|
||||
"displayField": "Champ d'affichage",
|
||||
"jsonPath": "Chemin JSON",
|
||||
"customApiLabel": "Étiquette",
|
||||
"customApiLabelPlaceholder": "ex. : Température",
|
||||
"customApiUnit": "Unité",
|
||||
"customApiNoUrl": "Aucune URL d'API configurée",
|
||||
"customApiError": "Échec de la récupération",
|
||||
"customApiNotArray": "La réponse n'est pas un tableau",
|
||||
"addService": "Ajouter un service",
|
||||
"showLabels": "Afficher les étiquettes",
|
||||
"iconSize": "Taille des icônes",
|
||||
"noDashboardLinks": "Aucun lien de tableau de bord configuré",
|
||||
"noLimit": "Aucune limite",
|
||||
"sectionHeading": "Titre de la section",
|
||||
"addSection": "Ajouter une section",
|
||||
"compactMode": "Mode compact",
|
||||
"selectHosts": "Sélectionner les hôtes",
|
||||
"listLayout": "Liste",
|
||||
"gridLayout": "Grille"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "דף הבית"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "קישורי שירות",
|
||||
"homepagePreview": "תצוגה מקדימה של דף הבית"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "קישורי שירות",
|
||||
"serviceLinksEmpty": "אין עדיין קישורים לשירותים",
|
||||
"serviceLinksAdd": "לְהוֹסִיף",
|
||||
"serviceLinksLabelPlaceholder": "השירות שלי",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "הזן כתובת אינטרנט חוקית",
|
||||
"serviceLinksAddFailed": "נכשל הוספת קישור לשירות"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "דף הבית",
|
||||
"addWidget": "הוסף ווידג'ט",
|
||||
"editWidget": "עריכת ווידג'ט",
|
||||
"zoomIn": "לְהִתְמַקֵד",
|
||||
"zoomOut": "התקרבות",
|
||||
"resetView": "איפוס תצוגה",
|
||||
"lockLayout": "פריסת נעילה",
|
||||
"unlockLayout": "נעילת פריסה",
|
||||
"noWidgets": "לחץ לחיצה ימנית או לחץ על + כדי להוסיף את הווידג'ט הראשון שלך",
|
||||
"openFullView": "פתח תצוגה מלאה",
|
||||
"previewTitle": "תצוגה מקדימה של דף הבית",
|
||||
"cancel": "לְבַטֵל",
|
||||
"save": "לְהַצִיל",
|
||||
"title_label": "כּוֹתֶרֶת",
|
||||
"widgetTitlePlaceholder": "כותרת הווידג'ט (אופציונלי)",
|
||||
"url": "כתובת אתר",
|
||||
"imageUrl": "כתובת אתר של תמונה מותאמת אישית",
|
||||
"imageUrlHint": "השאר ריק כדי להשתמש בסמל האתר באופן אוטומטי",
|
||||
"showImage": "הצג תמונה",
|
||||
"description": "תֵאוּר",
|
||||
"color": "צֶבַע",
|
||||
"timezone": "אזור זמן",
|
||||
"invalidTimezone": "אזור זמן לא חוקי. השתמש בשם IANA כגון America/New_York",
|
||||
"showSeconds": "הצג שניות",
|
||||
"format12h": "12 שעות",
|
||||
"content": "תוֹכֶן",
|
||||
"backgroundColor": "צבע רקע",
|
||||
"links": "קישורים",
|
||||
"addLink": "הוסף קישור",
|
||||
"linkLabel": "מַדבֵּקָה",
|
||||
"categoryLinks": "קישורים",
|
||||
"categoryInfo": "מידע",
|
||||
"categorySystem": "מַעֲרֶכֶת",
|
||||
"copyLink": "העתק קישור",
|
||||
"linkCopied": "קישור הועתק!",
|
||||
"location": "מִקוּם",
|
||||
"temperatureUnit": "יחידת טמפרטורה",
|
||||
"showForecast": "הצג תחזית ל-3 ימים",
|
||||
"scrolling": "אפשר גלילה",
|
||||
"feedUrl": "כתובת אתר של הזנה",
|
||||
"maxItems": "מקסימום פריטים",
|
||||
"showDescription": "הצג תיאור",
|
||||
"widgetRssName": "עדכון RSS",
|
||||
"noHostSelected": "לא נבחר מארח",
|
||||
"displayedMetrics": "מדדים מוצגים",
|
||||
"metricCpu": "מעבד",
|
||||
"metricMemory": "זֵכֶר",
|
||||
"metricDisk": "דִיסק",
|
||||
"metricUptime": "זמן פעולה",
|
||||
"metricSystem": "מַעֲרֶכֶת",
|
||||
"metricNetwork": "רֶשֶׁת",
|
||||
"metricProcesses": "תהליכים",
|
||||
"categoryMonitoring": "ניטור",
|
||||
"noPingUrls": "לא הוגדרו כתובות URL",
|
||||
"pingLabel": "מַדבֵּקָה",
|
||||
"addPingUrl": "הוסף כתובת URL",
|
||||
"showLatency": "הצג זמן השהייה",
|
||||
"refreshInterval": "מרווח רענון",
|
||||
"seconds": "שניות",
|
||||
"filterActivityTypes": "סוגי סינון",
|
||||
"filterTypesHint": "השאר ריק כדי להציג הכל",
|
||||
"showTimestamp": "הצג חותמת זמן",
|
||||
"uptimeUnavailable": "זמן פעולה לא זמין",
|
||||
"uptimeLabel": "זמן פעולה",
|
||||
"overviewVersion": "גִרְסָה",
|
||||
"overviewUpdateLabel": "לְעַדְכֵּן",
|
||||
"overviewUpdateAvailable": "עדכון זמין",
|
||||
"overviewDatabase": "מסד נתונים",
|
||||
"overviewUptime": "זמן פעולה",
|
||||
"noHosts": "לא הוגדרו מארחים",
|
||||
"hostGridHosts": "מארחים",
|
||||
"hostGridAllHint": "השאר ריק כדי להציג את כל המארחים",
|
||||
"columns": "עמודות",
|
||||
"showIp": "הצג כתובת IP",
|
||||
"layout": "מַעֲרָך",
|
||||
"showStatus": "הצג סטטוס",
|
||||
"noActivity": "אין פעילות",
|
||||
"widgetPingStatusName": "סטטוס פינג",
|
||||
"widgetRecentActivityName": "פעילות אחרונה",
|
||||
"sshTerminalNoHost": "לא הוגדר מארח",
|
||||
"sshTerminalConnect": "לְחַבֵּר",
|
||||
"sshTerminalAutoConnect": "חיבור אוטומטי בעת טעינה",
|
||||
"connectionTypes": "סוגי חיבור",
|
||||
"calMon": "מו",
|
||||
"calTue": "טו",
|
||||
"calWed": "אָנוּ",
|
||||
"calThu": "ה'",
|
||||
"calFri": "פר",
|
||||
"calSat": "סא",
|
||||
"calSun": "סו",
|
||||
"startOnMonday": "תחילת השבוע ביום שני",
|
||||
"countdownNoDate": "לא נקבע תאריך יעד",
|
||||
"countdownPast": "האירוע עבר",
|
||||
"countdownDays": "ימים",
|
||||
"countdownHours": "שעות",
|
||||
"countdownMinutes": "דקות",
|
||||
"countdownSeconds": "שניות",
|
||||
"countdownLabel": "מַדבֵּקָה",
|
||||
"countdownLabelPlaceholder": "למשל יום ההשקה",
|
||||
"countdownShowDays": "ימי המופע",
|
||||
"countdownShowHours": "שעות המופע",
|
||||
"targetDate": "תאריך יעד",
|
||||
"searchEngine": "מנוע חיפוש",
|
||||
"customSearchUrl": "כתובת אתר לחיפוש מותאמת אישית",
|
||||
"searchPlaceholder": "לְחַפֵּשׂ...",
|
||||
"searchGo": "לָלֶכֶת",
|
||||
"searchPlaceholderLabel": "טקסט מציין מיקום",
|
||||
"searchPlaceholderHint": "טקסט המוצג בתוך קלט החיפוש",
|
||||
"openInNewTab": "פתח בכרטיסייה חדשה",
|
||||
"searchQueryPlaceholder": "הזן שאילתה...",
|
||||
"noSearchShortcuts": "לא הוגדרו קיצורי דרך",
|
||||
"addSearchShortcut": "הוסף קיצור דרך",
|
||||
"fontSize": "גודל גופן",
|
||||
"textAlign": "יישור טקסט",
|
||||
"fontWeight": "משקל גופן",
|
||||
"clearColor": "צבע שקוף",
|
||||
"imageFit": "התאמת תמונה",
|
||||
"imageLinkUrl": "כתובת אתר של קישור",
|
||||
"noImage": "לא הוגדרה כתובת URL של תמונה",
|
||||
"altText": "טקסט חלופי",
|
||||
"altTextPlaceholder": "תאר את התמונה",
|
||||
"renderMarkdown": "רינדור סימון למטה",
|
||||
"displayMode": "מצב תצוגה",
|
||||
"displayField": "שדה תצוגה",
|
||||
"jsonPath": "נתיב JSON",
|
||||
"customApiLabel": "מַדבֵּקָה",
|
||||
"customApiLabelPlaceholder": "לדוגמה טמפרטורה",
|
||||
"customApiUnit": "יְחִידָה",
|
||||
"customApiNoUrl": "לא הוגדרה כתובת URL של API",
|
||||
"customApiError": "נכשלה האחזור",
|
||||
"customApiNotArray": "התגובה אינה מערך",
|
||||
"addService": "הוסף שירות",
|
||||
"showLabels": "הצג תוויות",
|
||||
"iconSize": "גודל סמל",
|
||||
"noDashboardLinks": "לא הוגדרו קישורי לוח מחוונים",
|
||||
"noLimit": "אין הגבלה",
|
||||
"sectionHeading": "כותרת סעיף",
|
||||
"addSection": "הוסף מדור",
|
||||
"compactMode": "מצב קומפקטי",
|
||||
"selectHosts": "בחר מארחים",
|
||||
"listLayout": "רְשִׁימָה",
|
||||
"gridLayout": "רֶשֶׁת"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "होमपेज"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "सेवा लिंक",
|
||||
"homepagePreview": "मुखपृष्ठ पूर्वावलोकन"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "सर्विस लिंक्स",
|
||||
"serviceLinksEmpty": "अभी तक कोई सर्विस लिंक नहीं",
|
||||
"serviceLinksAdd": "जोड़ें",
|
||||
"serviceLinksLabelPlaceholder": "मेरी सर्विस",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "एक मान्य वेब पता दर्ज करें",
|
||||
"serviceLinksAddFailed": "सर्विस लिंक जोड़ने में विफल"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "होमपेज",
|
||||
"addWidget": "विजेट जोड़ें",
|
||||
"editWidget": "विजेट संपादित करें",
|
||||
"zoomIn": "ज़ूम इन",
|
||||
"zoomOut": "ज़ूम आउट",
|
||||
"resetView": "दृश्य रीसेट करें",
|
||||
"lockLayout": "लेआउट लॉक करें",
|
||||
"unlockLayout": "लेआउट अनलॉक करें",
|
||||
"noWidgets": "अपना पहला विजेट जोड़ने के लिए राइट-क्लिक करें या + पर क्लिक करें",
|
||||
"openFullView": "पूर्ण दृश्य खोलें",
|
||||
"previewTitle": "होमपेज पूर्वावलोकन",
|
||||
"cancel": "रद्द करें",
|
||||
"save": "सहेजें",
|
||||
"title_label": "शीर्षक",
|
||||
"widgetTitlePlaceholder": "विजेट शीर्षक (वैकल्पिक)",
|
||||
"url": "URL",
|
||||
"imageUrl": "कस्टम इमेज URL",
|
||||
"imageUrlHint": "साइट फ़ेविकॉन का स्वचालित रूप से उपयोग करने के लिए खाली छोड़ें",
|
||||
"showImage": "इमेज दिखाएँ",
|
||||
"description": "विवरण",
|
||||
"color": "रंग",
|
||||
"timezone": "समय क्षेत्र",
|
||||
"invalidTimezone": "समय क्षेत्र अमान्य है। कृपया IANA नाम का उपयोग करें, जैसे कि America/New_York",
|
||||
"showSeconds": "सेकंड दिखाएँ",
|
||||
"format12h": "12-घंटे",
|
||||
"content": "सामग्री",
|
||||
"backgroundColor": "पृष्ठभूमि रंग",
|
||||
"links": "लिंक",
|
||||
"addLink": "लिंक जोड़ें",
|
||||
"linkLabel": "लेबल",
|
||||
"categoryLinks": "लिंक",
|
||||
"categoryInfo": "जानकारी",
|
||||
"categorySystem": "सिस्टम",
|
||||
"copyLink": "लिंक कॉपी करें",
|
||||
"linkCopied": "लिंक कॉपी हो गया!",
|
||||
"location": "स्थान",
|
||||
"temperatureUnit": "तापमान इकाई",
|
||||
"showForecast": "3-दिन का पूर्वानुमान दिखाएं",
|
||||
"scrolling": "स्क्रॉलिंग की अनुमति दें",
|
||||
"feedUrl": "फ़ीड URL",
|
||||
"maxItems": "अधिकतम आइटम",
|
||||
"showDescription": "विवरण दिखाएं",
|
||||
"widgetRssName": "RSS फ़ीड",
|
||||
"noHostSelected": "कोई होस्ट चयनित नहीं",
|
||||
"displayedMetrics": "प्रदर्शित मेट्रिक्स",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "मेमोरी",
|
||||
"metricDisk": "डिस्क",
|
||||
"metricUptime": "अपटाइम",
|
||||
"metricSystem": "सिस्टम",
|
||||
"metricNetwork": "नेटवर्क",
|
||||
"metricProcesses": "प्रोसेस",
|
||||
"categoryMonitoring": "निगरानी",
|
||||
"noPingUrls": "कोई URL कॉन्फ़िगर नहीं किया गया",
|
||||
"pingLabel": "लेबल",
|
||||
"addPingUrl": "URL जोड़ें",
|
||||
"showLatency": "लेटेंसी दिखाएं",
|
||||
"refreshInterval": "रीफ़्रेश अंतराल",
|
||||
"seconds": "सेकंड",
|
||||
"filterActivityTypes": "प्रकार फ़िल्टर करें",
|
||||
"filterTypesHint": "सभी दिखाने के लिए खाली छोड़ें",
|
||||
"showTimestamp": "टाइमस्टैम्प दिखाएं",
|
||||
"uptimeUnavailable": "अपटाइम अनुपलब्ध",
|
||||
"uptimeLabel": "अपटाइम",
|
||||
"overviewVersion": "संस्करण",
|
||||
"overviewUpdateLabel": "अद्यतन",
|
||||
"overviewUpdateAvailable": "अपडेट उपलब्ध है",
|
||||
"overviewDatabase": "डेटाबेस",
|
||||
"overviewUptime": "अपटाइम",
|
||||
"noHosts": "कोई होस्ट कॉन्फ़िगर नहीं किए गए",
|
||||
"hostGridHosts": "होस्ट",
|
||||
"hostGridAllHint": "सभी होस्ट दिखाने के लिए खाली छोड़ें",
|
||||
"columns": "कॉलम",
|
||||
"showIp": "IP पता दिखाएं",
|
||||
"layout": "लेआउट",
|
||||
"showStatus": "स्थिति दिखाएं",
|
||||
"noActivity": "कोई गतिविधि नहीं",
|
||||
"widgetPingStatusName": "पिंग स्थिति",
|
||||
"widgetRecentActivityName": "हाल की गतिविधि",
|
||||
"sshTerminalNoHost": "कोई होस्ट कॉन्फ़िगर नहीं किया गया",
|
||||
"sshTerminalConnect": "कनेक्ट करें",
|
||||
"sshTerminalAutoConnect": "लोड पर स्वतः कनेक्ट",
|
||||
"connectionTypes": "कनेक्शन प्रकार",
|
||||
"calMon": "सोम",
|
||||
"calTue": "मंगल",
|
||||
"calWed": "बुध",
|
||||
"calThu": "गुरु",
|
||||
"calFri": "शुक्र",
|
||||
"calSat": "शनि",
|
||||
"calSun": "रवि",
|
||||
"startOnMonday": "सप्ताह सोमवार से शुरू करें",
|
||||
"countdownNoDate": "कोई लक्ष्य तिथि निर्धारित नहीं",
|
||||
"countdownPast": "ईवेंट बीत चुका है",
|
||||
"countdownDays": "दिन",
|
||||
"countdownHours": "घंटे",
|
||||
"countdownMinutes": "मिनट",
|
||||
"countdownSeconds": "सेकंड",
|
||||
"countdownLabel": "लेबल",
|
||||
"countdownLabelPlaceholder": "उदा. लॉन्च दिवस",
|
||||
"countdownShowDays": "दिन दिखाएं",
|
||||
"countdownShowHours": "घंटे दिखाएं",
|
||||
"targetDate": "लक्ष्य तिथि",
|
||||
"searchEngine": "खोज इंजन",
|
||||
"customSearchUrl": "कस्टम खोज URL",
|
||||
"searchPlaceholder": "खोजें...",
|
||||
"searchGo": "जाएं",
|
||||
"searchPlaceholderLabel": "प्लेसहोल्डर टेक्स्ट",
|
||||
"searchPlaceholderHint": "खोज इनपुट के अंदर दिखाया जाने वाला पाठ",
|
||||
"openInNewTab": "नए टैब में खोलें",
|
||||
"searchQueryPlaceholder": "क्वेरी दर्ज करें...",
|
||||
"noSearchShortcuts": "कोई शॉर्टकट कॉन्फ़िगर नहीं है",
|
||||
"addSearchShortcut": "शॉर्टकट जोड़ें",
|
||||
"fontSize": "फ़ॉन्ट आकार",
|
||||
"textAlign": "पाठ संरेखण",
|
||||
"fontWeight": "फ़ॉन्ट मोटाई",
|
||||
"clearColor": "रंग साफ़ करें",
|
||||
"imageFit": "छवि फ़िट",
|
||||
"imageLinkUrl": "लिंक URL",
|
||||
"noImage": "कोई छवि URL सेट नहीं है",
|
||||
"altText": "Alt टेक्स्ट",
|
||||
"altTextPlaceholder": "छवि का विवरण दें",
|
||||
"renderMarkdown": "मार्कडाउन रेंडर करें",
|
||||
"displayMode": "प्रदर्शन मोड",
|
||||
"displayField": "प्रदर्शन फ़ील्ड",
|
||||
"jsonPath": "JSON पथ",
|
||||
"customApiLabel": "लेबल",
|
||||
"customApiLabelPlaceholder": "उदा. तापमान",
|
||||
"customApiUnit": "इकाई",
|
||||
"customApiNoUrl": "कोई API URL कॉन्फ़िगर नहीं है",
|
||||
"customApiError": "फ़ेच करने में विफल",
|
||||
"customApiNotArray": "प्रतिक्रिया एक ऐरे नहीं है",
|
||||
"addService": "सेवा जोड़ें",
|
||||
"showLabels": "लेबल दिखाएं",
|
||||
"iconSize": "आइकन का आकार",
|
||||
"noDashboardLinks": "कोई डैशबोर्ड लिंक कॉन्फ़िगर नहीं किया गया",
|
||||
"noLimit": "कोई सीमा नहीं",
|
||||
"sectionHeading": "अनुभाग शीर्षक",
|
||||
"addSection": "अनुभाग जोड़ें",
|
||||
"compactMode": "संक्षिप्त मोड",
|
||||
"selectHosts": "होस्ट चुनें",
|
||||
"listLayout": "सूची",
|
||||
"gridLayout": "ग्रिड"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Kezdőlap"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Szolgáltatási linkek",
|
||||
"homepagePreview": "Kezdőlap előnézete"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Szolgáltatási linkek",
|
||||
"serviceLinksEmpty": "Még nincsenek szolgáltatáslinkek",
|
||||
"serviceLinksAdd": "Hozzáadás",
|
||||
"serviceLinksLabelPlaceholder": "Szolgáltatásom",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Adjon meg egy érvényes webcímet",
|
||||
"serviceLinksAddFailed": "Nem sikerült hozzáadni a szolgáltatás linkjét"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Kezdőlap",
|
||||
"addWidget": "Widget hozzáadása",
|
||||
"editWidget": "Widget szerkesztése",
|
||||
"zoomIn": "Nagyítás",
|
||||
"zoomOut": "Kicsinyítés",
|
||||
"resetView": "Nézet visszaállítása",
|
||||
"lockLayout": "Zár elrendezése",
|
||||
"unlockLayout": "Elrendezés feloldása",
|
||||
"noWidgets": "Kattintson jobb gombbal, vagy kattintson a + ikonra az első widget hozzáadásához",
|
||||
"openFullView": "Teljes nézet megnyitása",
|
||||
"previewTitle": "Kezdőlap előnézete",
|
||||
"cancel": "Mégsem",
|
||||
"save": "Megtakarítás",
|
||||
"title_label": "Cím",
|
||||
"widgetTitlePlaceholder": "Widget címe (opcionális)",
|
||||
"url": "URL-cím",
|
||||
"imageUrl": "Egyéni kép URL-címe",
|
||||
"imageUrlHint": "Hagyja üresen a webhely faviconjának automatikus használatához",
|
||||
"showImage": "Kép megjelenítése",
|
||||
"description": "Leírás",
|
||||
"color": "Szín",
|
||||
"timezone": "Időzóna",
|
||||
"invalidTimezone": "Érvénytelen időzóna. Használjon IANA nevet, például Amerika/New_York",
|
||||
"showSeconds": "Másodpercek megjelenítése",
|
||||
"format12h": "12 órás",
|
||||
"content": "Tartalom",
|
||||
"backgroundColor": "Háttérszín",
|
||||
"links": "Linkek",
|
||||
"addLink": "Link hozzáadása",
|
||||
"linkLabel": "Címke",
|
||||
"categoryLinks": "Linkek",
|
||||
"categoryInfo": "Információ",
|
||||
"categorySystem": "Rendszer",
|
||||
"copyLink": "Link másolása",
|
||||
"linkCopied": "Link másolva!",
|
||||
"location": "Elhelyezkedés",
|
||||
"temperatureUnit": "Hőmérséklet egység",
|
||||
"showForecast": "3 napos előrejelzés megjelenítése",
|
||||
"scrolling": "Görgetés engedélyezése",
|
||||
"feedUrl": "Hírcsatorna URL-címe",
|
||||
"maxItems": "Max. tételek",
|
||||
"showDescription": "Leírás megjelenítése",
|
||||
"widgetRssName": "RSS-hírcsatorna",
|
||||
"noHostSelected": "Nincs kiválasztva gazdagép",
|
||||
"displayedMetrics": "Megjelenített mutatók",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "Memória",
|
||||
"metricDisk": "Korong",
|
||||
"metricUptime": "Üzemidő",
|
||||
"metricSystem": "Rendszer",
|
||||
"metricNetwork": "Hálózat",
|
||||
"metricProcesses": "Folyamatok",
|
||||
"categoryMonitoring": "Monitoring",
|
||||
"noPingUrls": "Nincsenek konfigurált URL-ek",
|
||||
"pingLabel": "Címke",
|
||||
"addPingUrl": "URL hozzáadása",
|
||||
"showLatency": "Késleltetés megjelenítése",
|
||||
"refreshInterval": "Frissítési intervallum",
|
||||
"seconds": "másodperc",
|
||||
"filterActivityTypes": "Szűrőtípusok",
|
||||
"filterTypesHint": "Hagyja üresen az összes megjelenítéséhez",
|
||||
"showTimestamp": "Időbélyeg megjelenítése",
|
||||
"uptimeUnavailable": "Üzemidő nem elérhető",
|
||||
"uptimeLabel": "Üzemidő",
|
||||
"overviewVersion": "Változat",
|
||||
"overviewUpdateLabel": "Frissítés",
|
||||
"overviewUpdateAvailable": "Frissítés elérhető",
|
||||
"overviewDatabase": "Adatbázis",
|
||||
"overviewUptime": "Üzemidő",
|
||||
"noHosts": "Nincsenek konfigurált gazdagépek",
|
||||
"hostGridHosts": "Házigazdák",
|
||||
"hostGridAllHint": "Hagyja üresen az összes hoszt megjelenítéséhez",
|
||||
"columns": "Oszlopok",
|
||||
"showIp": "IP-cím megjelenítése",
|
||||
"layout": "Elrendezés",
|
||||
"showStatus": "Állapot megjelenítése",
|
||||
"noActivity": "Nincs aktivitás",
|
||||
"widgetPingStatusName": "Ping állapot",
|
||||
"widgetRecentActivityName": "Legutóbbi tevékenységek",
|
||||
"sshTerminalNoHost": "Nincs konfigurálva gazdagép",
|
||||
"sshTerminalConnect": "Csatlakozás",
|
||||
"sshTerminalAutoConnect": "Automatikus csatlakozás terheléskor",
|
||||
"connectionTypes": "Kapcsolattípusok",
|
||||
"calMon": "Mo",
|
||||
"calTue": "Kedd",
|
||||
"calWed": "Mi",
|
||||
"calThu": "Cs",
|
||||
"calFri": "Péntek",
|
||||
"calSat": "Sa",
|
||||
"calSun": "Su",
|
||||
"startOnMonday": "Hétfőn kezdjük a hetet",
|
||||
"countdownNoDate": "Nincs kitűzött céldátum",
|
||||
"countdownPast": "Az esemény elmúlt",
|
||||
"countdownDays": "napok",
|
||||
"countdownHours": "óra",
|
||||
"countdownMinutes": "perc",
|
||||
"countdownSeconds": "másodperc",
|
||||
"countdownLabel": "Címke",
|
||||
"countdownLabelPlaceholder": "pl. Indítási nap",
|
||||
"countdownShowDays": "Kiállítási napok",
|
||||
"countdownShowHours": "Nyitvatartási idő megjelenítése",
|
||||
"targetDate": "Céldátum",
|
||||
"searchEngine": "Keresőmotor",
|
||||
"customSearchUrl": "Egyéni keresési URL",
|
||||
"searchPlaceholder": "Keresés...",
|
||||
"searchGo": "Megy",
|
||||
"searchPlaceholderLabel": "Helyőrző szöveg",
|
||||
"searchPlaceholderHint": "A keresési mezőben megjelenített szöveg",
|
||||
"openInNewTab": "Megnyitás új lapon",
|
||||
"searchQueryPlaceholder": "Írja be a lekérdezést...",
|
||||
"noSearchShortcuts": "Nincsenek konfigurált gyorsbillentyűk",
|
||||
"addSearchShortcut": "Parancsikon hozzáadása",
|
||||
"fontSize": "Betűméret",
|
||||
"textAlign": "Szöveg igazítása",
|
||||
"fontWeight": "Betűvastagság",
|
||||
"clearColor": "Tiszta szín",
|
||||
"imageFit": "Kép illesztése",
|
||||
"imageLinkUrl": "Link URL-címe",
|
||||
"noImage": "Nincs beállítva kép URL-címe",
|
||||
"altText": "Alt szöveg",
|
||||
"altTextPlaceholder": "Írd le a képet",
|
||||
"renderMarkdown": "Markdown renderelés",
|
||||
"displayMode": "Kijelző mód",
|
||||
"displayField": "Kijelző mező",
|
||||
"jsonPath": "JSON-útvonal",
|
||||
"customApiLabel": "Címke",
|
||||
"customApiLabelPlaceholder": "pl. hőmérséklet",
|
||||
"customApiUnit": "Egység",
|
||||
"customApiNoUrl": "Nincs konfigurálva API URL",
|
||||
"customApiError": "Nem sikerült lekérni",
|
||||
"customApiNotArray": "A válasz nem tömb",
|
||||
"addService": "Szolgáltatás hozzáadása",
|
||||
"showLabels": "Címkék megjelenítése",
|
||||
"iconSize": "Ikonméret",
|
||||
"noDashboardLinks": "Nincsenek konfigurált irányítópult-hivatkozások",
|
||||
"noLimit": "Nincs korlát",
|
||||
"sectionHeading": "Szakasz címsora",
|
||||
"addSection": "Szakasz hozzáadása",
|
||||
"compactMode": "Kompakt mód",
|
||||
"selectHosts": "Válasszon hosztokat",
|
||||
"listLayout": "Lista",
|
||||
"gridLayout": "Rács"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Beranda"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Tautan Layanan",
|
||||
"homepagePreview": "Pratinjau Halaman Utama"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Tautan Layanan",
|
||||
"serviceLinksEmpty": "Belum ada tautan layanan.",
|
||||
"serviceLinksAdd": "Menambahkan",
|
||||
"serviceLinksLabelPlaceholder": "Layanan Saya",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Masukkan alamat web yang valid",
|
||||
"serviceLinksAddFailed": "Gagal menambahkan tautan layanan"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Beranda",
|
||||
"addWidget": "Tambahkan Widget",
|
||||
"editWidget": "Edit Widget",
|
||||
"zoomIn": "Perbesar",
|
||||
"zoomOut": "Perkecil tampilan",
|
||||
"resetView": "Atur Ulang Tampilan",
|
||||
"lockLayout": "Tata Letak Kunci",
|
||||
"unlockLayout": "Buka Tata Letak",
|
||||
"noWidgets": "Klik kanan atau klik + untuk menambahkan widget pertama Anda.",
|
||||
"openFullView": "Buka Tampilan Penuh",
|
||||
"previewTitle": "Pratinjau Halaman Utama",
|
||||
"cancel": "Membatalkan",
|
||||
"save": "Menyimpan",
|
||||
"title_label": "Judul",
|
||||
"widgetTitlePlaceholder": "Judul widget (opsional)",
|
||||
"url": "URL",
|
||||
"imageUrl": "URL Gambar Kustom",
|
||||
"imageUrlHint": "Biarkan kosong untuk menggunakan favicon situs secara otomatis.",
|
||||
"showImage": "Tampilkan Gambar",
|
||||
"description": "Keterangan",
|
||||
"color": "Warna",
|
||||
"timezone": "Zona waktu",
|
||||
"invalidTimezone": "Zona waktu tidak valid. Gunakan nama IANA seperti America/New_York.",
|
||||
"showSeconds": "Tampilkan detik",
|
||||
"format12h": "12 jam",
|
||||
"content": "Isi",
|
||||
"backgroundColor": "Warna Latar Belakang",
|
||||
"links": "Tautan",
|
||||
"addLink": "Tambahkan Tautan",
|
||||
"linkLabel": "Label",
|
||||
"categoryLinks": "Tautan",
|
||||
"categoryInfo": "Informasi",
|
||||
"categorySystem": "Sistem",
|
||||
"copyLink": "Salin Tautan",
|
||||
"linkCopied": "Tautan berhasil disalin!",
|
||||
"location": "Lokasi",
|
||||
"temperatureUnit": "Satuan Suhu",
|
||||
"showForecast": "Tampilkan ramalan cuaca 3 hari ke depan",
|
||||
"scrolling": "Izinkan Pengguliran",
|
||||
"feedUrl": "URL Umpan",
|
||||
"maxItems": "Jumlah Item Maksimum",
|
||||
"showDescription": "Tampilkan Deskripsi",
|
||||
"widgetRssName": "Umpan RSS",
|
||||
"noHostSelected": "Tidak ada pembawa acara yang dipilih.",
|
||||
"displayedMetrics": "Metrik yang Ditampilkan",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "Ingatan",
|
||||
"metricDisk": "Disk",
|
||||
"metricUptime": "Waktu aktif",
|
||||
"metricSystem": "Sistem",
|
||||
"metricNetwork": "Jaringan",
|
||||
"metricProcesses": "Proses",
|
||||
"categoryMonitoring": "Pemantauan",
|
||||
"noPingUrls": "Tidak ada URL yang dikonfigurasi",
|
||||
"pingLabel": "Label",
|
||||
"addPingUrl": "Tambahkan URL",
|
||||
"showLatency": "Tampilkan Latensi",
|
||||
"refreshInterval": "Interval Penyegaran",
|
||||
"seconds": "detik",
|
||||
"filterActivityTypes": "Jenis Filter",
|
||||
"filterTypesHint": "Biarkan kosong untuk menampilkan semua",
|
||||
"showTimestamp": "Tampilkan Cap Waktu",
|
||||
"uptimeUnavailable": "Waktu aktif tidak tersedia",
|
||||
"uptimeLabel": "Waktu aktif",
|
||||
"overviewVersion": "Versi",
|
||||
"overviewUpdateLabel": "Memperbarui",
|
||||
"overviewUpdateAvailable": "Pembaruan tersedia",
|
||||
"overviewDatabase": "Basis data",
|
||||
"overviewUptime": "Waktu aktif",
|
||||
"noHosts": "Tidak ada host yang dikonfigurasi.",
|
||||
"hostGridHosts": "Tuan rumah",
|
||||
"hostGridAllHint": "Biarkan kosong untuk menampilkan semua host",
|
||||
"columns": "Kolom",
|
||||
"showIp": "Tampilkan Alamat IP",
|
||||
"layout": "Tata Letak",
|
||||
"showStatus": "Tampilkan Status",
|
||||
"noActivity": "Tidak ada kegiatan",
|
||||
"widgetPingStatusName": "Status Ping",
|
||||
"widgetRecentActivityName": "Aktivitas Terkini",
|
||||
"sshTerminalNoHost": "Tidak ada host yang dikonfigurasi.",
|
||||
"sshTerminalConnect": "Menghubungkan",
|
||||
"sshTerminalAutoConnect": "Terhubung otomatis saat dimuat",
|
||||
"connectionTypes": "Jenis Koneksi",
|
||||
"calMon": "Mo",
|
||||
"calTue": "Tu",
|
||||
"calWed": "Kami",
|
||||
"calThu": "Th",
|
||||
"calFri": "Fr",
|
||||
"calSat": "Sa",
|
||||
"calSun": "Su",
|
||||
"startOnMonday": "Minggu dimulai pada hari Senin.",
|
||||
"countdownNoDate": "Tidak ada tanggal target yang ditetapkan.",
|
||||
"countdownPast": "Acara telah berlalu",
|
||||
"countdownDays": "hari",
|
||||
"countdownHours": "jam",
|
||||
"countdownMinutes": "menit",
|
||||
"countdownSeconds": "detik",
|
||||
"countdownLabel": "Label",
|
||||
"countdownLabelPlaceholder": "misalnya Hari Peluncuran",
|
||||
"countdownShowDays": "Hari Pertunjukan",
|
||||
"countdownShowHours": "Jam Pertunjukan",
|
||||
"targetDate": "Tanggal Target",
|
||||
"searchEngine": "Mesin Pencari",
|
||||
"customSearchUrl": "URL Pencarian Kustom",
|
||||
"searchPlaceholder": "Mencari...",
|
||||
"searchGo": "Pergi",
|
||||
"searchPlaceholderLabel": "Teks Placeholder",
|
||||
"searchPlaceholderHint": "Teks yang ditampilkan di dalam kolom pencarian",
|
||||
"openInNewTab": "Buka di Tab Baru",
|
||||
"searchQueryPlaceholder": "Masukkan pertanyaan...",
|
||||
"noSearchShortcuts": "Tidak ada pintasan yang dikonfigurasi.",
|
||||
"addSearchShortcut": "Tambahkan Pintasan",
|
||||
"fontSize": "Ukuran Huruf",
|
||||
"textAlign": "Perataan Teks",
|
||||
"fontWeight": "Ketebalan Huruf",
|
||||
"clearColor": "Warna Bening",
|
||||
"imageFit": "Pencocokan Gambar",
|
||||
"imageLinkUrl": "Tautan URL",
|
||||
"noImage": "Tidak ada URL gambar yang ditetapkan.",
|
||||
"altText": "Teks Alternatif",
|
||||
"altTextPlaceholder": "Jelaskan gambar tersebut",
|
||||
"renderMarkdown": "Render Markdown",
|
||||
"displayMode": "Mode Tampilan",
|
||||
"displayField": "Bidang Tampilan",
|
||||
"jsonPath": "Jalur JSON",
|
||||
"customApiLabel": "Label",
|
||||
"customApiLabelPlaceholder": "misalnya Suhu",
|
||||
"customApiUnit": "Satuan",
|
||||
"customApiNoUrl": "Tidak ada URL API yang dikonfigurasi.",
|
||||
"customApiError": "Gagal mengambil",
|
||||
"customApiNotArray": "Respons bukanlah sebuah array.",
|
||||
"addService": "Tambahkan Layanan",
|
||||
"showLabels": "Tampilkan Label",
|
||||
"iconSize": "Ukuran Ikon",
|
||||
"noDashboardLinks": "Tidak ada tautan dasbor yang dikonfigurasi.",
|
||||
"noLimit": "Tidak ada batasan",
|
||||
"sectionHeading": "Judul Bagian",
|
||||
"addSection": "Tambahkan Bagian",
|
||||
"compactMode": "Mode Ringkas",
|
||||
"selectHosts": "Pilih Host",
|
||||
"listLayout": "Daftar",
|
||||
"gridLayout": "Jaringan"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Homepage"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Link ai servizi",
|
||||
"homepagePreview": "Anteprima home page"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Collegamenti ai servizi",
|
||||
"serviceLinksEmpty": "Nessun collegamento ai servizi",
|
||||
"serviceLinksAdd": "Aggiungi",
|
||||
"serviceLinksLabelPlaceholder": "Il mio servizio",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Inserisci un indirizzo web valido",
|
||||
"serviceLinksAddFailed": "Impossibile aggiungere il collegamento al servizio"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Pagina iniziale",
|
||||
"addWidget": "Aggiungi widget",
|
||||
"editWidget": "Modifica widget",
|
||||
"zoomIn": "Ingrandisci",
|
||||
"zoomOut": "Rimpicciolisci",
|
||||
"resetView": "Reimposta vista",
|
||||
"lockLayout": "Blocca layout",
|
||||
"unlockLayout": "Sblocca layout",
|
||||
"noWidgets": "Fai clic con il tasto destro o clicca su + per aggiungere il tuo primo widget",
|
||||
"openFullView": "Apri vista completa",
|
||||
"previewTitle": "Anteprima homepage",
|
||||
"cancel": "Annulla",
|
||||
"save": "Salva",
|
||||
"title_label": "Titolo",
|
||||
"widgetTitlePlaceholder": "Titolo widget (opzionale)",
|
||||
"url": "URL",
|
||||
"imageUrl": "URL immagine personalizzata",
|
||||
"imageUrlHint": "Lascia vuoto per utilizzare automaticamente la favicon del sito",
|
||||
"showImage": "Mostra immagine",
|
||||
"description": "Descrizione",
|
||||
"color": "Colore",
|
||||
"timezone": "Fuso orario",
|
||||
"invalidTimezone": "Fuso orario non valido. Utilizzare un nome IANA come America/New_York",
|
||||
"showSeconds": "Mostra secondi",
|
||||
"format12h": "12 ore",
|
||||
"content": "Contenuto",
|
||||
"backgroundColor": "Colore sfondo",
|
||||
"links": "Collegamenti",
|
||||
"addLink": "Aggiungi Collegamento",
|
||||
"linkLabel": "Etichetta",
|
||||
"categoryLinks": "Collegamenti",
|
||||
"categoryInfo": "Info",
|
||||
"categorySystem": "Sistema",
|
||||
"copyLink": "Copia Collegamento",
|
||||
"linkCopied": "Collegamento copiato!",
|
||||
"location": "Località",
|
||||
"temperatureUnit": "Unità di temperatura",
|
||||
"showForecast": "Mostra previsioni a 3 giorni",
|
||||
"scrolling": "Consenti scorrimento",
|
||||
"feedUrl": "URL del feed",
|
||||
"maxItems": "Numero massimo di elementi",
|
||||
"showDescription": "Mostra descrizione",
|
||||
"widgetRssName": "Feed RSS",
|
||||
"noHostSelected": "Nessun host selezionato",
|
||||
"displayedMetrics": "Metriche visualizzate",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "Memoria",
|
||||
"metricDisk": "Disco",
|
||||
"metricUptime": "Uptime",
|
||||
"metricSystem": "Sistema",
|
||||
"metricNetwork": "Rete",
|
||||
"metricProcesses": "Processi",
|
||||
"categoryMonitoring": "Monitoraggio",
|
||||
"noPingUrls": "Nessun URL configurato",
|
||||
"pingLabel": "Etichetta",
|
||||
"addPingUrl": "Aggiungi URL",
|
||||
"showLatency": "Mostra latenza",
|
||||
"refreshInterval": "Intervallo di aggiornamento",
|
||||
"seconds": "secondi",
|
||||
"filterActivityTypes": "Filtra tipi",
|
||||
"filterTypesHint": "Lascia vuoto per mostrare tutto",
|
||||
"showTimestamp": "Mostra timestamp",
|
||||
"uptimeUnavailable": "Uptime non disponibile",
|
||||
"uptimeLabel": "Uptime",
|
||||
"overviewVersion": "Versione",
|
||||
"overviewUpdateLabel": "Aggiornamento",
|
||||
"overviewUpdateAvailable": "Aggiornamento disponibile",
|
||||
"overviewDatabase": "Database",
|
||||
"overviewUptime": "Uptime",
|
||||
"noHosts": "Nessun host configurato",
|
||||
"hostGridHosts": "Host",
|
||||
"hostGridAllHint": "Lascia vuoto per mostrare tutti gli host",
|
||||
"columns": "Colonne",
|
||||
"showIp": "Mostra indirizzo IP",
|
||||
"layout": "Layout",
|
||||
"showStatus": "Mostra stato",
|
||||
"noActivity": "Nessuna attività",
|
||||
"widgetPingStatusName": "Stato ping",
|
||||
"widgetRecentActivityName": "Attività recente",
|
||||
"sshTerminalNoHost": "Nessun host configurato",
|
||||
"sshTerminalConnect": "Connetti",
|
||||
"sshTerminalAutoConnect": "Connessione automatica al caricamento",
|
||||
"connectionTypes": "Tipi di connessione",
|
||||
"calMon": "Lu",
|
||||
"calTue": "Ma",
|
||||
"calWed": "Me",
|
||||
"calThu": "Gi",
|
||||
"calFri": "Ve",
|
||||
"calSat": "Sa",
|
||||
"calSun": "Do",
|
||||
"startOnMonday": "Inizia la settimana il lunedì",
|
||||
"countdownNoDate": "Nessuna data obiettivo impostata",
|
||||
"countdownPast": "Evento passato",
|
||||
"countdownDays": "giorni",
|
||||
"countdownHours": "ore",
|
||||
"countdownMinutes": "min",
|
||||
"countdownSeconds": "sec",
|
||||
"countdownLabel": "Etichetta",
|
||||
"countdownLabelPlaceholder": "es. Giorno di lancio",
|
||||
"countdownShowDays": "Mostra giorni",
|
||||
"countdownShowHours": "Mostra ore",
|
||||
"targetDate": "Data obiettivo",
|
||||
"searchEngine": "Motore di ricerca",
|
||||
"customSearchUrl": "URL di ricerca personalizzato",
|
||||
"searchPlaceholder": "Cerca...",
|
||||
"searchGo": "Vai",
|
||||
"searchPlaceholderLabel": "Testo segnaposto",
|
||||
"searchPlaceholderHint": "Testo mostrato nel campo di ricerca",
|
||||
"openInNewTab": "Apri in nuova scheda",
|
||||
"searchQueryPlaceholder": "Inserisci query...",
|
||||
"noSearchShortcuts": "Nessuna scorciatoia configurata",
|
||||
"addSearchShortcut": "Aggiungi scorciatoia",
|
||||
"fontSize": "Dimensione carattere",
|
||||
"textAlign": "Allineamento testo",
|
||||
"fontWeight": "Spessore carattere",
|
||||
"clearColor": "Rimuovi colore",
|
||||
"imageFit": "Adatta immagine",
|
||||
"imageLinkUrl": "URL collegamento",
|
||||
"noImage": "Nessun URL immagine impostato",
|
||||
"altText": "Testo alternativo",
|
||||
"altTextPlaceholder": "Descrivi l'immagine",
|
||||
"renderMarkdown": "Visualizza Markdown",
|
||||
"displayMode": "Modalità visualizzazione",
|
||||
"displayField": "Campo visualizzazione",
|
||||
"jsonPath": "Percorso JSON",
|
||||
"customApiLabel": "Etichetta",
|
||||
"customApiLabelPlaceholder": "es. Temperatura",
|
||||
"customApiUnit": "Unità",
|
||||
"customApiNoUrl": "Nessun URL API configurato",
|
||||
"customApiError": "Recupero fallito",
|
||||
"customApiNotArray": "La risposta non è un array",
|
||||
"addService": "Aggiungi servizio",
|
||||
"showLabels": "Mostra etichette",
|
||||
"iconSize": "Dimensioni icone",
|
||||
"noDashboardLinks": "Nessun collegamento dashboard configurato",
|
||||
"noLimit": "Nessun limite",
|
||||
"sectionHeading": "Intestazione sezione",
|
||||
"addSection": "Aggiungi sezione",
|
||||
"compactMode": "Modalità compatta",
|
||||
"selectHosts": "Seleziona host",
|
||||
"listLayout": "Elenco",
|
||||
"gridLayout": "Griglia"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "ホームページ"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "サービスリンク",
|
||||
"homepagePreview": "ホームページプレビュー"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "サービスリンク",
|
||||
"serviceLinksEmpty": "サービスリンクはまだありません",
|
||||
"serviceLinksAdd": "追加",
|
||||
"serviceLinksLabelPlaceholder": "マイサービス",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "有効なWebアドレスを入力してください",
|
||||
"serviceLinksAddFailed": "サービスリンクの追加に失敗しました"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "ホームページ",
|
||||
"addWidget": "ウィジェットを追加",
|
||||
"editWidget": "ウィジェットを編集",
|
||||
"zoomIn": "拡大",
|
||||
"zoomOut": "縮小",
|
||||
"resetView": "表示をリセット",
|
||||
"lockLayout": "レイアウトをロック",
|
||||
"unlockLayout": "レイアウトのロックを解除",
|
||||
"noWidgets": "右クリックまたは「+」をクリックして最初のウィジェットを追加してください",
|
||||
"openFullView": "全画面表示を開く",
|
||||
"previewTitle": "ホームページプレビュー",
|
||||
"cancel": "キャンセル",
|
||||
"save": "保存",
|
||||
"title_label": "タイトル",
|
||||
"widgetTitlePlaceholder": "ウィジェットのタイトル(任意)",
|
||||
"url": "URL",
|
||||
"imageUrl": "カスタム画像URL",
|
||||
"imageUrlHint": "空白のままにすると、サイトのファビコンが自動的に使用されます",
|
||||
"showImage": "画像を表示",
|
||||
"description": "説明",
|
||||
"color": "色",
|
||||
"timezone": "タイムゾーン",
|
||||
"invalidTimezone": "無効なタイムゾーンです。IANA名(例:America/New_York)を使用してください。",
|
||||
"showSeconds": "秒を表示",
|
||||
"format12h": "12時間表示",
|
||||
"content": "内容",
|
||||
"backgroundColor": "背景色",
|
||||
"links": "リンク",
|
||||
"addLink": "リンクを追加",
|
||||
"linkLabel": "ラベル",
|
||||
"categoryLinks": "リンク",
|
||||
"categoryInfo": "情報",
|
||||
"categorySystem": "システム",
|
||||
"copyLink": "リンクをコピー",
|
||||
"linkCopied": "リンクがコピーされました!",
|
||||
"location": "場所",
|
||||
"temperatureUnit": "温度単位",
|
||||
"showForecast": "3日間の予報を表示",
|
||||
"scrolling": "スクロール許可",
|
||||
"feedUrl": "フィードURL",
|
||||
"maxItems": "最大アイテム数",
|
||||
"showDescription": "説明を表示",
|
||||
"widgetRssName": "RSSフィード",
|
||||
"noHostSelected": "ホストが選択されていません",
|
||||
"displayedMetrics": "表示メトリクス",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "メモリ",
|
||||
"metricDisk": "ディスク",
|
||||
"metricUptime": "稼働時間",
|
||||
"metricSystem": "システム",
|
||||
"metricNetwork": "ネットワーク",
|
||||
"metricProcesses": "プロセス",
|
||||
"categoryMonitoring": "モニタリング",
|
||||
"noPingUrls": "URLが設定されていません",
|
||||
"pingLabel": "ラベル",
|
||||
"addPingUrl": "URLを追加",
|
||||
"showLatency": "レイテンシを表示",
|
||||
"refreshInterval": "更新間隔",
|
||||
"seconds": "秒",
|
||||
"filterActivityTypes": "アクティビティタイプフィルター",
|
||||
"filterTypesHint": "すべてを表示するには空のままにします",
|
||||
"showTimestamp": "タイムスタンプを表示",
|
||||
"uptimeUnavailable": "稼働時間は利用できません",
|
||||
"uptimeLabel": "稼働時間",
|
||||
"overviewVersion": "バージョン",
|
||||
"overviewUpdateLabel": "アップデート",
|
||||
"overviewUpdateAvailable": "アップデートあり",
|
||||
"overviewDatabase": "データベース",
|
||||
"overviewUptime": "稼働時間",
|
||||
"noHosts": "ホストが設定されていません",
|
||||
"hostGridHosts": "ホスト",
|
||||
"hostGridAllHint": "空のままにするとすべてのホストを表示します",
|
||||
"columns": "列",
|
||||
"showIp": "IPアドレスを表示",
|
||||
"layout": "レイアウト",
|
||||
"showStatus": "ステータスを表示",
|
||||
"noActivity": "アクティビティはありません",
|
||||
"widgetPingStatusName": "Pingステータス",
|
||||
"widgetRecentActivityName": "最近のアクティビティ",
|
||||
"sshTerminalNoHost": "ホストが設定されていません",
|
||||
"sshTerminalConnect": "接続",
|
||||
"sshTerminalAutoConnect": "読み込み時に自動接続",
|
||||
"connectionTypes": "接続タイプ",
|
||||
"calMon": "月",
|
||||
"calTue": "火",
|
||||
"calWed": "水",
|
||||
"calThu": "木",
|
||||
"calFri": "金",
|
||||
"calSat": "土",
|
||||
"calSun": "日",
|
||||
"startOnMonday": "週の始まりを月曜に",
|
||||
"countdownNoDate": "目標日が設定されていません",
|
||||
"countdownPast": "イベントは終了しました",
|
||||
"countdownDays": "日",
|
||||
"countdownHours": "時間",
|
||||
"countdownMinutes": "分",
|
||||
"countdownSeconds": "秒",
|
||||
"countdownLabel": "ラベル",
|
||||
"countdownLabelPlaceholder": "例: リリース日",
|
||||
"countdownShowDays": "日数を表示",
|
||||
"countdownShowHours": "時間を表示",
|
||||
"targetDate": "目標日",
|
||||
"searchEngine": "検索エンジン",
|
||||
"customSearchUrl": "カスタム検索URL",
|
||||
"searchPlaceholder": "検索...",
|
||||
"searchGo": "検索",
|
||||
"searchPlaceholderLabel": "プレースホルダーテキスト",
|
||||
"searchPlaceholderHint": "検索入力欄に表示されるテキスト",
|
||||
"openInNewTab": "新しいタブで開く",
|
||||
"searchQueryPlaceholder": "クエリを入力...",
|
||||
"noSearchShortcuts": "設定済みのショートカットはありません",
|
||||
"addSearchShortcut": "ショートカットを追加",
|
||||
"fontSize": "フォントサイズ",
|
||||
"textAlign": "テキスト配置",
|
||||
"fontWeight": "フォントの太さ",
|
||||
"clearColor": "色をクリア",
|
||||
"imageFit": "画像フィット",
|
||||
"imageLinkUrl": "リンクURL",
|
||||
"noImage": "画像URLが設定されていません",
|
||||
"altText": "代替テキスト",
|
||||
"altTextPlaceholder": "画像の説明を入力",
|
||||
"renderMarkdown": "Markdownをレンダリング",
|
||||
"displayMode": "表示モード",
|
||||
"displayField": "表示フィールド",
|
||||
"jsonPath": "JSONパス",
|
||||
"customApiLabel": "ラベル",
|
||||
"customApiLabelPlaceholder": "例: 温度",
|
||||
"customApiUnit": "単位",
|
||||
"customApiNoUrl": "API URLが設定されていません",
|
||||
"customApiError": "取得に失敗しました",
|
||||
"customApiNotArray": "レスポンスが配列ではありません",
|
||||
"addService": "サービスを追加",
|
||||
"showLabels": "ラベルを表示",
|
||||
"iconSize": "アイコンサイズ",
|
||||
"noDashboardLinks": "ダッシュボードリンクが設定されていません",
|
||||
"noLimit": "制限なし",
|
||||
"sectionHeading": "セクション見出し",
|
||||
"addSection": "セクションを追加",
|
||||
"compactMode": "コンパクトモード",
|
||||
"selectHosts": "ホストを選択",
|
||||
"listLayout": "リスト",
|
||||
"gridLayout": "グリッド"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "홈페이지"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "서비스 링크",
|
||||
"homepagePreview": "홈페이지 미리보기"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "서비스 링크",
|
||||
"serviceLinksEmpty": "아직 서비스 링크 없음",
|
||||
"serviceLinksAdd": "추가",
|
||||
"serviceLinksLabelPlaceholder": "내 서비스",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "올바른 웹 주소를 입력하세요",
|
||||
"serviceLinksAddFailed": "서비스 링크 추가 실패"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "홈페이지",
|
||||
"addWidget": "위젯 추가",
|
||||
"editWidget": "위젯 편집",
|
||||
"zoomIn": "확대",
|
||||
"zoomOut": "축소",
|
||||
"resetView": "보기 초기화",
|
||||
"lockLayout": "레이아웃 잠금",
|
||||
"unlockLayout": "레이아웃 잠금 해제",
|
||||
"noWidgets": "마우스 오른쪽 클릭 또는 +를 클릭하여 첫 위젯을 추가하세요.",
|
||||
"openFullView": "전체 보기 열기",
|
||||
"previewTitle": "홈페이지 미리보기",
|
||||
"cancel": "취소",
|
||||
"save": "저장",
|
||||
"title_label": "제목",
|
||||
"widgetTitlePlaceholder": "위젯 제목 (선택 사항)",
|
||||
"url": "URL",
|
||||
"imageUrl": "사용자 지정 이미지 URL",
|
||||
"imageUrlHint": "비워 두면 사이트 파비콘이 자동으로 사용됩니다.",
|
||||
"showImage": "이미지 표시",
|
||||
"description": "설명",
|
||||
"color": "색상",
|
||||
"timezone": "시간대",
|
||||
"invalidTimezone": "잘못된 시간대입니다. America/New_York와 같은 IANA 이름을 사용하십시오.",
|
||||
"showSeconds": "초 표시",
|
||||
"format12h": "12시간",
|
||||
"content": "내용",
|
||||
"backgroundColor": "배경색",
|
||||
"links": "링크",
|
||||
"addLink": "링크 추가",
|
||||
"linkLabel": "레이블",
|
||||
"categoryLinks": "링크",
|
||||
"categoryInfo": "정보",
|
||||
"categorySystem": "시스템",
|
||||
"copyLink": "링크 복사",
|
||||
"linkCopied": "링크가 복사되었습니다!",
|
||||
"location": "위치",
|
||||
"temperatureUnit": "온도 단위",
|
||||
"showForecast": "3일 예보 표시",
|
||||
"scrolling": "스크롤 허용",
|
||||
"feedUrl": "피드 URL",
|
||||
"maxItems": "최대 항목 수",
|
||||
"showDescription": "설명 표시",
|
||||
"widgetRssName": "RSS 피드",
|
||||
"noHostSelected": "선택된 호스트 없음",
|
||||
"displayedMetrics": "표시된 메트릭",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "메모리",
|
||||
"metricDisk": "디스크",
|
||||
"metricUptime": "업타임",
|
||||
"metricSystem": "시스템",
|
||||
"metricNetwork": "네트워크",
|
||||
"metricProcesses": "프로세스",
|
||||
"categoryMonitoring": "모니터링",
|
||||
"noPingUrls": "구성된 URL 없음",
|
||||
"pingLabel": "레이블",
|
||||
"addPingUrl": "URL 추가",
|
||||
"showLatency": "지연 시간 표시",
|
||||
"refreshInterval": "새로 고침 간격",
|
||||
"seconds": "초",
|
||||
"filterActivityTypes": "유형 필터",
|
||||
"filterTypesHint": "모두 보려면 비워 두세요",
|
||||
"showTimestamp": "타임스탬프 표시",
|
||||
"uptimeUnavailable": "업타임 없음",
|
||||
"uptimeLabel": "업타임",
|
||||
"overviewVersion": "버전",
|
||||
"overviewUpdateLabel": "업데이트",
|
||||
"overviewUpdateAvailable": "업데이트 있음",
|
||||
"overviewDatabase": "데이터베이스",
|
||||
"overviewUptime": "업타임",
|
||||
"noHosts": "호스트 구성 없음",
|
||||
"hostGridHosts": "호스트",
|
||||
"hostGridAllHint": "비워 두면 모든 호스트 표시",
|
||||
"columns": "열",
|
||||
"showIp": "IP 주소 표시",
|
||||
"layout": "레이아웃",
|
||||
"showStatus": "상태 표시",
|
||||
"noActivity": "활동 없음",
|
||||
"widgetPingStatusName": "Ping 상태",
|
||||
"widgetRecentActivityName": "최근 활동",
|
||||
"sshTerminalNoHost": "호스트가 구성되지 않음",
|
||||
"sshTerminalConnect": "연결",
|
||||
"sshTerminalAutoConnect": "로드 시 자동 연결",
|
||||
"connectionTypes": "연결 유형",
|
||||
"calMon": "월",
|
||||
"calTue": "화",
|
||||
"calWed": "수",
|
||||
"calThu": "목",
|
||||
"calFri": "금",
|
||||
"calSat": "토",
|
||||
"calSun": "일",
|
||||
"startOnMonday": "주 시작을 월요일로",
|
||||
"countdownNoDate": "목표 날짜가 설정되지 않음",
|
||||
"countdownPast": "이벤트가 지났습니다",
|
||||
"countdownDays": "일",
|
||||
"countdownHours": "시간",
|
||||
"countdownMinutes": "분",
|
||||
"countdownSeconds": "초",
|
||||
"countdownLabel": "레이블",
|
||||
"countdownLabelPlaceholder": "예: 출시일",
|
||||
"countdownShowDays": "일 표시",
|
||||
"countdownShowHours": "시간 표시",
|
||||
"targetDate": "목표 날짜",
|
||||
"searchEngine": "검색 엔진",
|
||||
"customSearchUrl": "사용자 지정 검색 URL",
|
||||
"searchPlaceholder": "검색...",
|
||||
"searchGo": "이동",
|
||||
"searchPlaceholderLabel": "플레이스홀더 텍스트",
|
||||
"searchPlaceholderHint": "검색 입력란에 표시되는 텍스트",
|
||||
"openInNewTab": "새 탭에서 열기",
|
||||
"searchQueryPlaceholder": "검색어 입력...",
|
||||
"noSearchShortcuts": "바로가기가 구성되지 않음",
|
||||
"addSearchShortcut": "바로가기 추가",
|
||||
"fontSize": "글꼴 크기",
|
||||
"textAlign": "텍스트 정렬",
|
||||
"fontWeight": "글꼴 두께",
|
||||
"clearColor": "색상 지우기",
|
||||
"imageFit": "이미지 맞춤",
|
||||
"imageLinkUrl": "링크 URL",
|
||||
"noImage": "이미지 URL이 설정되지 않음",
|
||||
"altText": "대체 텍스트",
|
||||
"altTextPlaceholder": "이미지 설명 입력",
|
||||
"renderMarkdown": "마크다운 렌더링",
|
||||
"displayMode": "표시 모드",
|
||||
"displayField": "표시 필드",
|
||||
"jsonPath": "JSON 경로",
|
||||
"customApiLabel": "레이블",
|
||||
"customApiLabelPlaceholder": "예: 온도",
|
||||
"customApiUnit": "단위",
|
||||
"customApiNoUrl": "API URL이 구성되지 않음",
|
||||
"customApiError": "가져오기 실패",
|
||||
"customApiNotArray": "응답이 배열이 아닙니다",
|
||||
"addService": "서비스 추가",
|
||||
"showLabels": "레이블 표시",
|
||||
"iconSize": "아이콘 크기",
|
||||
"noDashboardLinks": "대시보드 링크가 구성되지 않음",
|
||||
"noLimit": "제한 없음",
|
||||
"sectionHeading": "섹션 제목",
|
||||
"addSection": "섹션 추가",
|
||||
"compactMode": "간결 모드",
|
||||
"selectHosts": "호스트 선택",
|
||||
"listLayout": "목록",
|
||||
"gridLayout": "그리드"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Startpagina"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Servicelinks",
|
||||
"homepagePreview": "Startpagina-voorbeeld"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Servicelinks",
|
||||
"serviceLinksEmpty": "Er zijn nog geen servicelinks beschikbaar.",
|
||||
"serviceLinksAdd": "Toevoegen",
|
||||
"serviceLinksLabelPlaceholder": "Mijn Service",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Voer een geldig webadres in.",
|
||||
"serviceLinksAddFailed": "Het toevoegen van de servicelink is mislukt."
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Startpagina",
|
||||
"addWidget": "Widget toevoegen",
|
||||
"editWidget": "Widget bewerken",
|
||||
"zoomIn": "Inzoomen",
|
||||
"zoomOut": "Uitzoomen",
|
||||
"resetView": "Weergave opnieuw instellen",
|
||||
"lockLayout": "Slotindeling",
|
||||
"unlockLayout": "Lay-out ontgrendelen",
|
||||
"noWidgets": "Klik met de rechtermuisknop of klik op + om je eerste widget toe te voegen.",
|
||||
"openFullView": "Volledige weergave openen",
|
||||
"previewTitle": "Startpagina-voorbeeld",
|
||||
"cancel": "Annuleren",
|
||||
"save": "Redden",
|
||||
"title_label": "Titel",
|
||||
"widgetTitlePlaceholder": "Widgettitel (optioneel)",
|
||||
"url": "URL",
|
||||
"imageUrl": "Aangepaste afbeeldings-URL",
|
||||
"imageUrlHint": "Laat dit veld leeg om automatisch het site-favicon te gebruiken.",
|
||||
"showImage": "Afbeelding weergeven",
|
||||
"description": "Beschrijving",
|
||||
"color": "Kleur",
|
||||
"timezone": "Tijdzone",
|
||||
"invalidTimezone": "Ongeldige tijdzone. Gebruik een IANA-naam zoals America/New_York.",
|
||||
"showSeconds": "Toon seconden",
|
||||
"format12h": "12 uur",
|
||||
"content": "Inhoud",
|
||||
"backgroundColor": "Achtergrondkleur",
|
||||
"links": "Links",
|
||||
"addLink": "Link toevoegen",
|
||||
"linkLabel": "Label",
|
||||
"categoryLinks": "Links",
|
||||
"categoryInfo": "Informatie",
|
||||
"categorySystem": "Systeem",
|
||||
"copyLink": "Link kopiëren",
|
||||
"linkCopied": "Link gekopieerd!",
|
||||
"location": "Locatie",
|
||||
"temperatureUnit": "Temperatuureenheid",
|
||||
"showForecast": "Toon de 3-daagse weersvoorspelling",
|
||||
"scrolling": "Scrollen toestaan",
|
||||
"feedUrl": "Feed-URL",
|
||||
"maxItems": "Max. aantal items",
|
||||
"showDescription": "Beschrijving weergeven",
|
||||
"widgetRssName": "RSS-feed",
|
||||
"noHostSelected": "Geen host geselecteerd",
|
||||
"displayedMetrics": "Weergegeven statistieken",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "Geheugen",
|
||||
"metricDisk": "Schijf",
|
||||
"metricUptime": "Beschikbaarheid",
|
||||
"metricSystem": "Systeem",
|
||||
"metricNetwork": "Netwerk",
|
||||
"metricProcesses": "Processen",
|
||||
"categoryMonitoring": "Monitoring",
|
||||
"noPingUrls": "Geen URL's geconfigureerd",
|
||||
"pingLabel": "Label",
|
||||
"addPingUrl": "URL toevoegen",
|
||||
"showLatency": "Toon latentie",
|
||||
"refreshInterval": "Vernieuwingsinterval",
|
||||
"seconds": "seconden",
|
||||
"filterActivityTypes": "Filtertypen",
|
||||
"filterTypesHint": "Laat dit veld leeg om alles weer te geven.",
|
||||
"showTimestamp": "Toon tijdstempel",
|
||||
"uptimeUnavailable": "Beschikbaarheid niet beschikbaar",
|
||||
"uptimeLabel": "Beschikbaarheid",
|
||||
"overviewVersion": "Versie",
|
||||
"overviewUpdateLabel": "Update",
|
||||
"overviewUpdateAvailable": "Update beschikbaar",
|
||||
"overviewDatabase": "Database",
|
||||
"overviewUptime": "Beschikbaarheid",
|
||||
"noHosts": "Geen hosts geconfigureerd",
|
||||
"hostGridHosts": "Gastheren",
|
||||
"hostGridAllHint": "Laat dit veld leeg om alle hosts weer te geven.",
|
||||
"columns": "Kolommen",
|
||||
"showIp": "IP-adres weergeven",
|
||||
"layout": "Indeling",
|
||||
"showStatus": "Status weergeven",
|
||||
"noActivity": "Geen activiteit",
|
||||
"widgetPingStatusName": "Pingstatus",
|
||||
"widgetRecentActivityName": "Recente activiteit",
|
||||
"sshTerminalNoHost": "Geen host geconfigureerd",
|
||||
"sshTerminalConnect": "Verbinden",
|
||||
"sshTerminalAutoConnect": "Automatisch aansluiten bij belasting",
|
||||
"connectionTypes": "Verbindingstypen",
|
||||
"calMon": "Mo",
|
||||
"calTue": "Tu",
|
||||
"calWed": "Wij",
|
||||
"calThu": "E",
|
||||
"calFri": "Fr",
|
||||
"calSat": "Sa",
|
||||
"calSun": "Zo",
|
||||
"startOnMonday": "De week begint op maandag.",
|
||||
"countdownNoDate": "Geen streefdatum vastgesteld",
|
||||
"countdownPast": "Het evenement heeft plaatsgevonden.",
|
||||
"countdownDays": "dagen",
|
||||
"countdownHours": "uren",
|
||||
"countdownMinutes": "min",
|
||||
"countdownSeconds": "sec",
|
||||
"countdownLabel": "Label",
|
||||
"countdownLabelPlaceholder": "bijvoorbeeld Lanceringsdag",
|
||||
"countdownShowDays": "Showdagen",
|
||||
"countdownShowHours": "Openingstijden",
|
||||
"targetDate": "Streefdatum",
|
||||
"searchEngine": "Zoekmachine",
|
||||
"customSearchUrl": "Aangepaste zoek-URL",
|
||||
"searchPlaceholder": "Zoekopdracht...",
|
||||
"searchGo": "Gaan",
|
||||
"searchPlaceholderLabel": "Plaatsvervangende tekst",
|
||||
"searchPlaceholderHint": "Tekst die wordt weergegeven in het zoekveld",
|
||||
"openInNewTab": "Openen in een nieuw tabblad",
|
||||
"searchQueryPlaceholder": "Voer uw zoekopdracht in...",
|
||||
"noSearchShortcuts": "Er zijn geen snelkoppelingen geconfigureerd.",
|
||||
"addSearchShortcut": "Snelkoppeling toevoegen",
|
||||
"fontSize": "Lettergrootte",
|
||||
"textAlign": "Tekst uitlijnen",
|
||||
"fontWeight": "Lettertypegewicht",
|
||||
"clearColor": "Transparante kleur",
|
||||
"imageFit": "Afbeelding passend maken",
|
||||
"imageLinkUrl": "Link-URL",
|
||||
"noImage": "Geen afbeeldings-URL ingesteld",
|
||||
"altText": "Alternatieve tekst",
|
||||
"altTextPlaceholder": "Beschrijf de afbeelding",
|
||||
"renderMarkdown": "Markdown weergeven",
|
||||
"displayMode": "Weergavemodus",
|
||||
"displayField": "Weergaveveld",
|
||||
"jsonPath": "JSON-pad",
|
||||
"customApiLabel": "Label",
|
||||
"customApiLabelPlaceholder": "bijv. Temperatuur",
|
||||
"customApiUnit": "Eenheid",
|
||||
"customApiNoUrl": "Geen API-URL geconfigureerd",
|
||||
"customApiError": "Het ophalen is mislukt.",
|
||||
"customApiNotArray": "Het antwoord is geen array.",
|
||||
"addService": "Voeg een service toe",
|
||||
"showLabels": "Labels weergeven",
|
||||
"iconSize": "Pictogramgrootte",
|
||||
"noDashboardLinks": "Er zijn geen dashboardlinks geconfigureerd.",
|
||||
"noLimit": "Geen limiet",
|
||||
"sectionHeading": "Sectiekop",
|
||||
"addSection": "Sectie toevoegen",
|
||||
"compactMode": "Compacte modus",
|
||||
"selectHosts": "Selecteer hosts",
|
||||
"listLayout": "Lijst",
|
||||
"gridLayout": "Rooster"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Hjemmeside"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Tjenestelenker",
|
||||
"homepagePreview": "Forhåndsvisning av hjemmeside"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Tjenestelenker",
|
||||
"serviceLinksEmpty": "Ingen tjenestelenker ennå",
|
||||
"serviceLinksAdd": "Legge til",
|
||||
"serviceLinksLabelPlaceholder": "Min tjeneste",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Skriv inn en gyldig nettadresse",
|
||||
"serviceLinksAddFailed": "Kunne ikke legge til tjenestelenke"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Hjemmeside",
|
||||
"addWidget": "Legg til widget",
|
||||
"editWidget": "Rediger widget",
|
||||
"zoomIn": "Zoom inn",
|
||||
"zoomOut": "Zoom ut",
|
||||
"resetView": "Tilbakestill visning",
|
||||
"lockLayout": "Låseoppsett",
|
||||
"unlockLayout": "Lås opp oppsett",
|
||||
"noWidgets": "Høyreklikk eller klikk på + for å legge til din første widget",
|
||||
"openFullView": "Åpne fullvisning",
|
||||
"previewTitle": "Forhåndsvisning av hjemmeside",
|
||||
"cancel": "Kansellere",
|
||||
"save": "Spare",
|
||||
"title_label": "Tittel",
|
||||
"widgetTitlePlaceholder": "Widgettittel (valgfritt)",
|
||||
"url": "URL-adresse",
|
||||
"imageUrl": "Tilpasset bilde-URL",
|
||||
"imageUrlHint": "La feltet stå tomt for å bruke nettstedets favicon automatisk",
|
||||
"showImage": "Vis bilde",
|
||||
"description": "Beskrivelse",
|
||||
"color": "Farge",
|
||||
"timezone": "Tidssone",
|
||||
"invalidTimezone": "Ugyldig tidssone. Bruk et IANA-navn, for eksempel America/New_York.",
|
||||
"showSeconds": "Vis sekunder",
|
||||
"format12h": "12-timers",
|
||||
"content": "Innhold",
|
||||
"backgroundColor": "Bakgrunnsfarge",
|
||||
"links": "Lenker",
|
||||
"addLink": "Legg til lenke",
|
||||
"linkLabel": "Merkelapp",
|
||||
"categoryLinks": "Lenker",
|
||||
"categoryInfo": "Info",
|
||||
"categorySystem": "System",
|
||||
"copyLink": "Kopier lenke",
|
||||
"linkCopied": "Lenken er kopiert!",
|
||||
"location": "Sted",
|
||||
"temperatureUnit": "Temperaturenhet",
|
||||
"showForecast": "Vis 3-dagers værmelding",
|
||||
"scrolling": "Tillat rulling",
|
||||
"feedUrl": "Nettadresse for feeden",
|
||||
"maxItems": "Maksimalt antall varer",
|
||||
"showDescription": "Vis beskrivelse",
|
||||
"widgetRssName": "RSS-feed",
|
||||
"noHostSelected": "Ingen vert valgt",
|
||||
"displayedMetrics": "Viste målinger",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "Hukommelse",
|
||||
"metricDisk": "Disk",
|
||||
"metricUptime": "Oppetid",
|
||||
"metricSystem": "System",
|
||||
"metricNetwork": "Nettverk",
|
||||
"metricProcesses": "Prosesser",
|
||||
"categoryMonitoring": "Overvåking",
|
||||
"noPingUrls": "Ingen nettadresser konfigurert",
|
||||
"pingLabel": "Merkelapp",
|
||||
"addPingUrl": "Legg til nettadresse",
|
||||
"showLatency": "Vis latens",
|
||||
"refreshInterval": "Oppdateringsintervall",
|
||||
"seconds": "sekunder",
|
||||
"filterActivityTypes": "Filtertyper",
|
||||
"filterTypesHint": "La stå tomt for å vise alle",
|
||||
"showTimestamp": "Vis tidsstempel",
|
||||
"uptimeUnavailable": "Oppetid utilgjengelig",
|
||||
"uptimeLabel": "Oppetid",
|
||||
"overviewVersion": "Versjon",
|
||||
"overviewUpdateLabel": "Oppdatering",
|
||||
"overviewUpdateAvailable": "Oppdatering tilgjengelig",
|
||||
"overviewDatabase": "Database",
|
||||
"overviewUptime": "Oppetid",
|
||||
"noHosts": "Ingen verter konfigurert",
|
||||
"hostGridHosts": "Verter",
|
||||
"hostGridAllHint": "La stå tomt for å vise alle verter",
|
||||
"columns": "Kolonner",
|
||||
"showIp": "Vis IP-adresse",
|
||||
"layout": "Oppsett",
|
||||
"showStatus": "Vis status",
|
||||
"noActivity": "Ingen aktivitet",
|
||||
"widgetPingStatusName": "Ping-status",
|
||||
"widgetRecentActivityName": "Nylig aktivitet",
|
||||
"sshTerminalNoHost": "Ingen vert konfigurert",
|
||||
"sshTerminalConnect": "Koble til",
|
||||
"sshTerminalAutoConnect": "Automatisk tilkobling ved lasting",
|
||||
"connectionTypes": "Tilkoblingstyper",
|
||||
"calMon": "Mo",
|
||||
"calTue": "Ti",
|
||||
"calWed": "Vi",
|
||||
"calThu": "To",
|
||||
"calFri": "Fr",
|
||||
"calSat": "Lø",
|
||||
"calSun": "Søn",
|
||||
"startOnMonday": "Start uken på mandag",
|
||||
"countdownNoDate": "Ingen måldato angitt",
|
||||
"countdownPast": "Arrangementet har gått",
|
||||
"countdownDays": "dager",
|
||||
"countdownHours": "timer",
|
||||
"countdownMinutes": "min",
|
||||
"countdownSeconds": "sek",
|
||||
"countdownLabel": "Merkelapp",
|
||||
"countdownLabelPlaceholder": "f.eks. lanseringsdag",
|
||||
"countdownShowDays": "Vis dager",
|
||||
"countdownShowHours": "Åpningstider",
|
||||
"targetDate": "Måldato",
|
||||
"searchEngine": "Søkemotor",
|
||||
"customSearchUrl": "Tilpasset søke-URL",
|
||||
"searchPlaceholder": "Søk...",
|
||||
"searchGo": "Gå",
|
||||
"searchPlaceholderLabel": "Plassholdertekst",
|
||||
"searchPlaceholderHint": "Tekst vist i søkefeltet",
|
||||
"openInNewTab": "Åpne i ny fane",
|
||||
"searchQueryPlaceholder": "Skriv inn spørring...",
|
||||
"noSearchShortcuts": "Ingen snarveier konfigurert",
|
||||
"addSearchShortcut": "Legg til snarvei",
|
||||
"fontSize": "Skriftstørrelse",
|
||||
"textAlign": "Tekstjustering",
|
||||
"fontWeight": "Skriftvekt",
|
||||
"clearColor": "Klar farge",
|
||||
"imageFit": "Bildetilpasning",
|
||||
"imageLinkUrl": "Lenke-URL",
|
||||
"noImage": "Ingen bilde-URL angitt",
|
||||
"altText": "Alternativ tekst",
|
||||
"altTextPlaceholder": "Beskriv bildet",
|
||||
"renderMarkdown": "Gjengivelsesmarkering",
|
||||
"displayMode": "Visningsmodus",
|
||||
"displayField": "Visningsfelt",
|
||||
"jsonPath": "JSON-sti",
|
||||
"customApiLabel": "Merkelapp",
|
||||
"customApiLabelPlaceholder": "f.eks. Temperatur",
|
||||
"customApiUnit": "Enhet",
|
||||
"customApiNoUrl": "Ingen API-URL konfigurert",
|
||||
"customApiError": "Kunne ikke hente",
|
||||
"customApiNotArray": "Responsen er ikke en matrise",
|
||||
"addService": "Legg til tjeneste",
|
||||
"showLabels": "Vis etiketter",
|
||||
"iconSize": "Ikonstørrelse",
|
||||
"noDashboardLinks": "Ingen dashbordlenker konfigurert",
|
||||
"noLimit": "Ingen grense",
|
||||
"sectionHeading": "Seksjonsoverskrift",
|
||||
"addSection": "Legg til seksjon",
|
||||
"compactMode": "Kompakt modus",
|
||||
"selectHosts": "Velg verter",
|
||||
"listLayout": "Liste",
|
||||
"gridLayout": "Rutenett"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Strona główna"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Linki do usług",
|
||||
"homepagePreview": "Podgląd strony głównej"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Linki do usług",
|
||||
"serviceLinksEmpty": "Brak linków do usług",
|
||||
"serviceLinksAdd": "Dodaj",
|
||||
"serviceLinksLabelPlaceholder": "Moja usługa",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Wprowadź prawidłowy adres internetowy",
|
||||
"serviceLinksAddFailed": "Nie udało się dodać linku do usługi"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Strona główna",
|
||||
"addWidget": "Dodaj widżet",
|
||||
"editWidget": "Edytuj widżet",
|
||||
"zoomIn": "Powiększ",
|
||||
"zoomOut": "Oddal",
|
||||
"resetView": "Resetuj widok",
|
||||
"lockLayout": "Zablokuj układ",
|
||||
"unlockLayout": "Odblokuj układ",
|
||||
"noWidgets": "Kliknij prawym przyciskiem myszy lub kliknij +, aby dodać pierwszy widżet",
|
||||
"openFullView": "Otwórz pełny widok",
|
||||
"previewTitle": "Podgląd strony głównej",
|
||||
"cancel": "Anuluj",
|
||||
"save": "Zapisz",
|
||||
"title_label": "Tytuł",
|
||||
"widgetTitlePlaceholder": "Tytuł widżetu (opcjonalnie)",
|
||||
"url": "Adres URL",
|
||||
"imageUrl": "Niestandardowy adres URL obrazu",
|
||||
"imageUrlHint": "Pozostaw puste, aby automatycznie użyć ikony witryny",
|
||||
"showImage": "Pokaż obraz",
|
||||
"description": "Opis",
|
||||
"color": "Kolor",
|
||||
"timezone": "Strefa czasowa",
|
||||
"invalidTimezone": "Nieprawidłowa strefa czasowa. Użyj nazwy IANA, takiej jak America/New_York",
|
||||
"showSeconds": "Pokaż sekundy",
|
||||
"format12h": "12-godzinny",
|
||||
"content": "Treść",
|
||||
"backgroundColor": "Kolor tła",
|
||||
"links": "Linki",
|
||||
"addLink": "Dodaj link",
|
||||
"linkLabel": "Etykieta",
|
||||
"categoryLinks": "Linki",
|
||||
"categoryInfo": "Informacje",
|
||||
"categorySystem": "System",
|
||||
"copyLink": "Kopiuj link",
|
||||
"linkCopied": "Link skopiowany!",
|
||||
"location": "Lokalizacja",
|
||||
"temperatureUnit": "Jednostka temperatury",
|
||||
"showForecast": "Pokaż prognozę 3-dniową",
|
||||
"scrolling": "Zezwól na przewijanie",
|
||||
"feedUrl": "Adres URL kanału",
|
||||
"maxItems": "Maksymalna liczba elementów",
|
||||
"showDescription": "Pokaż opis",
|
||||
"widgetRssName": "Kanał RSS",
|
||||
"noHostSelected": "Nie wybrano hosta",
|
||||
"displayedMetrics": "Wyświetlane metryki",
|
||||
"metricCpu": "Procesor",
|
||||
"metricMemory": "Pamięć",
|
||||
"metricDisk": "Dysk",
|
||||
"metricUptime": "Czas działania",
|
||||
"metricSystem": "System",
|
||||
"metricNetwork": "Sieć",
|
||||
"metricProcesses": "Procesy",
|
||||
"categoryMonitoring": "Monitorowanie",
|
||||
"noPingUrls": "Brak skonfigurowanych adresów URL",
|
||||
"pingLabel": "Etykieta",
|
||||
"addPingUrl": "Dodaj adres URL",
|
||||
"showLatency": "Pokaż opóźnienie",
|
||||
"refreshInterval": "Interwał odświeżania",
|
||||
"seconds": "sekundy",
|
||||
"filterActivityTypes": "Filtruj typy",
|
||||
"filterTypesHint": "Pozostaw puste, aby wyświetlić wszystkie",
|
||||
"showTimestamp": "Pokaż znacznik czasu",
|
||||
"uptimeUnavailable": "Czas działania niedostępny",
|
||||
"uptimeLabel": "Czas działania",
|
||||
"overviewVersion": "Wersja",
|
||||
"overviewUpdateLabel": "Aktualizacja",
|
||||
"overviewUpdateAvailable": "Dostępna aktualizacja",
|
||||
"overviewDatabase": "Baza danych",
|
||||
"overviewUptime": "Czas działania",
|
||||
"noHosts": "Brak skonfigurowanych hostów",
|
||||
"hostGridHosts": "Hosty",
|
||||
"hostGridAllHint": "Pozostaw puste, aby wyświetlić wszystkie hosty",
|
||||
"columns": "Kolumny",
|
||||
"showIp": "Pokaż adres IP",
|
||||
"layout": "Układ",
|
||||
"showStatus": "Pokaż status",
|
||||
"noActivity": "Brak aktywności",
|
||||
"widgetPingStatusName": "Status pingu",
|
||||
"widgetRecentActivityName": "Ostatnia aktywność",
|
||||
"sshTerminalNoHost": "Brak skonfigurowanego hosta",
|
||||
"sshTerminalConnect": "Połącz",
|
||||
"sshTerminalAutoConnect": "Automatyczne łączenie po załadowaniu",
|
||||
"connectionTypes": "Typy połączeń",
|
||||
"calMon": "Pn",
|
||||
"calTue": "Wt",
|
||||
"calWed": "Śr",
|
||||
"calThu": "Cz",
|
||||
"calFri": "Pt",
|
||||
"calSat": "So",
|
||||
"calSun": "Nd",
|
||||
"startOnMonday": "Zacznij tydzień w poniedziałek",
|
||||
"countdownNoDate": "Nie ustalono daty docelowej",
|
||||
"countdownPast": "Wydarzenie minęło",
|
||||
"countdownDays": "dni",
|
||||
"countdownHours": "godz.",
|
||||
"countdownMinutes": "min",
|
||||
"countdownSeconds": "sek",
|
||||
"countdownLabel": "Etykieta",
|
||||
"countdownLabelPlaceholder": "np. Dzień premiery",
|
||||
"countdownShowDays": "Pokaż dni",
|
||||
"countdownShowHours": "Pokaż godziny",
|
||||
"targetDate": "Data docelowa",
|
||||
"searchEngine": "Wyszukiwarka",
|
||||
"customSearchUrl": "Niestandardowy adres URL wyszukiwania",
|
||||
"searchPlaceholder": "Szukaj...",
|
||||
"searchGo": "Szukaj",
|
||||
"searchPlaceholderLabel": "Tekst zastępczy",
|
||||
"searchPlaceholderHint": "Tekst wyświetlany w polu wyszukiwania",
|
||||
"openInNewTab": "Otwórz w nowej karcie",
|
||||
"searchQueryPlaceholder": "Wprowadź zapytanie...",
|
||||
"noSearchShortcuts": "Brak skonfigurowanych skrótów",
|
||||
"addSearchShortcut": "Dodaj skrót",
|
||||
"fontSize": "Rozmiar czcionki",
|
||||
"textAlign": "Wyrównanie tekstu",
|
||||
"fontWeight": "Grubość czcionki",
|
||||
"clearColor": "Wyczyść kolor",
|
||||
"imageFit": "Dopasowanie obrazu",
|
||||
"imageLinkUrl": "Adres URL łącza",
|
||||
"noImage": "Nie ustawiono adresu URL obrazu",
|
||||
"altText": "Tekst alternatywny",
|
||||
"altTextPlaceholder": "Opisz obraz",
|
||||
"renderMarkdown": "Renderuj Markdown",
|
||||
"displayMode": "Tryb wyświetlania",
|
||||
"displayField": "Pole wyświetlania",
|
||||
"jsonPath": "Ścieżka JSON",
|
||||
"customApiLabel": "Etykieta",
|
||||
"customApiLabelPlaceholder": "np. Temperatura",
|
||||
"customApiUnit": "Jednostka",
|
||||
"customApiNoUrl": "Brak skonfigurowanego adresu URL API",
|
||||
"customApiError": "Nie udało się pobrać",
|
||||
"customApiNotArray": "Odpowiedź nie jest tablicą",
|
||||
"addService": "Dodaj usługę",
|
||||
"showLabels": "Pokaż etykiety",
|
||||
"iconSize": "Rozmiar ikony",
|
||||
"noDashboardLinks": "Brak skonfigurowanych linków pulpitu",
|
||||
"noLimit": "Bez limitu",
|
||||
"sectionHeading": "Nagłówek sekcji",
|
||||
"addSection": "Dodaj sekcję",
|
||||
"compactMode": "Tryb kompaktowy",
|
||||
"selectHosts": "Wybierz hosty",
|
||||
"listLayout": "Lista",
|
||||
"gridLayout": "Siatka"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Página inicial"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Links de serviço",
|
||||
"homepagePreview": "Pré-visualização da página inicial"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Links de serviço",
|
||||
"serviceLinksEmpty": "Ainda não há links de serviço.",
|
||||
"serviceLinksAdd": "Adicionar",
|
||||
"serviceLinksLabelPlaceholder": "Meu Serviço",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Insira um endereço web válido.",
|
||||
"serviceLinksAddFailed": "Falha ao adicionar o link de serviço"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Página inicial",
|
||||
"addWidget": "Adicionar widget",
|
||||
"editWidget": "Widget de edição",
|
||||
"zoomIn": "Ampliar",
|
||||
"zoomOut": "Diminuir o zoom",
|
||||
"resetView": "Redefinir visualização",
|
||||
"lockLayout": "Configuração de fechadura",
|
||||
"unlockLayout": "Desbloquear layout",
|
||||
"noWidgets": "Clique com o botão direito do mouse ou clique em + para adicionar seu primeiro widget.",
|
||||
"openFullView": "Abrir visualização completa",
|
||||
"previewTitle": "Pré-visualização da página inicial",
|
||||
"cancel": "Cancelar",
|
||||
"save": "Salvar",
|
||||
"title_label": "Título",
|
||||
"widgetTitlePlaceholder": "Título do widget (opcional)",
|
||||
"url": "URL",
|
||||
"imageUrl": "URL da imagem personalizada",
|
||||
"imageUrlHint": "Deixe em branco para usar o favicon do site automaticamente.",
|
||||
"showImage": "Mostrar imagem",
|
||||
"description": "Descrição",
|
||||
"color": "Cor",
|
||||
"timezone": "Fuso horário",
|
||||
"invalidTimezone": "Fuso horário inválido. Use um nome IANA, como America/New_York.",
|
||||
"showSeconds": "Mostrar segundos",
|
||||
"format12h": "12 horas",
|
||||
"content": "Contente",
|
||||
"backgroundColor": "Cor de fundo",
|
||||
"links": "Links",
|
||||
"addLink": "Adicionar link",
|
||||
"linkLabel": "Rótulo",
|
||||
"categoryLinks": "Links",
|
||||
"categoryInfo": "Informações",
|
||||
"categorySystem": "Sistema",
|
||||
"copyLink": "Copiar link",
|
||||
"linkCopied": "Link copiado!",
|
||||
"location": "Localização",
|
||||
"temperatureUnit": "Unidade de temperatura",
|
||||
"showForecast": "Mostrar previsão para 3 dias",
|
||||
"scrolling": "Permitir rolagem",
|
||||
"feedUrl": "URL do feed",
|
||||
"maxItems": "Número máximo de itens",
|
||||
"showDescription": "Descrição do programa",
|
||||
"widgetRssName": "Feed RSS",
|
||||
"noHostSelected": "Nenhum host selecionado",
|
||||
"displayedMetrics": "Métricas exibidas",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "Memória",
|
||||
"metricDisk": "Disco",
|
||||
"metricUptime": "Tempo de atividade",
|
||||
"metricSystem": "Sistema",
|
||||
"metricNetwork": "Rede",
|
||||
"metricProcesses": "Processos",
|
||||
"categoryMonitoring": "Monitoramento",
|
||||
"noPingUrls": "Nenhuma URL configurada",
|
||||
"pingLabel": "Rótulo",
|
||||
"addPingUrl": "Adicionar URL",
|
||||
"showLatency": "Mostrar latência",
|
||||
"refreshInterval": "Intervalo de atualização",
|
||||
"seconds": "segundos",
|
||||
"filterActivityTypes": "Tipos de filtro",
|
||||
"filterTypesHint": "Deixe em branco para mostrar tudo",
|
||||
"showTimestamp": "Mostrar carimbo de data/hora",
|
||||
"uptimeUnavailable": "Tempo de atividade indisponível",
|
||||
"uptimeLabel": "Tempo de atividade",
|
||||
"overviewVersion": "Versão",
|
||||
"overviewUpdateLabel": "Atualizar",
|
||||
"overviewUpdateAvailable": "Atualização disponível",
|
||||
"overviewDatabase": "Banco de dados",
|
||||
"overviewUptime": "Tempo de atividade",
|
||||
"noHosts": "Nenhum host configurado",
|
||||
"hostGridHosts": "Anfitriões",
|
||||
"hostGridAllHint": "Deixe em branco para mostrar todos os hosts.",
|
||||
"columns": "Colunas",
|
||||
"showIp": "Mostrar endereço IP",
|
||||
"layout": "Layout",
|
||||
"showStatus": "Mostrar status",
|
||||
"noActivity": "Nenhuma atividade",
|
||||
"widgetPingStatusName": "Status do ping",
|
||||
"widgetRecentActivityName": "Atividade recente",
|
||||
"sshTerminalNoHost": "Nenhum host configurado",
|
||||
"sshTerminalConnect": "Conectar",
|
||||
"sshTerminalAutoConnect": "Conexão automática sob carga",
|
||||
"connectionTypes": "Tipos de conexão",
|
||||
"calMon": "Mo",
|
||||
"calTue": "Tu",
|
||||
"calWed": "Nós",
|
||||
"calThu": "O",
|
||||
"calFri": "Padre",
|
||||
"calSat": "Sa",
|
||||
"calSun": "Su",
|
||||
"startOnMonday": "Comece a semana na segunda-feira",
|
||||
"countdownNoDate": "Nenhuma data-alvo definida.",
|
||||
"countdownPast": "O evento já aconteceu.",
|
||||
"countdownDays": "dias",
|
||||
"countdownHours": "horas",
|
||||
"countdownMinutes": "min",
|
||||
"countdownSeconds": "seg",
|
||||
"countdownLabel": "Rótulo",
|
||||
"countdownLabelPlaceholder": "Exemplo: Dia do Lançamento",
|
||||
"countdownShowDays": "Dias de exibição",
|
||||
"countdownShowHours": "Horário de funcionamento",
|
||||
"targetDate": "Data-alvo",
|
||||
"searchEngine": "Mecanismo de busca",
|
||||
"customSearchUrl": "URL de pesquisa personalizada",
|
||||
"searchPlaceholder": "Procurar...",
|
||||
"searchGo": "Ir",
|
||||
"searchPlaceholderLabel": "Texto de espaço reservado",
|
||||
"searchPlaceholderHint": "Texto exibido dentro do campo de pesquisa",
|
||||
"openInNewTab": "Abrir em nova aba",
|
||||
"searchQueryPlaceholder": "Digite a consulta...",
|
||||
"noSearchShortcuts": "Nenhum atalho configurado",
|
||||
"addSearchShortcut": "Adicionar atalho",
|
||||
"fontSize": "Tamanho da fonte",
|
||||
"textAlign": "Alinhamento de texto",
|
||||
"fontWeight": "Peso da fonte",
|
||||
"clearColor": "Cor transparente",
|
||||
"imageFit": "Ajuste de imagem",
|
||||
"imageLinkUrl": "URL do link",
|
||||
"noImage": "Nenhuma URL de imagem definida.",
|
||||
"altText": "Texto alternativo",
|
||||
"altTextPlaceholder": "Descreva a imagem",
|
||||
"renderMarkdown": "Renderizar Markdown",
|
||||
"displayMode": "Modo de exibição",
|
||||
"displayField": "Campo de exibição",
|
||||
"jsonPath": "Caminho JSON",
|
||||
"customApiLabel": "Rótulo",
|
||||
"customApiLabelPlaceholder": "Exemplo: Temperatura",
|
||||
"customApiUnit": "Unidade",
|
||||
"customApiNoUrl": "Nenhuma URL de API configurada",
|
||||
"customApiError": "Falha ao buscar",
|
||||
"customApiNotArray": "A resposta não é uma matriz.",
|
||||
"addService": "Adicionar serviço",
|
||||
"showLabels": "Mostrar rótulos",
|
||||
"iconSize": "Tamanho do ícone",
|
||||
"noDashboardLinks": "Nenhum link para o painel de controle configurado",
|
||||
"noLimit": "Sem limite",
|
||||
"sectionHeading": "Título da seção",
|
||||
"addSection": "Adicionar seção",
|
||||
"compactMode": "Modo compacto",
|
||||
"selectHosts": "Selecionar anfitriões",
|
||||
"listLayout": "Lista",
|
||||
"gridLayout": "Grade"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Página inicial"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Links de Serviço",
|
||||
"homepagePreview": "Pré-visualização da Página Inicial"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Links de Serviço",
|
||||
"serviceLinksEmpty": "Ainda sem links de serviço",
|
||||
"serviceLinksAdd": "Adicionar",
|
||||
"serviceLinksLabelPlaceholder": "O Meu Serviço",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Insira um endereço web válido",
|
||||
"serviceLinksAddFailed": "Falha ao adicionar link de serviço"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Página Inicial",
|
||||
"addWidget": "Adicionar widget",
|
||||
"editWidget": "Editar widget",
|
||||
"zoomIn": "Ampliar",
|
||||
"zoomOut": "Reduzir",
|
||||
"resetView": "Repor Vista",
|
||||
"lockLayout": "Bloquear Disposição",
|
||||
"unlockLayout": "Desbloquear Disposição",
|
||||
"noWidgets": "Clique com o botão direito ou em + para adicionar o seu primeiro widget",
|
||||
"openFullView": "Abrir Vista Completa",
|
||||
"previewTitle": "Pré-visualização da Página Inicial",
|
||||
"cancel": "Cancelar",
|
||||
"save": "Guardar",
|
||||
"title_label": "Título",
|
||||
"widgetTitlePlaceholder": "Título do widget (opcional)",
|
||||
"url": "URL",
|
||||
"imageUrl": "URL de Imagem Personalizada",
|
||||
"imageUrlHint": "Deixe em branco para usar automaticamente o favicon do site",
|
||||
"showImage": "Mostrar Imagem",
|
||||
"description": "Descrição",
|
||||
"color": "Cor",
|
||||
"timezone": "Fuso Horário",
|
||||
"invalidTimezone": "Fuso horário inválido. Utilize um nome IANA, como America/New_York.",
|
||||
"showSeconds": "Mostrar segundos",
|
||||
"format12h": "12 horas",
|
||||
"content": "Conteúdo",
|
||||
"backgroundColor": "Cor de Fundo",
|
||||
"links": "Hiperligações",
|
||||
"addLink": "Adicionar ligação",
|
||||
"linkLabel": "Etiqueta",
|
||||
"categoryLinks": "Hiperligações",
|
||||
"categoryInfo": "Info",
|
||||
"categorySystem": "Sistema",
|
||||
"copyLink": "Copiar ligação",
|
||||
"linkCopied": "Ligação copiada!",
|
||||
"location": "Localização",
|
||||
"temperatureUnit": "Unidade de temperatura",
|
||||
"showForecast": "Mostrar previsão de 3 dias",
|
||||
"scrolling": "Permitir deslocamento",
|
||||
"feedUrl": "URL do feed",
|
||||
"maxItems": "Itens máx.",
|
||||
"showDescription": "Mostrar descrição",
|
||||
"widgetRssName": "Feed RSS",
|
||||
"noHostSelected": "Nenhum anfitrião selecionado",
|
||||
"displayedMetrics": "Métricas Exibidas",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "Memória",
|
||||
"metricDisk": "Disco",
|
||||
"metricUptime": "Tempo de atividade",
|
||||
"metricSystem": "Sistema",
|
||||
"metricNetwork": "Rede",
|
||||
"metricProcesses": "Processos",
|
||||
"categoryMonitoring": "Monitorização",
|
||||
"noPingUrls": "Nenhum URL configurado",
|
||||
"pingLabel": "Etiqueta",
|
||||
"addPingUrl": "Adicionar URL",
|
||||
"showLatency": "Mostrar latência",
|
||||
"refreshInterval": "Intervalo de atualização",
|
||||
"seconds": "segundos",
|
||||
"filterActivityTypes": "Filtrar tipos",
|
||||
"filterTypesHint": "Deixar vazio para mostrar todos",
|
||||
"showTimestamp": "Mostrar timestamp",
|
||||
"uptimeUnavailable": "Tempo de atividade indisponível",
|
||||
"uptimeLabel": "Tempo de atividade",
|
||||
"overviewVersion": "Versão",
|
||||
"overviewUpdateLabel": "Atualização",
|
||||
"overviewUpdateAvailable": "Atualização disponível",
|
||||
"overviewDatabase": "Base de dados",
|
||||
"overviewUptime": "Tempo de atividade",
|
||||
"noHosts": "Sem hosts configurados",
|
||||
"hostGridHosts": "Hosts",
|
||||
"hostGridAllHint": "Deixar em branco para mostrar todos os hosts",
|
||||
"columns": "Colunas",
|
||||
"showIp": "Mostrar endereço IP",
|
||||
"layout": "Layout",
|
||||
"showStatus": "Mostrar estado",
|
||||
"noActivity": "Sem atividade",
|
||||
"widgetPingStatusName": "Estado do ping",
|
||||
"widgetRecentActivityName": "Atividade recente",
|
||||
"sshTerminalNoHost": "Nenhum host configurado",
|
||||
"sshTerminalConnect": "Ligar",
|
||||
"sshTerminalAutoConnect": "Ligação automática ao carregar",
|
||||
"connectionTypes": "Tipos de Ligação",
|
||||
"calMon": "Seg",
|
||||
"calTue": "Ter",
|
||||
"calWed": "Qua",
|
||||
"calThu": "Qui",
|
||||
"calFri": "Sex",
|
||||
"calSat": "Sáb",
|
||||
"calSun": "Dom",
|
||||
"startOnMonday": "Começar a semana na segunda-feira",
|
||||
"countdownNoDate": "Nenhuma data alvo definida",
|
||||
"countdownPast": "O evento já passou",
|
||||
"countdownDays": "dias",
|
||||
"countdownHours": "horas",
|
||||
"countdownMinutes": "min",
|
||||
"countdownSeconds": "seg",
|
||||
"countdownLabel": "Rótulo",
|
||||
"countdownLabelPlaceholder": "p. ex. Dia de Lançamento",
|
||||
"countdownShowDays": "Mostrar Dias",
|
||||
"countdownShowHours": "Mostrar Horas",
|
||||
"targetDate": "Data Alvo",
|
||||
"searchEngine": "Motor de Pesquisa",
|
||||
"customSearchUrl": "URL de Pesquisa Personalizada",
|
||||
"searchPlaceholder": "Pesquisar...",
|
||||
"searchGo": "Ir",
|
||||
"searchPlaceholderLabel": "Texto do Placeholder",
|
||||
"searchPlaceholderHint": "Texto apresentado no campo de pesquisa",
|
||||
"openInNewTab": "Abrir num Novo Separador",
|
||||
"searchQueryPlaceholder": "Introduzir consulta...",
|
||||
"noSearchShortcuts": "Nenhum atalho configurado",
|
||||
"addSearchShortcut": "Adicionar Atalho",
|
||||
"fontSize": "Tamanho da Letra",
|
||||
"textAlign": "Alinhamento do Texto",
|
||||
"fontWeight": "Peso da Fonte",
|
||||
"clearColor": "Limpar Cor",
|
||||
"imageFit": "Ajuste da Imagem",
|
||||
"imageLinkUrl": "URL do Link",
|
||||
"noImage": "Nenhum URL de imagem definido",
|
||||
"altText": "Texto Alternativo",
|
||||
"altTextPlaceholder": "Descreva a imagem",
|
||||
"renderMarkdown": "Renderizar Markdown",
|
||||
"displayMode": "Modo de Exibição",
|
||||
"displayField": "Campo de Exibição",
|
||||
"jsonPath": "Caminho JSON",
|
||||
"customApiLabel": "Etiqueta",
|
||||
"customApiLabelPlaceholder": "p. ex. Temperatura",
|
||||
"customApiUnit": "Unidade",
|
||||
"customApiNoUrl": "Nenhum URL de API configurado",
|
||||
"customApiError": "Falha ao obter",
|
||||
"customApiNotArray": "A resposta não é um array",
|
||||
"addService": "Adicionar Serviço",
|
||||
"showLabels": "Mostrar Etiquetas",
|
||||
"iconSize": "Tamanho do Ícone",
|
||||
"noDashboardLinks": "Nenhuma ligação do painel configurada",
|
||||
"noLimit": "Sem limite",
|
||||
"sectionHeading": "Cabeçalho da Secção",
|
||||
"addSection": "Adicionar Secção",
|
||||
"compactMode": "Modo Compacto",
|
||||
"selectHosts": "Selecionar Hosts",
|
||||
"listLayout": "Lista",
|
||||
"gridLayout": "Grelha"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Pagina principală"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Linkuri de servicii",
|
||||
"homepagePreview": "Previzualizare pagină principală"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Linkuri de servicii",
|
||||
"serviceLinksEmpty": "Încă nu există linkuri de servicii",
|
||||
"serviceLinksAdd": "Adăuga",
|
||||
"serviceLinksLabelPlaceholder": "Serviciul meu",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Introduceți o adresă web validă",
|
||||
"serviceLinksAddFailed": "Nu s-a putut adăuga linkul de serviciu"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Pagina principală",
|
||||
"addWidget": "Adăugați widget",
|
||||
"editWidget": "Editare widget",
|
||||
"zoomIn": "Mărire",
|
||||
"zoomOut": "Micșorare",
|
||||
"resetView": "Resetare vizualizare",
|
||||
"lockLayout": "Aspectul blocării",
|
||||
"unlockLayout": "Deblocare aspect",
|
||||
"noWidgets": "Faceți clic dreapta sau faceți clic pe + pentru a adăuga primul widget",
|
||||
"openFullView": "Deschideți Vizualizarea Completă",
|
||||
"previewTitle": "Previzualizare pagină principală",
|
||||
"cancel": "Anula",
|
||||
"save": "Salva",
|
||||
"title_label": "Titlu",
|
||||
"widgetTitlePlaceholder": "Titlul widgetului (opțional)",
|
||||
"url": "URL-ul",
|
||||
"imageUrl": "URL-ul imaginii personalizate",
|
||||
"imageUrlHint": "Lăsați necompletat pentru a utiliza automat favicon-ul site-ului",
|
||||
"showImage": "Afișați imaginea",
|
||||
"description": "Descriere",
|
||||
"color": "Culoare",
|
||||
"timezone": "Fus orar",
|
||||
"invalidTimezone": "Fus orar nevalid. Folosiți un nume IANA, cum ar fi America/New_York.",
|
||||
"showSeconds": "Afișează secundele",
|
||||
"format12h": "12 ore",
|
||||
"content": "Conţinut",
|
||||
"backgroundColor": "Culoare de fundal",
|
||||
"links": "Linkuri",
|
||||
"addLink": "Adăugați link",
|
||||
"linkLabel": "Eticheta",
|
||||
"categoryLinks": "Linkuri",
|
||||
"categoryInfo": "Informații",
|
||||
"categorySystem": "Sistem",
|
||||
"copyLink": "Copiază linkul",
|
||||
"linkCopied": "Link copiat!",
|
||||
"location": "Locaţie",
|
||||
"temperatureUnit": "Unitate de temperatură",
|
||||
"showForecast": "Afișați prognoza meteo pe 3 zile",
|
||||
"scrolling": "Permiteți derularea",
|
||||
"feedUrl": "URL-ul fluxului",
|
||||
"maxItems": "Articole maxime",
|
||||
"showDescription": "Afișați descrierea",
|
||||
"widgetRssName": "Flux RSS",
|
||||
"noHostSelected": "Nicio gazdă selectată",
|
||||
"displayedMetrics": "Metrici afișate",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "Memorie",
|
||||
"metricDisk": "Disc",
|
||||
"metricUptime": "Timp de funcționare",
|
||||
"metricSystem": "Sistem",
|
||||
"metricNetwork": "Reţea",
|
||||
"metricProcesses": "Procese",
|
||||
"categoryMonitoring": "Monitorizare",
|
||||
"noPingUrls": "Nicio adresă URL configurată",
|
||||
"pingLabel": "Eticheta",
|
||||
"addPingUrl": "Adăugați adresa URL",
|
||||
"showLatency": "Afișați latența",
|
||||
"refreshInterval": "Interval de actualizare",
|
||||
"seconds": "secunde",
|
||||
"filterActivityTypes": "Tipuri de filtre",
|
||||
"filterTypesHint": "Lăsați gol pentru a afișa toate",
|
||||
"showTimestamp": "Afișați marcajul temporal",
|
||||
"uptimeUnavailable": "Disponibilitate indisponibilă",
|
||||
"uptimeLabel": "Timp de funcționare",
|
||||
"overviewVersion": "Versiune",
|
||||
"overviewUpdateLabel": "Actualizare",
|
||||
"overviewUpdateAvailable": "Actualizare disponibilă",
|
||||
"overviewDatabase": "Bază de date",
|
||||
"overviewUptime": "Timp de funcționare",
|
||||
"noHosts": "Nicio gazdă configurată",
|
||||
"hostGridHosts": "Gazde",
|
||||
"hostGridAllHint": "Lăsați gol pentru a afișa toate gazdele",
|
||||
"columns": "Coloane",
|
||||
"showIp": "Afișați adresa IP",
|
||||
"layout": "Aspect",
|
||||
"showStatus": "Afișați starea",
|
||||
"noActivity": "Nicio activitate",
|
||||
"widgetPingStatusName": "Starea ping-ului",
|
||||
"widgetRecentActivityName": "Activitate recentă",
|
||||
"sshTerminalNoHost": "Nicio gazdă configurată",
|
||||
"sshTerminalConnect": "Conectează-te",
|
||||
"sshTerminalAutoConnect": "Conectare automată la sarcină",
|
||||
"connectionTypes": "Tipuri de conexiune",
|
||||
"calMon": "Luni",
|
||||
"calTue": "Marți",
|
||||
"calWed": "Noi",
|
||||
"calThu": "Joi",
|
||||
"calFri": "Fr.",
|
||||
"calSat": "Sâmbătă",
|
||||
"calSun": "Su",
|
||||
"startOnMonday": "Începeți săptămâna luni",
|
||||
"countdownNoDate": "Nu a fost stabilită o dată țintă",
|
||||
"countdownPast": "Evenimentul a trecut",
|
||||
"countdownDays": "zile",
|
||||
"countdownHours": "ore",
|
||||
"countdownMinutes": "min",
|
||||
"countdownSeconds": "sec",
|
||||
"countdownLabel": "Eticheta",
|
||||
"countdownLabelPlaceholder": "de exemplu, ziua lansării",
|
||||
"countdownShowDays": "Zile de spectacol",
|
||||
"countdownShowHours": "Programul de lucru",
|
||||
"targetDate": "Data țintă",
|
||||
"searchEngine": "Motor de căutare",
|
||||
"customSearchUrl": "URL de căutare personalizată",
|
||||
"searchPlaceholder": "Căutare...",
|
||||
"searchGo": "Merge",
|
||||
"searchPlaceholderLabel": "Text substituent",
|
||||
"searchPlaceholderHint": "Text afișat în câmpul de căutare",
|
||||
"openInNewTab": "Deschide într-o filă nouă",
|
||||
"searchQueryPlaceholder": "Introduceți interogarea...",
|
||||
"noSearchShortcuts": "Nu sunt configurate comenzi rapide",
|
||||
"addSearchShortcut": "Adăugați o comandă rapidă",
|
||||
"fontSize": "Dimensiunea fontului",
|
||||
"textAlign": "Aliniere text",
|
||||
"fontWeight": "Grosimea fontului",
|
||||
"clearColor": "Culoare clară",
|
||||
"imageFit": "Potrivire imagine",
|
||||
"imageLinkUrl": "URL-ul linkului",
|
||||
"noImage": "Nu a fost setată nicio adresă URL a imaginii",
|
||||
"altText": "Text alternativ",
|
||||
"altTextPlaceholder": "Descrieți imaginea",
|
||||
"renderMarkdown": "Reducere de randare",
|
||||
"displayMode": "Mod de afișare",
|
||||
"displayField": "Câmp de afișare",
|
||||
"jsonPath": "Calea JSON",
|
||||
"customApiLabel": "Eticheta",
|
||||
"customApiLabelPlaceholder": "de exemplu, temperatura",
|
||||
"customApiUnit": "Unitate",
|
||||
"customApiNoUrl": "Nicio adresă URL API configurată",
|
||||
"customApiError": "Preluarea nu a reușit",
|
||||
"customApiNotArray": "Răspunsul nu este o matrice",
|
||||
"addService": "Adăugați serviciu",
|
||||
"showLabels": "Afișați etichetele",
|
||||
"iconSize": "Dimensiunea pictogramei",
|
||||
"noDashboardLinks": "Nu sunt configurate linkuri la tabloul de bord",
|
||||
"noLimit": "Fără limită",
|
||||
"sectionHeading": "Titlu secțiune",
|
||||
"addSection": "Adăugați secțiune",
|
||||
"compactMode": "Mod compact",
|
||||
"selectHosts": "Selectați gazdele",
|
||||
"listLayout": "Listă",
|
||||
"gridLayout": "Grilă"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Главная"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Ссылки на сервисы",
|
||||
"homepagePreview": "Предпросмотр главной страницы"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Сервисные ссылки",
|
||||
"serviceLinksEmpty": "Пока нет сервисных ссылок",
|
||||
"serviceLinksAdd": "Добавить",
|
||||
"serviceLinksLabelPlaceholder": "Мой сервис",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Введите действительный веб-адрес",
|
||||
"serviceLinksAddFailed": "Не удалось добавить сервисную ссылку"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Главная страница",
|
||||
"addWidget": "Добавить виджет",
|
||||
"editWidget": "Изменить виджет",
|
||||
"zoomIn": "Приблизить",
|
||||
"zoomOut": "Отдалить",
|
||||
"resetView": "Сбросить вид",
|
||||
"lockLayout": "Заблокировать макет",
|
||||
"unlockLayout": "Разблокировать макет",
|
||||
"noWidgets": "Нажмите правой кнопкой мыши или на +, чтобы добавить первый виджет",
|
||||
"openFullView": "Открыть полный вид",
|
||||
"previewTitle": "Предпросмотр домашней страницы",
|
||||
"cancel": "Отмена",
|
||||
"save": "Сохранить",
|
||||
"title_label": "Название",
|
||||
"widgetTitlePlaceholder": "Название виджета (необязательно)",
|
||||
"url": "URL",
|
||||
"imageUrl": "Пользовательский URL изображения",
|
||||
"imageUrlHint": "Оставьте пустым, чтобы автоматически использовать favicon сайта",
|
||||
"showImage": "Показывать изображение",
|
||||
"description": "Описание",
|
||||
"color": "Цвет",
|
||||
"timezone": "Часовой пояс",
|
||||
"invalidTimezone": "Неверный часовой пояс. Используйте название IANA, например, America/New_York.",
|
||||
"showSeconds": "Показывать секунды",
|
||||
"format12h": "12-часовой",
|
||||
"content": "Содержимое",
|
||||
"backgroundColor": "Цвет фона",
|
||||
"links": "Ссылки",
|
||||
"addLink": "Добавить ссылку",
|
||||
"linkLabel": "Название",
|
||||
"categoryLinks": "Ссылки",
|
||||
"categoryInfo": "Информация",
|
||||
"categorySystem": "Система",
|
||||
"copyLink": "Копировать ссылку",
|
||||
"linkCopied": "Ссылка скопирована!",
|
||||
"location": "Местоположение",
|
||||
"temperatureUnit": "Единица температуры",
|
||||
"showForecast": "Показывать прогноз на 3 дня",
|
||||
"scrolling": "Разрешить прокрутку",
|
||||
"feedUrl": "URL ленты",
|
||||
"maxItems": "Макс. элементов",
|
||||
"showDescription": "Показывать описание",
|
||||
"widgetRssName": "RSS-лента",
|
||||
"noHostSelected": "Хост не выбран",
|
||||
"displayedMetrics": "Отображаемые метрики",
|
||||
"metricCpu": "ЦП",
|
||||
"metricMemory": "Память",
|
||||
"metricDisk": "Диск",
|
||||
"metricUptime": "Время работы",
|
||||
"metricSystem": "Система",
|
||||
"metricNetwork": "Сеть",
|
||||
"metricProcesses": "Процессы",
|
||||
"categoryMonitoring": "Мониторинг",
|
||||
"noPingUrls": "URL-адреса не настроены",
|
||||
"pingLabel": "Метка",
|
||||
"addPingUrl": "Добавить URL",
|
||||
"showLatency": "Показать задержку",
|
||||
"refreshInterval": "Интервал обновления",
|
||||
"seconds": "секунд",
|
||||
"filterActivityTypes": "Типы фильтрации",
|
||||
"filterTypesHint": "Оставьте пустым, чтобы показать все",
|
||||
"showTimestamp": "Показывать метку времени",
|
||||
"uptimeUnavailable": "Время работы недоступно",
|
||||
"uptimeLabel": "Время работы",
|
||||
"overviewVersion": "Версия",
|
||||
"overviewUpdateLabel": "Обновлять",
|
||||
"overviewUpdateAvailable": "Доступно обновление",
|
||||
"overviewDatabase": "База данных",
|
||||
"overviewUptime": "Время работы",
|
||||
"noHosts": "Нет настроенных хостов",
|
||||
"hostGridHosts": "Хосты",
|
||||
"hostGridAllHint": "Оставьте пустым, чтобы показать все хосты",
|
||||
"columns": "Колонки",
|
||||
"showIp": "Показать IP-адрес",
|
||||
"layout": "Макет",
|
||||
"showStatus": "Показать статус",
|
||||
"noActivity": "Нет активности",
|
||||
"widgetPingStatusName": "Статус ping",
|
||||
"widgetRecentActivityName": "Недавняя активность",
|
||||
"sshTerminalNoHost": "Хост не настроен",
|
||||
"sshTerminalConnect": "Подключиться",
|
||||
"sshTerminalAutoConnect": "Автоподключение при загрузке",
|
||||
"connectionTypes": "Типы подключений",
|
||||
"calMon": "Пн",
|
||||
"calTue": "Вт",
|
||||
"calWed": "Ср",
|
||||
"calThu": "Чт",
|
||||
"calFri": "Пт",
|
||||
"calSat": "Сб",
|
||||
"calSun": "Вс",
|
||||
"startOnMonday": "Начинать неделю с понедельника",
|
||||
"countdownNoDate": "Целевая дата не задана",
|
||||
"countdownPast": "Событие прошло",
|
||||
"countdownDays": "дн.",
|
||||
"countdownHours": "ч",
|
||||
"countdownMinutes": "мин",
|
||||
"countdownSeconds": "с",
|
||||
"countdownLabel": "Название",
|
||||
"countdownLabelPlaceholder": "например, День запуска",
|
||||
"countdownShowDays": "Показать дни",
|
||||
"countdownShowHours": "Показать часы",
|
||||
"targetDate": "Целевая дата",
|
||||
"searchEngine": "Поисковая система",
|
||||
"customSearchUrl": "Пользовательский URL поиска",
|
||||
"searchPlaceholder": "Поиск...",
|
||||
"searchGo": "Искать",
|
||||
"searchPlaceholderLabel": "Текст подсказки",
|
||||
"searchPlaceholderHint": "Текст внутри поля поиска",
|
||||
"openInNewTab": "Открывать в новой вкладке",
|
||||
"searchQueryPlaceholder": "Введите запрос...",
|
||||
"noSearchShortcuts": "Нет ярлыков поиска",
|
||||
"addSearchShortcut": "Добавить ярлык",
|
||||
"fontSize": "Размер шрифта",
|
||||
"textAlign": "Выравнивание текста",
|
||||
"fontWeight": "Жирность шрифта",
|
||||
"clearColor": "Очистить цвет",
|
||||
"imageFit": "Вписывание изображения",
|
||||
"imageLinkUrl": "URL ссылки",
|
||||
"noImage": "URL изображения не задан",
|
||||
"altText": "Альтернативный текст",
|
||||
"altTextPlaceholder": "Опишите изображение",
|
||||
"renderMarkdown": "Отображать Markdown",
|
||||
"displayMode": "Режим отображения",
|
||||
"displayField": "Отображаемое поле",
|
||||
"jsonPath": "Путь JSON",
|
||||
"customApiLabel": "Название",
|
||||
"customApiLabelPlaceholder": "например, Температура",
|
||||
"customApiUnit": "Единица измерения",
|
||||
"customApiNoUrl": "URL API не настроен",
|
||||
"customApiError": "Не удалось загрузить данные",
|
||||
"customApiNotArray": "Ответ не является массивом",
|
||||
"addService": "Добавить сервис",
|
||||
"showLabels": "Показывать метки",
|
||||
"iconSize": "Размер иконок",
|
||||
"noDashboardLinks": "Нет настроенных ссылок на панель",
|
||||
"noLimit": "Без лимита",
|
||||
"sectionHeading": "Заголовок раздела",
|
||||
"addSection": "Добавить раздел",
|
||||
"compactMode": "Компактный режим",
|
||||
"selectHosts": "Выбрать хосты",
|
||||
"listLayout": "Список",
|
||||
"gridLayout": "Сетка"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Почетна страница"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Линкови услуга",
|
||||
"homepagePreview": "Преглед почетне странице"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Линкови услуга",
|
||||
"serviceLinksEmpty": "Још нема линкова за услуге",
|
||||
"serviceLinksAdd": "Додај",
|
||||
"serviceLinksLabelPlaceholder": "Моја услуга",
|
||||
"serviceLinksUrlPlaceholder": "хттп://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Унесите важећу веб адресу",
|
||||
"serviceLinksAddFailed": "Додавање линка услуге није успело"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Почетна страница",
|
||||
"addWidget": "Додај виџет",
|
||||
"editWidget": "Измени виџет",
|
||||
"zoomIn": "Увећај",
|
||||
"zoomOut": "Умањи",
|
||||
"resetView": "Ресетуј приказ",
|
||||
"lockLayout": "Закључај распоред",
|
||||
"unlockLayout": "Откључај распоред",
|
||||
"noWidgets": "Кликните десним тастером миша или кликните на + да бисте додали свој први виџет",
|
||||
"openFullView": "Отвори цео приказ",
|
||||
"previewTitle": "Преглед почетне странице",
|
||||
"cancel": "Откажи",
|
||||
"save": "Сачувај",
|
||||
"title_label": "Наслов",
|
||||
"widgetTitlePlaceholder": "Наслов виџета (опционо)",
|
||||
"url": "URL",
|
||||
"imageUrl": "Прилагођени URL слике",
|
||||
"imageUrlHint": "Оставите празно да бисте аутоматски користили фавикон сајта",
|
||||
"showImage": "Прикажи слику",
|
||||
"description": "Опис",
|
||||
"color": "Боја",
|
||||
"timezone": "Временска зона",
|
||||
"invalidTimezone": "Неважећа временска зона. Користите IANA назив као што је Америка/Њујорк",
|
||||
"showSeconds": "Прикажи секунде",
|
||||
"format12h": "12 сати",
|
||||
"content": "Садржај",
|
||||
"backgroundColor": "Боја позадине",
|
||||
"links": "Линкови",
|
||||
"addLink": "Додај линк",
|
||||
"linkLabel": "Ознака",
|
||||
"categoryLinks": "Линкови",
|
||||
"categoryInfo": "Информације",
|
||||
"categorySystem": "Систем",
|
||||
"copyLink": "Копирај линк",
|
||||
"linkCopied": "Линк копиран!",
|
||||
"location": "Локација",
|
||||
"temperatureUnit": "Јединица за температуру",
|
||||
"showForecast": "Прикажи тродневну прогнозу",
|
||||
"scrolling": "Дозволи скроловање",
|
||||
"feedUrl": "URL фида",
|
||||
"maxItems": "Максималан број ставки",
|
||||
"showDescription": "Прикажи опис",
|
||||
"widgetRssName": "RSS фид",
|
||||
"noHostSelected": "Није изабран домаћин",
|
||||
"displayedMetrics": "Приказане метрике",
|
||||
"metricCpu": "Процесор",
|
||||
"metricMemory": "Памћење",
|
||||
"metricDisk": "Диск",
|
||||
"metricUptime": "Време непрекидног рада",
|
||||
"metricSystem": "Систем",
|
||||
"metricNetwork": "Мрежа",
|
||||
"metricProcesses": "Процеси",
|
||||
"categoryMonitoring": "Праћење",
|
||||
"noPingUrls": "Није конфигурисан ниједан URL",
|
||||
"pingLabel": "Ознака",
|
||||
"addPingUrl": "Додај URL",
|
||||
"showLatency": "Прикажи латенцију",
|
||||
"refreshInterval": "Интервал освежавања",
|
||||
"seconds": "секунде",
|
||||
"filterActivityTypes": "Типови филтера",
|
||||
"filterTypesHint": "Оставите празно да бисте видели све",
|
||||
"showTimestamp": "Прикажи временску ознаку",
|
||||
"uptimeUnavailable": "Време непрекидног рада није доступно",
|
||||
"uptimeLabel": "Време непрекидног рада",
|
||||
"overviewVersion": "Верзија",
|
||||
"overviewUpdateLabel": "Ажурирање",
|
||||
"overviewUpdateAvailable": "Ажурирање је доступно",
|
||||
"overviewDatabase": "База података",
|
||||
"overviewUptime": "Време непрекидног рада",
|
||||
"noHosts": "Ниједан хост није конфигурисан",
|
||||
"hostGridHosts": "Домаћини",
|
||||
"hostGridAllHint": "Оставите празно да бисте приказали све хостове",
|
||||
"columns": "Колоне",
|
||||
"showIp": "Прикажи ИП адресу",
|
||||
"layout": "Распоред",
|
||||
"showStatus": "Прикажи статус",
|
||||
"noActivity": "Нема активности",
|
||||
"widgetPingStatusName": "Статус пинга",
|
||||
"widgetRecentActivityName": "Недавне активности",
|
||||
"sshTerminalNoHost": "Није конфигурисан хост",
|
||||
"sshTerminalConnect": "Повежи се",
|
||||
"sshTerminalAutoConnect": "Аутоматско повезивање при учитавању",
|
||||
"connectionTypes": "Типови повезивања",
|
||||
"calMon": "Мо",
|
||||
"calTue": "Уто",
|
||||
"calWed": "Ми",
|
||||
"calThu": "Чет",
|
||||
"calFri": "Пет",
|
||||
"calSat": "Са",
|
||||
"calSun": "Су",
|
||||
"startOnMonday": "Почетак недеље у понедељак",
|
||||
"countdownNoDate": "Није одређен циљни датум",
|
||||
"countdownPast": "Догађај је прошао",
|
||||
"countdownDays": "дана",
|
||||
"countdownHours": "сати",
|
||||
"countdownMinutes": "мин",
|
||||
"countdownSeconds": "сек",
|
||||
"countdownLabel": "Ознака",
|
||||
"countdownLabelPlaceholder": "нпр. Дан лансирања",
|
||||
"countdownShowDays": "Дани емисије",
|
||||
"countdownShowHours": "Радно време емисије",
|
||||
"targetDate": "Циљни датум",
|
||||
"searchEngine": "Претраживач",
|
||||
"customSearchUrl": "URL прилагођене претраге",
|
||||
"searchPlaceholder": "Претрага...",
|
||||
"searchGo": "Иди",
|
||||
"searchPlaceholderLabel": "Текст резервисаног места",
|
||||
"searchPlaceholderHint": "Текст приказан унутар поља за претрагу",
|
||||
"openInNewTab": "Отвори у новој картици",
|
||||
"searchQueryPlaceholder": "Унесите упит...",
|
||||
"noSearchShortcuts": "Ниједна пречица није конфигурисана",
|
||||
"addSearchShortcut": "Додај пречицу",
|
||||
"fontSize": "Величина фонта",
|
||||
"textAlign": "Поравнање текста",
|
||||
"fontWeight": "Дебљина фонта",
|
||||
"clearColor": "Чиста боја",
|
||||
"imageFit": "Уклапање слике",
|
||||
"imageLinkUrl": "URL линка",
|
||||
"noImage": "Није подешен URL слике",
|
||||
"altText": "Алтернативни текст",
|
||||
"altTextPlaceholder": "Опишите слику",
|
||||
"renderMarkdown": "Рендеровање маркдауна",
|
||||
"displayMode": "Режим приказа",
|
||||
"displayField": "Поље за приказ",
|
||||
"jsonPath": "JSON путања",
|
||||
"customApiLabel": "Ознака",
|
||||
"customApiLabelPlaceholder": "нпр. Температура",
|
||||
"customApiUnit": "Јединица",
|
||||
"customApiNoUrl": "Није конфигурисан API URL",
|
||||
"customApiError": "Преузимање није успело",
|
||||
"customApiNotArray": "Одговор није низ",
|
||||
"addService": "Додај услугу",
|
||||
"showLabels": "Прикажи ознаке",
|
||||
"iconSize": "Величина иконе",
|
||||
"noDashboardLinks": "Није конфигурисан ниједан линк до контролне табле",
|
||||
"noLimit": "Без ограничења",
|
||||
"sectionHeading": "Наслов одељка",
|
||||
"addSection": "Додај одељак",
|
||||
"compactMode": "Компактни режим",
|
||||
"selectHosts": "Изаберите хостове",
|
||||
"listLayout": "Листа",
|
||||
"gridLayout": "Мрежа"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Hemsida"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Servicelänkar",
|
||||
"homepagePreview": "Förhandsvisning av startsidan"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Servicelänkar",
|
||||
"serviceLinksEmpty": "Inga tjänstelänkar ännu",
|
||||
"serviceLinksAdd": "Tillägga",
|
||||
"serviceLinksLabelPlaceholder": "Min tjänst",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Ange en giltig webbadress",
|
||||
"serviceLinksAddFailed": "Misslyckades med att lägga till tjänstelänk"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Hemsida",
|
||||
"addWidget": "Lägg till widget",
|
||||
"editWidget": "Redigera widget",
|
||||
"zoomIn": "Zooma in",
|
||||
"zoomOut": "Zooma ut",
|
||||
"resetView": "Återställ vy",
|
||||
"lockLayout": "Låslayout",
|
||||
"unlockLayout": "Lås upp layouten",
|
||||
"noWidgets": "Högerklicka eller klicka på + för att lägga till din första widget",
|
||||
"openFullView": "Öppna helvy",
|
||||
"previewTitle": "Förhandsvisning av startsidan",
|
||||
"cancel": "Avboka",
|
||||
"save": "Spara",
|
||||
"title_label": "Titel",
|
||||
"widgetTitlePlaceholder": "Widgettitel (valfritt)",
|
||||
"url": "URL",
|
||||
"imageUrl": "Anpassad bild-URL",
|
||||
"imageUrlHint": "Lämna tomt för att använda webbplatsens favicon automatiskt",
|
||||
"showImage": "Visa bild",
|
||||
"description": "Beskrivning",
|
||||
"color": "Färg",
|
||||
"timezone": "Tidszon",
|
||||
"invalidTimezone": "Ogiltig tidszon. Använd ett IANA-namn som America/New_York",
|
||||
"showSeconds": "Visa sekunder",
|
||||
"format12h": "12-timmars",
|
||||
"content": "Innehåll",
|
||||
"backgroundColor": "Bakgrundsfärg",
|
||||
"links": "Länkar",
|
||||
"addLink": "Lägg till länk",
|
||||
"linkLabel": "Märka",
|
||||
"categoryLinks": "Länkar",
|
||||
"categoryInfo": "Info",
|
||||
"categorySystem": "System",
|
||||
"copyLink": "Kopiera länk",
|
||||
"linkCopied": "Länk kopierad!",
|
||||
"location": "Plats",
|
||||
"temperatureUnit": "Temperaturenhet",
|
||||
"showForecast": "Visa 3-dagarsprognos",
|
||||
"scrolling": "Tillåt rullning",
|
||||
"feedUrl": "Flödes-URL",
|
||||
"maxItems": "Max antal artiklar",
|
||||
"showDescription": "Visa beskrivning",
|
||||
"widgetRssName": "RSS-flöde",
|
||||
"noHostSelected": "Ingen värd vald",
|
||||
"displayedMetrics": "Visade mätvärden",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "Minne",
|
||||
"metricDisk": "Disk",
|
||||
"metricUptime": "Drifttid",
|
||||
"metricSystem": "System",
|
||||
"metricNetwork": "Nätverk",
|
||||
"metricProcesses": "Processer",
|
||||
"categoryMonitoring": "Övervakning",
|
||||
"noPingUrls": "Inga webbadresser konfigurerade",
|
||||
"pingLabel": "Märka",
|
||||
"addPingUrl": "Lägg till URL",
|
||||
"showLatency": "Visa latens",
|
||||
"refreshInterval": "Uppdateringsintervall",
|
||||
"seconds": "sekunder",
|
||||
"filterActivityTypes": "Filtertyper",
|
||||
"filterTypesHint": "Lämna tomt för att visa alla",
|
||||
"showTimestamp": "Visa tidsstämpel",
|
||||
"uptimeUnavailable": "Drifttid otillgänglig",
|
||||
"uptimeLabel": "Drifttid",
|
||||
"overviewVersion": "Version",
|
||||
"overviewUpdateLabel": "Uppdatera",
|
||||
"overviewUpdateAvailable": "Uppdatering tillgänglig",
|
||||
"overviewDatabase": "Databas",
|
||||
"overviewUptime": "Drifttid",
|
||||
"noHosts": "Inga värdar konfigurerade",
|
||||
"hostGridHosts": "Värdar",
|
||||
"hostGridAllHint": "Lämna tomt för att visa alla värdar",
|
||||
"columns": "Kolumner",
|
||||
"showIp": "Visa IP-adress",
|
||||
"layout": "Layout",
|
||||
"showStatus": "Visa status",
|
||||
"noActivity": "Ingen aktivitet",
|
||||
"widgetPingStatusName": "Pingstatus",
|
||||
"widgetRecentActivityName": "Senaste aktivitet",
|
||||
"sshTerminalNoHost": "Ingen värd konfigurerad",
|
||||
"sshTerminalConnect": "Ansluta",
|
||||
"sshTerminalAutoConnect": "Automatisk anslutning vid laddning",
|
||||
"connectionTypes": "Anslutningstyper",
|
||||
"calMon": "Må",
|
||||
"calTue": "Tis",
|
||||
"calWed": "Vi",
|
||||
"calThu": "To",
|
||||
"calFri": "Fr",
|
||||
"calSat": "Lör",
|
||||
"calSun": "Sö",
|
||||
"startOnMonday": "Börja veckan på måndag",
|
||||
"countdownNoDate": "Inget måldatum angivet",
|
||||
"countdownPast": "Händelsen har passerat",
|
||||
"countdownDays": "dagar",
|
||||
"countdownHours": "timmar",
|
||||
"countdownMinutes": "min",
|
||||
"countdownSeconds": "sek",
|
||||
"countdownLabel": "Märka",
|
||||
"countdownLabelPlaceholder": "t.ex. lanseringsdagen",
|
||||
"countdownShowDays": "Visa dagar",
|
||||
"countdownShowHours": "Visa öppettider",
|
||||
"targetDate": "Måldatum",
|
||||
"searchEngine": "Sökmotor",
|
||||
"customSearchUrl": "Anpassad sök-URL",
|
||||
"searchPlaceholder": "Söka...",
|
||||
"searchGo": "Gå",
|
||||
"searchPlaceholderLabel": "Platshållartext",
|
||||
"searchPlaceholderHint": "Text som visas inuti sökinmatningen",
|
||||
"openInNewTab": "Öppna i ny flik",
|
||||
"searchQueryPlaceholder": "Ange fråga...",
|
||||
"noSearchShortcuts": "Inga genvägar konfigurerade",
|
||||
"addSearchShortcut": "Lägg till genväg",
|
||||
"fontSize": "Fontstorlek",
|
||||
"textAlign": "Textjustering",
|
||||
"fontWeight": "Teckensnittsvikt",
|
||||
"clearColor": "Klar färg",
|
||||
"imageFit": "Bildanpassning",
|
||||
"imageLinkUrl": "Länk-URL",
|
||||
"noImage": "Ingen bild-URL angiven",
|
||||
"altText": "Alternativtext",
|
||||
"altTextPlaceholder": "Beskriv bilden",
|
||||
"renderMarkdown": "Rendera Markdown",
|
||||
"displayMode": "Visningsläge",
|
||||
"displayField": "Visningsfält",
|
||||
"jsonPath": "JSON-sökväg",
|
||||
"customApiLabel": "Märka",
|
||||
"customApiLabelPlaceholder": "t.ex. temperatur",
|
||||
"customApiUnit": "Enhet",
|
||||
"customApiNoUrl": "Ingen API-URL konfigurerad",
|
||||
"customApiError": "Misslyckades med att hämta",
|
||||
"customApiNotArray": "Svaret är inte en array",
|
||||
"addService": "Lägg till tjänst",
|
||||
"showLabels": "Visa etiketter",
|
||||
"iconSize": "Ikonstorlek",
|
||||
"noDashboardLinks": "Inga länkar till instrumentpanelen konfigurerade",
|
||||
"noLimit": "Ingen gräns",
|
||||
"sectionHeading": "Avsnittsrubrik",
|
||||
"addSection": "Lägg till avsnitt",
|
||||
"compactMode": "Kompaktläge",
|
||||
"selectHosts": "Välj värdar",
|
||||
"listLayout": "Lista",
|
||||
"gridLayout": "Rutnät"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "หน้าแรก"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "ลิงก์บริการ",
|
||||
"homepagePreview": "ตัวอย่างหน้าแรก"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "ลิงก์บริการ",
|
||||
"serviceLinksEmpty": "ยังไม่มีลิงก์บริการ",
|
||||
"serviceLinksAdd": "เพิ่ม",
|
||||
"serviceLinksLabelPlaceholder": "บริการของฉัน",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "ป้อนที่อยู่เว็บที่ถูกต้อง",
|
||||
"serviceLinksAddFailed": "ไม่สามารถเพิ่มลิงก์บริการได้"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "หน้าแรก",
|
||||
"addWidget": "เพิ่มวิดเจ็ต",
|
||||
"editWidget": "แก้ไขวิดเจ็ต",
|
||||
"zoomIn": "ซูมเข้า",
|
||||
"zoomOut": "ซูมออก",
|
||||
"resetView": "รีเซ็ตมุมมอง",
|
||||
"lockLayout": "การจัดวางล็อค",
|
||||
"unlockLayout": "ปลดล็อกเค้าโครง",
|
||||
"noWidgets": "คลิกขวาหรือคลิก + เพื่อเพิ่มวิดเจ็ตแรกของคุณ",
|
||||
"openFullView": "เปิดดูแบบเต็มหน้าจอ",
|
||||
"previewTitle": "ตัวอย่างหน้าแรก",
|
||||
"cancel": "ยกเลิก",
|
||||
"save": "บันทึก",
|
||||
"title_label": "ชื่อ",
|
||||
"widgetTitlePlaceholder": "ชื่อวิดเจ็ต (ไม่บังคับ)",
|
||||
"url": "URL",
|
||||
"imageUrl": "URL รูปภาพแบบกำหนดเอง",
|
||||
"imageUrlHint": "เว้นว่างไว้เพื่อใช้ไอคอนเว็บไซต์โดยอัตโนมัติ",
|
||||
"showImage": "แสดงภาพ",
|
||||
"description": "คำอธิบาย",
|
||||
"color": "สี",
|
||||
"timezone": "เขตเวลา",
|
||||
"invalidTimezone": "เขตเวลาไม่ถูกต้อง กรุณาใช้ชื่อ IANA เช่น America/New_York",
|
||||
"showSeconds": "แสดงวินาที",
|
||||
"format12h": "12 ชั่วโมง",
|
||||
"content": "เนื้อหา",
|
||||
"backgroundColor": "สีพื้นหลัง",
|
||||
"links": "ลิงก์",
|
||||
"addLink": "เพิ่มลิงก์",
|
||||
"linkLabel": "ฉลาก",
|
||||
"categoryLinks": "ลิงก์",
|
||||
"categoryInfo": "ข้อมูล",
|
||||
"categorySystem": "ระบบ",
|
||||
"copyLink": "คัดลอกลิงก์",
|
||||
"linkCopied": "คัดลอกลิงก์เรียบร้อยแล้ว!",
|
||||
"location": "ที่ตั้ง",
|
||||
"temperatureUnit": "หน่วยอุณหภูมิ",
|
||||
"showForecast": "แสดงพยากรณ์อากาศ 3 วัน",
|
||||
"scrolling": "อนุญาตให้เลื่อน",
|
||||
"feedUrl": "URL ฟีด",
|
||||
"maxItems": "ไอเทมสูงสุด",
|
||||
"showDescription": "แสดงคำอธิบาย",
|
||||
"widgetRssName": "ฟีด RSS",
|
||||
"noHostSelected": "ไม่ได้เลือกโฮสต์",
|
||||
"displayedMetrics": "ตัวชี้วัดที่แสดงผล",
|
||||
"metricCpu": "ซีพียู",
|
||||
"metricMemory": "หน่วยความจำ",
|
||||
"metricDisk": "ดิสก์",
|
||||
"metricUptime": "เวลาใช้งาน",
|
||||
"metricSystem": "ระบบ",
|
||||
"metricNetwork": "เครือข่าย",
|
||||
"metricProcesses": "กระบวนการ",
|
||||
"categoryMonitoring": "การตรวจสอบ",
|
||||
"noPingUrls": "ไม่มีการกำหนดค่า URL",
|
||||
"pingLabel": "ฉลาก",
|
||||
"addPingUrl": "เพิ่ม URL",
|
||||
"showLatency": "แสดงค่าความหน่วง",
|
||||
"refreshInterval": "ช่วงเวลาการรีเฟรช",
|
||||
"seconds": "วินาที",
|
||||
"filterActivityTypes": "ประเภทตัวกรอง",
|
||||
"filterTypesHint": "เว้นว่างไว้เพื่อแสดงทั้งหมด",
|
||||
"showTimestamp": "แสดงเวลา",
|
||||
"uptimeUnavailable": "ไม่สามารถระบุเวลาใช้งานได้",
|
||||
"uptimeLabel": "เวลาใช้งาน",
|
||||
"overviewVersion": "เวอร์ชั่น",
|
||||
"overviewUpdateLabel": "อัปเดต",
|
||||
"overviewUpdateAvailable": "มีการอัปเดตแล้ว",
|
||||
"overviewDatabase": "ฐานข้อมูล",
|
||||
"overviewUptime": "เวลาใช้งาน",
|
||||
"noHosts": "ไม่มีการกำหนดค่าโฮสต์",
|
||||
"hostGridHosts": "โฮสต์",
|
||||
"hostGridAllHint": "เว้นว่างไว้เพื่อแสดงโฮสต์ทั้งหมด",
|
||||
"columns": "คอลัมน์",
|
||||
"showIp": "แสดงที่อยู่ IP",
|
||||
"layout": "เค้าโครง",
|
||||
"showStatus": "แสดงสถานะ",
|
||||
"noActivity": "ไม่มีกิจกรรม",
|
||||
"widgetPingStatusName": "สถานะการปิง",
|
||||
"widgetRecentActivityName": "กิจกรรมล่าสุด",
|
||||
"sshTerminalNoHost": "ไม่มีการกำหนดค่าโฮสต์",
|
||||
"sshTerminalConnect": "เชื่อมต่อ",
|
||||
"sshTerminalAutoConnect": "เชื่อมต่ออัตโนมัติเมื่อโหลดเสร็จ",
|
||||
"connectionTypes": "ประเภทการเชื่อมต่อ",
|
||||
"calMon": "โม",
|
||||
"calTue": "ตู",
|
||||
"calWed": "เรา",
|
||||
"calThu": "ไทย",
|
||||
"calFri": "ฟร",
|
||||
"calSat": "ซา",
|
||||
"calSun": "ซู",
|
||||
"startOnMonday": "เริ่มต้นสัปดาห์ในวันจันทร์",
|
||||
"countdownNoDate": "ยังไม่ได้กำหนดวันเป้าหมาย",
|
||||
"countdownPast": "กิจกรรมได้จบลงแล้ว",
|
||||
"countdownDays": "วัน",
|
||||
"countdownHours": "ชั่วโมง",
|
||||
"countdownMinutes": "นาที",
|
||||
"countdownSeconds": "วินาที",
|
||||
"countdownLabel": "ฉลาก",
|
||||
"countdownLabelPlaceholder": "เช่น วันเปิดตัว",
|
||||
"countdownShowDays": "วันแสดง",
|
||||
"countdownShowHours": "เวลาทำการ",
|
||||
"targetDate": "วันที่เป้าหมาย",
|
||||
"searchEngine": "เครื่องมือค้นหา",
|
||||
"customSearchUrl": "URL ค้นหาแบบกำหนดเอง",
|
||||
"searchPlaceholder": "ค้นหา...",
|
||||
"searchGo": "ไป",
|
||||
"searchPlaceholderLabel": "ข้อความตัวอย่าง",
|
||||
"searchPlaceholderHint": "ข้อความที่แสดงอยู่ภายในช่องค้นหา",
|
||||
"openInNewTab": "เปิดในแท็บใหม่",
|
||||
"searchQueryPlaceholder": "ป้อนคำค้นหา...",
|
||||
"noSearchShortcuts": "ไม่ได้ตั้งค่าทางลัดใดๆ",
|
||||
"addSearchShortcut": "เพิ่มทางลัด",
|
||||
"fontSize": "ขนาดตัวอักษร",
|
||||
"textAlign": "จัดแนวข้อความ",
|
||||
"fontWeight": "น้ำหนักตัวอักษร",
|
||||
"clearColor": "สีใส",
|
||||
"imageFit": "ภาพที่พอดี",
|
||||
"imageLinkUrl": "ลิงก์ URL",
|
||||
"noImage": "ไม่ได้ตั้งค่า URL รูปภาพ",
|
||||
"altText": "ข้อความแสดงแทน (Alt Text)",
|
||||
"altTextPlaceholder": "อธิบายภาพ",
|
||||
"renderMarkdown": "แสดงผลมาร์คดาวน์",
|
||||
"displayMode": "โหมดการแสดงผล",
|
||||
"displayField": "ช่องแสดงผล",
|
||||
"jsonPath": "เส้นทาง JSON",
|
||||
"customApiLabel": "ฉลาก",
|
||||
"customApiLabelPlaceholder": "เช่น อุณหภูมิ",
|
||||
"customApiUnit": "หน่วย",
|
||||
"customApiNoUrl": "ไม่ได้กำหนดค่า URL API ไว้",
|
||||
"customApiError": "ไม่สามารถดึงข้อมูลได้",
|
||||
"customApiNotArray": "คำตอบไม่ใช่อาร์เรย์",
|
||||
"addService": "เพิ่มบริการ",
|
||||
"showLabels": "แสดงป้ายกำกับ",
|
||||
"iconSize": "ขนาดไอคอน",
|
||||
"noDashboardLinks": "ไม่มีการกำหนดค่าลิงก์แดชบอร์ด",
|
||||
"noLimit": "ไม่มีขีดจำกัด",
|
||||
"sectionHeading": "หัวข้อส่วน",
|
||||
"addSection": "เพิ่มส่วน",
|
||||
"compactMode": "โหมดกะทัดรัด",
|
||||
"selectHosts": "เลือกโฮสต์",
|
||||
"listLayout": "รายการ",
|
||||
"gridLayout": "ตาราง"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Ana Sayfa"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Hizmet Bağlantıları",
|
||||
"homepagePreview": "Ana Sayfa Önizlemesi"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Servis Bağlantıları",
|
||||
"serviceLinksEmpty": "Henüz servis bağlantısı yok",
|
||||
"serviceLinksAdd": "Ekle",
|
||||
"serviceLinksLabelPlaceholder": "Hizmetim",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Geçerli bir web adresi girin",
|
||||
"serviceLinksAddFailed": "Servis bağlantısı eklenemedi"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Ana Sayfa",
|
||||
"addWidget": "Bileşen Ekle",
|
||||
"editWidget": "Bileşeni Düzenle",
|
||||
"zoomIn": "Yakınlaştır",
|
||||
"zoomOut": "Uzaklaştır",
|
||||
"resetView": "Görünümü Sıfırla",
|
||||
"lockLayout": "Düzeni Kilitle",
|
||||
"unlockLayout": "Düzen Kilidini Aç",
|
||||
"noWidgets": "İlk widget'ınızı eklemek için sağ tıklayın veya + simgesine tıklayın",
|
||||
"openFullView": "Tam Görünümü Aç",
|
||||
"previewTitle": "Ana Sayfa Önizlemesi",
|
||||
"cancel": "İptal",
|
||||
"save": "Kaydet",
|
||||
"title_label": "Başlık",
|
||||
"widgetTitlePlaceholder": "Widget başlığı (isteğe bağlı)",
|
||||
"url": "URL",
|
||||
"imageUrl": "Özel Resim URL'si",
|
||||
"imageUrlHint": "Site favicon'unu otomatik olarak kullanmak için boş bırakın",
|
||||
"showImage": "Resmi Göster",
|
||||
"description": "Açıklama",
|
||||
"color": "Renk",
|
||||
"timezone": "Zaman Dilimi",
|
||||
"invalidTimezone": "Geçersiz saat dilimi. Lütfen America/New_York gibi bir IANA adı kullanın.",
|
||||
"showSeconds": "Saniyeleri göster",
|
||||
"format12h": "12 saatlik",
|
||||
"content": "İçerik",
|
||||
"backgroundColor": "Arka Plan Rengi",
|
||||
"links": "Bağlantılar",
|
||||
"addLink": "Bağlantı Ekle",
|
||||
"linkLabel": "Etiket",
|
||||
"categoryLinks": "Bağlantılar",
|
||||
"categoryInfo": "Bilgi",
|
||||
"categorySystem": "Sistem",
|
||||
"copyLink": "Bağlantıyı Kopyala",
|
||||
"linkCopied": "Bağlantı kopyalandı!",
|
||||
"location": "Konum",
|
||||
"temperatureUnit": "Sıcaklık Birimi",
|
||||
"showForecast": "3 günlük tahmini göster",
|
||||
"scrolling": "Kaydırmaya İzin Ver",
|
||||
"feedUrl": "Akış URL'si",
|
||||
"maxItems": "Maksimum Öğe Sayısı",
|
||||
"showDescription": "Açıklamayı Göster",
|
||||
"widgetRssName": "RSS Akışı",
|
||||
"noHostSelected": "Sunucu seçilmedi",
|
||||
"displayedMetrics": "Gösterilen Metrikler",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "Bellek",
|
||||
"metricDisk": "Disk",
|
||||
"metricUptime": "Çalışma Süresi",
|
||||
"metricSystem": "Sistem",
|
||||
"metricNetwork": "Ağ",
|
||||
"metricProcesses": "İşlemler",
|
||||
"categoryMonitoring": "İzleme",
|
||||
"noPingUrls": "Hiçbir URL yapılandırılmamış",
|
||||
"pingLabel": "Etiket",
|
||||
"addPingUrl": "URL Ekle",
|
||||
"showLatency": "Gecikmeyi Göster",
|
||||
"refreshInterval": "Yenileme Aralığı",
|
||||
"seconds": "saniye",
|
||||
"filterActivityTypes": "Filtre Türleri",
|
||||
"filterTypesHint": "Tümünü göstermek için boş bırakın",
|
||||
"showTimestamp": "Zaman Damgasını Göster",
|
||||
"uptimeUnavailable": "Çalışma süresi kullanılamıyor",
|
||||
"uptimeLabel": "Çalışma Süresi",
|
||||
"overviewVersion": "Sürüm",
|
||||
"overviewUpdateLabel": "Güncelleme",
|
||||
"overviewUpdateAvailable": "Güncelleme mevcut",
|
||||
"overviewDatabase": "Veritabanı",
|
||||
"overviewUptime": "Çalışma Süresi",
|
||||
"noHosts": "Hiçbir sunucu yapılandırılmamış",
|
||||
"hostGridHosts": "Sunucular",
|
||||
"hostGridAllHint": "Tüm sunucuları göstermek için boş bırakın",
|
||||
"columns": "Sütunlar",
|
||||
"showIp": "IP Adresini Göster",
|
||||
"layout": "Düzen",
|
||||
"showStatus": "Durumu Göster",
|
||||
"noActivity": "Etkinlik yok",
|
||||
"widgetPingStatusName": "Ping Durumu",
|
||||
"widgetRecentActivityName": "Son Etkinlikler",
|
||||
"sshTerminalNoHost": "Hiçbir sunucu yapılandırılmamış",
|
||||
"sshTerminalConnect": "Bağlan",
|
||||
"sshTerminalAutoConnect": "Yüklendiğinde otomatik bağlan",
|
||||
"connectionTypes": "Bağlantı Türleri",
|
||||
"calMon": "Pzt",
|
||||
"calTue": "Sal",
|
||||
"calWed": "Çar",
|
||||
"calThu": "Per",
|
||||
"calFri": "Cum",
|
||||
"calSat": "Cmt",
|
||||
"calSun": "Paz",
|
||||
"startOnMonday": "Haftayı Pazartesi başlat",
|
||||
"countdownNoDate": "Hedef tarih ayarlanmadı",
|
||||
"countdownPast": "Etkinlik geçmişte kaldı",
|
||||
"countdownDays": "gün",
|
||||
"countdownHours": "sa",
|
||||
"countdownMinutes": "dk",
|
||||
"countdownSeconds": "sn",
|
||||
"countdownLabel": "Etiket",
|
||||
"countdownLabelPlaceholder": "Örn. Lansman Günü",
|
||||
"countdownShowDays": "Günleri Göster",
|
||||
"countdownShowHours": "Saatleri Göster",
|
||||
"targetDate": "Hedef Tarih",
|
||||
"searchEngine": "Arama Motoru",
|
||||
"customSearchUrl": "Özel Arama URL'si",
|
||||
"searchPlaceholder": "Ara...",
|
||||
"searchGo": "Git",
|
||||
"searchPlaceholderLabel": "Yer Tutucu Metin",
|
||||
"searchPlaceholderHint": "Arama girişinde gösterilen metin",
|
||||
"openInNewTab": "Yeni Sekmede Aç",
|
||||
"searchQueryPlaceholder": "Sorgu girin...",
|
||||
"noSearchShortcuts": "Hiç kısayol yapılandırılmadı",
|
||||
"addSearchShortcut": "Kısayol Ekle",
|
||||
"fontSize": "Yazı Tipi Boyutu",
|
||||
"textAlign": "Metin Hizalama",
|
||||
"fontWeight": "Yazı Tipi Kalınlığı",
|
||||
"clearColor": "Rengi Temizle",
|
||||
"imageFit": "Görüntü Sığdırma",
|
||||
"imageLinkUrl": "Bağlantı URL'si",
|
||||
"noImage": "Görüntü URL'si ayarlanmadı",
|
||||
"altText": "Alternatif Metin",
|
||||
"altTextPlaceholder": "Görüntüyü tanımlayın",
|
||||
"renderMarkdown": "Markdown'ı Görüntüle",
|
||||
"displayMode": "Görüntüleme Modu",
|
||||
"displayField": "Görüntüleme Alanı",
|
||||
"jsonPath": "JSON Yolu",
|
||||
"customApiLabel": "Etiket",
|
||||
"customApiLabelPlaceholder": "Örn. Sıcaklık",
|
||||
"customApiUnit": "Birim",
|
||||
"customApiNoUrl": "API URL'si yapılandırılmadı",
|
||||
"customApiError": "Getirme başarısız",
|
||||
"customApiNotArray": "Yanıt bir dizi değil",
|
||||
"addService": "Servis Ekle",
|
||||
"showLabels": "Etiketleri Göster",
|
||||
"iconSize": "Simge Boyutu",
|
||||
"noDashboardLinks": "Hiçbir pano bağlantısı yapılandırılmamış",
|
||||
"noLimit": "Sınır yok",
|
||||
"sectionHeading": "Bölüm Başlığı",
|
||||
"addSection": "Bölüm Ekle",
|
||||
"compactMode": "Kompakt Mod",
|
||||
"selectHosts": "Sunucuları Seç",
|
||||
"listLayout": "Liste",
|
||||
"gridLayout": "Izgara"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Домашня сторінка"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Посилання на сервіси",
|
||||
"homepagePreview": "Попередній перегляд головної сторінки"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Посилання на сервіси",
|
||||
"serviceLinksEmpty": "Поки що немає посилань на сервіси",
|
||||
"serviceLinksAdd": "Додати",
|
||||
"serviceLinksLabelPlaceholder": "Моя послуга",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Введіть дійсну веб-адресу",
|
||||
"serviceLinksAddFailed": "Не вдалося додати посилання на послугу"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Домашня сторінка",
|
||||
"addWidget": "Додати віджет",
|
||||
"editWidget": "Редагувати віджет",
|
||||
"zoomIn": "Збільшити масштаб",
|
||||
"zoomOut": "Зменшити масштаб",
|
||||
"resetView": "Скинути вигляд",
|
||||
"lockLayout": "Макет замка",
|
||||
"unlockLayout": "Розблокувати макет",
|
||||
"noWidgets": "Клацніть правою кнопкою миші або натисніть кнопку +, щоб додати свій перший віджет",
|
||||
"openFullView": "Відкрити повний перегляд",
|
||||
"previewTitle": "Попередній перегляд головної сторінки",
|
||||
"cancel": "Скасувати",
|
||||
"save": "Зберегти",
|
||||
"title_label": "Назва",
|
||||
"widgetTitlePlaceholder": "Назва віджета (необов'язково)",
|
||||
"url": "URL-адреса",
|
||||
"imageUrl": "URL-адреса користувацького зображення",
|
||||
"imageUrlHint": "Залиште поле порожнім, щоб автоматично використовувати фавіконку сайту",
|
||||
"showImage": "Показати зображення",
|
||||
"description": "Опис",
|
||||
"color": "Колір",
|
||||
"timezone": "Часовий пояс",
|
||||
"invalidTimezone": "Недійсний часовий пояс. Використайте назву IANA, наприклад, America/New_York.",
|
||||
"showSeconds": "Показати секунди",
|
||||
"format12h": "12-годинний",
|
||||
"content": "Зміст",
|
||||
"backgroundColor": "Колір фону",
|
||||
"links": "Посилання",
|
||||
"addLink": "Додати посилання",
|
||||
"linkLabel": "Мітка",
|
||||
"categoryLinks": "Посилання",
|
||||
"categoryInfo": "Інформація",
|
||||
"categorySystem": "Система",
|
||||
"copyLink": "Копіювати посилання",
|
||||
"linkCopied": "Посилання скопійовано!",
|
||||
"location": "Розташування",
|
||||
"temperatureUnit": "Одиниця вимірювання температури",
|
||||
"showForecast": "Показати 3-денний прогноз",
|
||||
"scrolling": "Дозволити прокручування",
|
||||
"feedUrl": "URL-адреса стрічки",
|
||||
"maxItems": "Макс. кількість елементів",
|
||||
"showDescription": "Показати опис",
|
||||
"widgetRssName": "RSS-канал",
|
||||
"noHostSelected": "Хост не вибрано",
|
||||
"displayedMetrics": "Відображені показники",
|
||||
"metricCpu": "Процесор",
|
||||
"metricMemory": "Пам'ять",
|
||||
"metricDisk": "Диск",
|
||||
"metricUptime": "Час безперебійної роботи",
|
||||
"metricSystem": "Система",
|
||||
"metricNetwork": "Мережа",
|
||||
"metricProcesses": "Процеси",
|
||||
"categoryMonitoring": "Моніторинг",
|
||||
"noPingUrls": "URL-адреси не налаштовано",
|
||||
"pingLabel": "Мітка",
|
||||
"addPingUrl": "Додати URL-адресу",
|
||||
"showLatency": "Показати затримку",
|
||||
"refreshInterval": "Інтервал оновлення",
|
||||
"seconds": "секунди",
|
||||
"filterActivityTypes": "Типи фільтрів",
|
||||
"filterTypesHint": "Залиште порожнім, щоб показати всі",
|
||||
"showTimestamp": "Показати позначку часу",
|
||||
"uptimeUnavailable": "Час безперебійної роботи недоступний",
|
||||
"uptimeLabel": "Час безперебійної роботи",
|
||||
"overviewVersion": "Версія",
|
||||
"overviewUpdateLabel": "Оновлення",
|
||||
"overviewUpdateAvailable": "Доступне оновлення",
|
||||
"overviewDatabase": "База даних",
|
||||
"overviewUptime": "Час безперебійної роботи",
|
||||
"noHosts": "Немає налаштованих хостів",
|
||||
"hostGridHosts": "Хости",
|
||||
"hostGridAllHint": "Залиште порожнім, щоб показати всіх хостів",
|
||||
"columns": "Колонки",
|
||||
"showIp": "Показати IP-адресу",
|
||||
"layout": "Макет",
|
||||
"showStatus": "Показати статус",
|
||||
"noActivity": "Немає активності",
|
||||
"widgetPingStatusName": "Стан пінгу",
|
||||
"widgetRecentActivityName": "Нещодавня активність",
|
||||
"sshTerminalNoHost": "Хост не налаштовано",
|
||||
"sshTerminalConnect": "Підключитися",
|
||||
"sshTerminalAutoConnect": "Автоматичне підключення під час завантаження",
|
||||
"connectionTypes": "Типи з'єднань",
|
||||
"calMon": "Мо",
|
||||
"calTue": "Вт",
|
||||
"calWed": "Ми",
|
||||
"calThu": "Чт",
|
||||
"calFri": "Пт",
|
||||
"calSat": "Сб",
|
||||
"calSun": "Су",
|
||||
"startOnMonday": "Початок тижня в понеділок",
|
||||
"countdownNoDate": "Цільова дата не встановлена",
|
||||
"countdownPast": "Подія пройшла",
|
||||
"countdownDays": "дні",
|
||||
"countdownHours": "год.",
|
||||
"countdownMinutes": "хв",
|
||||
"countdownSeconds": "сек",
|
||||
"countdownLabel": "Мітка",
|
||||
"countdownLabelPlaceholder": "наприклад, день запуску",
|
||||
"countdownShowDays": "Дні показу",
|
||||
"countdownShowHours": "Години роботи шоу",
|
||||
"targetDate": "Цільова дата",
|
||||
"searchEngine": "Пошукова система",
|
||||
"customSearchUrl": "URL-адреса користувацького пошуку",
|
||||
"searchPlaceholder": "Пошук...",
|
||||
"searchGo": "Іти",
|
||||
"searchPlaceholderLabel": "Текст-заповнювач",
|
||||
"searchPlaceholderHint": "Текст, що відображається всередині пошукового поля",
|
||||
"openInNewTab": "Відкрити в новій вкладці",
|
||||
"searchQueryPlaceholder": "Введіть запит...",
|
||||
"noSearchShortcuts": "Немає налаштованих комбінацій клавіш",
|
||||
"addSearchShortcut": "Додати ярлик",
|
||||
"fontSize": "Розмір шрифту",
|
||||
"textAlign": "Вирівнювання тексту",
|
||||
"fontWeight": "Вага шрифту",
|
||||
"clearColor": "Чіткий колір",
|
||||
"imageFit": "Розмір зображення",
|
||||
"imageLinkUrl": "URL-адреса посилання",
|
||||
"noImage": "URL-адреса зображення не встановлена",
|
||||
"altText": "Альтернативний текст",
|
||||
"altTextPlaceholder": "Опишіть зображення",
|
||||
"renderMarkdown": "Рендеринг Markdown",
|
||||
"displayMode": "Режим відображення",
|
||||
"displayField": "Поле відображення",
|
||||
"jsonPath": "Шлях JSON",
|
||||
"customApiLabel": "Мітка",
|
||||
"customApiLabelPlaceholder": "наприклад, температура",
|
||||
"customApiUnit": "Одиниця",
|
||||
"customApiNoUrl": "URL-адреса API не налаштована",
|
||||
"customApiError": "Не вдалося отримати",
|
||||
"customApiNotArray": "Відповідь не є масивом",
|
||||
"addService": "Додати послугу",
|
||||
"showLabels": "Показати мітки",
|
||||
"iconSize": "Розмір значка",
|
||||
"noDashboardLinks": "Посилання на інформаційну панель не налаштовано",
|
||||
"noLimit": "Без обмежень",
|
||||
"sectionHeading": "Заголовок розділу",
|
||||
"addSection": "Додати розділ",
|
||||
"compactMode": "Компактний режим",
|
||||
"selectHosts": "Виберіть хости",
|
||||
"listLayout": "Список",
|
||||
"gridLayout": "Сітка"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "Trang chủ"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "Liên kết Dịch vụ",
|
||||
"homepagePreview": "Xem trước Trang chủ"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "Liên kết dịch vụ",
|
||||
"serviceLinksEmpty": "Chưa có liên kết dịch vụ nào",
|
||||
"serviceLinksAdd": "Thêm",
|
||||
"serviceLinksLabelPlaceholder": "Dịch vụ của tôi",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "Nhập địa chỉ web hợp lệ",
|
||||
"serviceLinksAddFailed": "Thêm liên kết dịch vụ thất bại"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Trang chủ",
|
||||
"addWidget": "Thêm widget",
|
||||
"editWidget": "Sửa widget",
|
||||
"zoomIn": "Phóng to",
|
||||
"zoomOut": "Thu nhỏ",
|
||||
"resetView": "Đặt lại chế độ xem",
|
||||
"lockLayout": "Khóa bố cục",
|
||||
"unlockLayout": "Mở khóa bố cục",
|
||||
"noWidgets": "Nhấp chuột phải hoặc nhấn + để thêm widget đầu tiên",
|
||||
"openFullView": "Mở toàn màn hình",
|
||||
"previewTitle": "Xem trước trang chủ",
|
||||
"cancel": "Hủy",
|
||||
"save": "Lưu",
|
||||
"title_label": "Tiêu đề",
|
||||
"widgetTitlePlaceholder": "Tiêu đề widget (tùy chọn)",
|
||||
"url": "URL",
|
||||
"imageUrl": "URL hình ảnh tùy chỉnh",
|
||||
"imageUrlHint": "Để trống để tự động dùng favicon của trang",
|
||||
"showImage": "Hiển thị hình ảnh",
|
||||
"description": "Mô tả",
|
||||
"color": "Màu sắc",
|
||||
"timezone": "Múi giờ",
|
||||
"invalidTimezone": "Múi giờ không hợp lệ. Hãy sử dụng tên IANA như America/New_York.",
|
||||
"showSeconds": "Hiển thị giây",
|
||||
"format12h": "12 giờ",
|
||||
"content": "Nội dung",
|
||||
"backgroundColor": "Màu nền",
|
||||
"links": "Liên kết",
|
||||
"addLink": "Thêm liên kết",
|
||||
"linkLabel": "Nhãn",
|
||||
"categoryLinks": "Liên kết",
|
||||
"categoryInfo": "Thông tin",
|
||||
"categorySystem": "Hệ thống",
|
||||
"copyLink": "Sao chép liên kết",
|
||||
"linkCopied": "Đã sao chép liên kết!",
|
||||
"location": "Vị trí",
|
||||
"temperatureUnit": "Đơn vị nhiệt độ",
|
||||
"showForecast": "Hiển thị dự báo 3 ngày",
|
||||
"scrolling": "Cho phép cuộn",
|
||||
"feedUrl": "URL nguồn cấp",
|
||||
"maxItems": "Số mục tối đa",
|
||||
"showDescription": "Hiển thị mô tả",
|
||||
"widgetRssName": "Nguồn RSS",
|
||||
"noHostSelected": "Chưa chọn máy chủ",
|
||||
"displayedMetrics": "Chỉ số hiển thị",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "Bộ nhớ",
|
||||
"metricDisk": "Đĩa",
|
||||
"metricUptime": "Thời gian hoạt động",
|
||||
"metricSystem": "Hệ thống",
|
||||
"metricNetwork": "Mạng",
|
||||
"metricProcesses": "Tiến trình",
|
||||
"categoryMonitoring": "Giám sát",
|
||||
"noPingUrls": "Chưa cấu hình URL",
|
||||
"pingLabel": "Nhãn",
|
||||
"addPingUrl": "Thêm URL",
|
||||
"showLatency": "Hiển thị độ trễ",
|
||||
"refreshInterval": "Chu kỳ làm mới",
|
||||
"seconds": "giây",
|
||||
"filterActivityTypes": "Loại hoạt động",
|
||||
"filterTypesHint": "Để trống để hiển thị tất cả",
|
||||
"showTimestamp": "Hiển thị dấu thời gian",
|
||||
"uptimeUnavailable": "Không có thời gian hoạt động",
|
||||
"uptimeLabel": "Thời gian hoạt động",
|
||||
"overviewVersion": "Phiên bản",
|
||||
"overviewUpdateLabel": "Cập nhật",
|
||||
"overviewUpdateAvailable": "Có bản cập nhật",
|
||||
"overviewDatabase": "Cơ sở dữ liệu",
|
||||
"overviewUptime": "Thời gian hoạt động",
|
||||
"noHosts": "Chưa cấu hình máy chủ nào",
|
||||
"hostGridHosts": "Máy chủ",
|
||||
"hostGridAllHint": "Để trống để hiển thị tất cả máy chủ",
|
||||
"columns": "Cột",
|
||||
"showIp": "Hiển thị địa chỉ IP",
|
||||
"layout": "Bố cục",
|
||||
"showStatus": "Hiển thị trạng thái",
|
||||
"noActivity": "Không có hoạt động",
|
||||
"widgetPingStatusName": "Trạng thái ping",
|
||||
"widgetRecentActivityName": "Hoạt động gần đây",
|
||||
"sshTerminalNoHost": "Chưa cấu hình máy chủ",
|
||||
"sshTerminalConnect": "Kết nối",
|
||||
"sshTerminalAutoConnect": "Tự động kết nối khi tải",
|
||||
"connectionTypes": "Loại kết nối",
|
||||
"calMon": "T2",
|
||||
"calTue": "T3",
|
||||
"calWed": "T4",
|
||||
"calThu": "T5",
|
||||
"calFri": "T6",
|
||||
"calSat": "T7",
|
||||
"calSun": "CN",
|
||||
"startOnMonday": "Bắt đầu tuần từ Thứ 2",
|
||||
"countdownNoDate": "Chưa đặt ngày mục tiêu",
|
||||
"countdownPast": "Sự kiện đã qua",
|
||||
"countdownDays": "ngày",
|
||||
"countdownHours": "giờ",
|
||||
"countdownMinutes": "phút",
|
||||
"countdownSeconds": "giây",
|
||||
"countdownLabel": "Nhãn",
|
||||
"countdownLabelPlaceholder": "Ví dụ: Ngày ra mắt",
|
||||
"countdownShowDays": "Hiển thị ngày",
|
||||
"countdownShowHours": "Hiển thị giờ",
|
||||
"targetDate": "Ngày mục tiêu",
|
||||
"searchEngine": "Công cụ tìm kiếm",
|
||||
"customSearchUrl": "URL tìm kiếm tùy chỉnh",
|
||||
"searchPlaceholder": "Tìm kiếm...",
|
||||
"searchGo": "Tìm",
|
||||
"searchPlaceholderLabel": "Văn bản gợi ý",
|
||||
"searchPlaceholderHint": "Văn bản hiển thị trong ô tìm kiếm",
|
||||
"openInNewTab": "Mở trong tab mới",
|
||||
"searchQueryPlaceholder": "Nhập truy vấn...",
|
||||
"noSearchShortcuts": "Chưa có phím tắt nào được cấu hình",
|
||||
"addSearchShortcut": "Thêm phím tắt",
|
||||
"fontSize": "Cỡ chữ",
|
||||
"textAlign": "Căn chỉnh văn bản",
|
||||
"fontWeight": "Độ đậm chữ",
|
||||
"clearColor": "Xóa màu",
|
||||
"imageFit": "Kiểu vừa ảnh",
|
||||
"imageLinkUrl": "URL liên kết",
|
||||
"noImage": "Chưa đặt URL hình ảnh",
|
||||
"altText": "Văn bản thay thế",
|
||||
"altTextPlaceholder": "Mô tả hình ảnh",
|
||||
"renderMarkdown": "Kết xuất Markdown",
|
||||
"displayMode": "Chế độ hiển thị",
|
||||
"displayField": "Trường hiển thị",
|
||||
"jsonPath": "Đường dẫn JSON",
|
||||
"customApiLabel": "Nhãn",
|
||||
"customApiLabelPlaceholder": "Ví dụ: Nhiệt độ",
|
||||
"customApiUnit": "Đơn vị",
|
||||
"customApiNoUrl": "Chưa cấu hình URL API",
|
||||
"customApiError": "Không tải được",
|
||||
"customApiNotArray": "Phản hồi không phải là một mảng",
|
||||
"addService": "Thêm dịch vụ",
|
||||
"showLabels": "Hiển thị nhãn",
|
||||
"iconSize": "Kích thước biểu tượng",
|
||||
"noDashboardLinks": "Chưa có liên kết bảng điều khiển",
|
||||
"noLimit": "Không giới hạn",
|
||||
"sectionHeading": "Tiêu đề phần",
|
||||
"addSection": "Thêm phần",
|
||||
"compactMode": "Chế độ thu gọn",
|
||||
"selectHosts": "Chọn máy chủ",
|
||||
"listLayout": "Danh sách",
|
||||
"gridLayout": "Lưới"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "首页"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "服务链接",
|
||||
"homepagePreview": "主页预览"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "服务链接",
|
||||
"serviceLinksEmpty": "暂无服务链接",
|
||||
"serviceLinksAdd": "添加",
|
||||
"serviceLinksLabelPlaceholder": "我的服务",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "输入有效的网址",
|
||||
"serviceLinksAddFailed": "添加服务链接失败"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "主页",
|
||||
"addWidget": "添加小部件",
|
||||
"editWidget": "编辑小部件",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "缩小",
|
||||
"resetView": "重置视图",
|
||||
"lockLayout": "锁定布局",
|
||||
"unlockLayout": "解锁布局",
|
||||
"noWidgets": "右键单击或点击 + 来添加第一个小部件",
|
||||
"openFullView": "打开完整视图",
|
||||
"previewTitle": "首页预览",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"title_label": "标题",
|
||||
"widgetTitlePlaceholder": "小组件标题(可选)",
|
||||
"url": "URL",
|
||||
"imageUrl": "自定义图片 URL",
|
||||
"imageUrlHint": "留空将自动使用网站图标",
|
||||
"showImage": "显示图片",
|
||||
"description": "描述",
|
||||
"color": "颜色",
|
||||
"timezone": "时区",
|
||||
"invalidTimezone": "时区无效。请使用 IANA 名称,例如 America/New_York。",
|
||||
"showSeconds": "显示秒",
|
||||
"format12h": "12 小时制",
|
||||
"content": "内容",
|
||||
"backgroundColor": "背景颜色",
|
||||
"links": "链接",
|
||||
"addLink": "添加链接",
|
||||
"linkLabel": "标签",
|
||||
"categoryLinks": "链接",
|
||||
"categoryInfo": "信息",
|
||||
"categorySystem": "系统",
|
||||
"copyLink": "复制链接",
|
||||
"linkCopied": "链接已复制!",
|
||||
"location": "位置",
|
||||
"temperatureUnit": "温度单位",
|
||||
"showForecast": "显示三日天气预报",
|
||||
"scrolling": "允许滚动",
|
||||
"feedUrl": "订阅源 URL",
|
||||
"maxItems": "最大条目数",
|
||||
"showDescription": "显示描述",
|
||||
"widgetRssName": "RSS 订阅源",
|
||||
"noHostSelected": "未选择主机",
|
||||
"displayedMetrics": "显示指标",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "内存",
|
||||
"metricDisk": "磁盘",
|
||||
"metricUptime": "运行时间",
|
||||
"metricSystem": "系统",
|
||||
"metricNetwork": "网络",
|
||||
"metricProcesses": "进程",
|
||||
"categoryMonitoring": "监控",
|
||||
"noPingUrls": "未配置 URL",
|
||||
"pingLabel": "标签",
|
||||
"addPingUrl": "添加 URL",
|
||||
"showLatency": "显示延迟",
|
||||
"refreshInterval": "刷新间隔",
|
||||
"seconds": "秒",
|
||||
"filterActivityTypes": "过滤类型",
|
||||
"filterTypesHint": "留空显示全部",
|
||||
"showTimestamp": "显示时间戳",
|
||||
"uptimeUnavailable": "运行时间不可用",
|
||||
"uptimeLabel": "运行时间",
|
||||
"overviewVersion": "版本",
|
||||
"overviewUpdateLabel": "更新",
|
||||
"overviewUpdateAvailable": "有可用更新",
|
||||
"overviewDatabase": "数据库",
|
||||
"overviewUptime": "运行时间",
|
||||
"noHosts": "未配置主机",
|
||||
"hostGridHosts": "主机",
|
||||
"hostGridAllHint": "留空显示所有主机",
|
||||
"columns": "列",
|
||||
"showIp": "显示 IP 地址",
|
||||
"layout": "布局",
|
||||
"showStatus": "显示状态",
|
||||
"noActivity": "无活动",
|
||||
"widgetPingStatusName": "Ping 状态",
|
||||
"widgetRecentActivityName": "近期活动",
|
||||
"sshTerminalNoHost": "未配置主机",
|
||||
"sshTerminalConnect": "连接",
|
||||
"sshTerminalAutoConnect": "加载时自动连接",
|
||||
"connectionTypes": "连接类型",
|
||||
"calMon": "一",
|
||||
"calTue": "二",
|
||||
"calWed": "三",
|
||||
"calThu": "四",
|
||||
"calFri": "五",
|
||||
"calSat": "六",
|
||||
"calSun": "日",
|
||||
"startOnMonday": "从周一开始",
|
||||
"countdownNoDate": "未设置目标日期",
|
||||
"countdownPast": "事件已过",
|
||||
"countdownDays": "天",
|
||||
"countdownHours": "小时",
|
||||
"countdownMinutes": "分钟",
|
||||
"countdownSeconds": "秒",
|
||||
"countdownLabel": "标签",
|
||||
"countdownLabelPlaceholder": "例如:发布日",
|
||||
"countdownShowDays": "显示天数",
|
||||
"countdownShowHours": "显示小时",
|
||||
"targetDate": "目标日期",
|
||||
"searchEngine": "搜索引擎",
|
||||
"customSearchUrl": "自定义搜索 URL",
|
||||
"searchPlaceholder": "搜索...",
|
||||
"searchGo": "搜索",
|
||||
"searchPlaceholderLabel": "占位符文本",
|
||||
"searchPlaceholderHint": "搜索输入框中显示的文本",
|
||||
"openInNewTab": "在新标签页中打开",
|
||||
"searchQueryPlaceholder": "输入查询...",
|
||||
"noSearchShortcuts": "未配置快捷方式",
|
||||
"addSearchShortcut": "添加快捷方式",
|
||||
"fontSize": "字体大小",
|
||||
"textAlign": "文本对齐",
|
||||
"fontWeight": "字体粗细",
|
||||
"clearColor": "清除颜色",
|
||||
"imageFit": "图片适应",
|
||||
"imageLinkUrl": "链接 URL",
|
||||
"noImage": "未设置图片 URL",
|
||||
"altText": "替代文本",
|
||||
"altTextPlaceholder": "描述图片",
|
||||
"renderMarkdown": "渲染 Markdown",
|
||||
"displayMode": "显示模式",
|
||||
"displayField": "显示字段",
|
||||
"jsonPath": "JSON 路径",
|
||||
"customApiLabel": "标签",
|
||||
"customApiLabelPlaceholder": "例如:温度",
|
||||
"customApiUnit": "单位",
|
||||
"customApiNoUrl": "未配置 API URL",
|
||||
"customApiError": "获取失败",
|
||||
"customApiNotArray": "响应不是数组",
|
||||
"addService": "添加服务",
|
||||
"showLabels": "显示标签",
|
||||
"iconSize": "图标大小",
|
||||
"noDashboardLinks": "未配置仪表板链接",
|
||||
"noLimit": "无限制",
|
||||
"sectionHeading": "分区标题",
|
||||
"addSection": "添加分区",
|
||||
"compactMode": "紧凑模式",
|
||||
"selectHosts": "选择主机",
|
||||
"listLayout": "列表",
|
||||
"gridLayout": "网格"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"nav": {
|
||||
"homepage": "首頁"
|
||||
},
|
||||
"dashboard": {
|
||||
"serviceLinks": "服務連結",
|
||||
"homepagePreview": "首頁預覽"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"serviceLinksTitle": "服務連結",
|
||||
"serviceLinksEmpty": "尚無服務連結",
|
||||
"serviceLinksAdd": "新增",
|
||||
"serviceLinksLabelPlaceholder": "我的服務",
|
||||
"serviceLinksUrlPlaceholder": "http://192.168.1.10:8080",
|
||||
"serviceLinksInvalidUrl": "輸入有效的網址",
|
||||
"serviceLinksAddFailed": "新增服務連結失敗"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "首頁",
|
||||
"addWidget": "新增小工具",
|
||||
"editWidget": "編輯小工具",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "縮小",
|
||||
"resetView": "重設檢視",
|
||||
"lockLayout": "鎖定佈局",
|
||||
"unlockLayout": "解鎖佈局",
|
||||
"noWidgets": "按右鍵或點擊 + 來新增您的第一個小工具",
|
||||
"openFullView": "開啟完整檢視",
|
||||
"previewTitle": "首頁預覽",
|
||||
"cancel": "取消",
|
||||
"save": "儲存",
|
||||
"title_label": "標題",
|
||||
"widgetTitlePlaceholder": "小工具標題(選填)",
|
||||
"url": "URL",
|
||||
"imageUrl": "自訂圖片 URL",
|
||||
"imageUrlHint": "留空以自動使用網站圖標",
|
||||
"showImage": "顯示圖片",
|
||||
"description": "描述",
|
||||
"color": "顏色",
|
||||
"timezone": "時區",
|
||||
"invalidTimezone": "時區無效。請使用 IANA 名稱,例如 America/New_York。",
|
||||
"showSeconds": "顯示秒數",
|
||||
"format12h": "12 小時制",
|
||||
"content": "內容",
|
||||
"backgroundColor": "背景顏色",
|
||||
"links": "連結",
|
||||
"addLink": "新增連結",
|
||||
"linkLabel": "標籤",
|
||||
"categoryLinks": "連結",
|
||||
"categoryInfo": "資訊",
|
||||
"categorySystem": "系統",
|
||||
"copyLink": "複製連結",
|
||||
"linkCopied": "連結已複製!",
|
||||
"location": "位置",
|
||||
"temperatureUnit": "溫度單位",
|
||||
"showForecast": "顯示 3 天天氣預報",
|
||||
"scrolling": "允許捲動",
|
||||
"feedUrl": "摘要來源 URL",
|
||||
"maxItems": "最大項目數",
|
||||
"showDescription": "顯示描述",
|
||||
"widgetRssName": "RSS 摘要",
|
||||
"noHostSelected": "未選取主機",
|
||||
"displayedMetrics": "顯示的指標",
|
||||
"metricCpu": "CPU",
|
||||
"metricMemory": "記憶體",
|
||||
"metricDisk": "磁碟",
|
||||
"metricUptime": "運作時間",
|
||||
"metricSystem": "系統",
|
||||
"metricNetwork": "網路",
|
||||
"metricProcesses": "程序",
|
||||
"categoryMonitoring": "監控",
|
||||
"noPingUrls": "未設定 URL",
|
||||
"pingLabel": "標籤",
|
||||
"addPingUrl": "新增 URL",
|
||||
"showLatency": "顯示延遲",
|
||||
"refreshInterval": "重新整理間隔",
|
||||
"seconds": "秒",
|
||||
"filterActivityTypes": "篩選類型",
|
||||
"filterTypesHint": "留空以顯示全部",
|
||||
"showTimestamp": "顯示時間戳記",
|
||||
"uptimeUnavailable": "運作時間無法取得",
|
||||
"uptimeLabel": "運作時間",
|
||||
"overviewVersion": "版本",
|
||||
"overviewUpdateLabel": "更新",
|
||||
"overviewUpdateAvailable": "有可用更新",
|
||||
"overviewDatabase": "資料庫",
|
||||
"overviewUptime": "運作時間",
|
||||
"noHosts": "未設定任何主機",
|
||||
"hostGridHosts": "主機",
|
||||
"hostGridAllHint": "留空以顯示所有主機",
|
||||
"columns": "欄",
|
||||
"showIp": "顯示 IP 位址",
|
||||
"layout": "版面配置",
|
||||
"showStatus": "顯示狀態",
|
||||
"noActivity": "無活動",
|
||||
"widgetPingStatusName": "Ping 狀態",
|
||||
"widgetRecentActivityName": "近期活動",
|
||||
"sshTerminalNoHost": "未設定主機",
|
||||
"sshTerminalConnect": "連線",
|
||||
"sshTerminalAutoConnect": "載入時自動連線",
|
||||
"connectionTypes": "連線類型",
|
||||
"calMon": "週一",
|
||||
"calTue": "週二",
|
||||
"calWed": "週三",
|
||||
"calThu": "週四",
|
||||
"calFri": "週五",
|
||||
"calSat": "週六",
|
||||
"calSun": "週日",
|
||||
"startOnMonday": "從週一開始",
|
||||
"countdownNoDate": "未設定目標日期",
|
||||
"countdownPast": "目標日期已過",
|
||||
"countdownDays": "天",
|
||||
"countdownHours": "小時",
|
||||
"countdownMinutes": "分鐘",
|
||||
"countdownSeconds": "秒",
|
||||
"countdownLabel": "標籤",
|
||||
"countdownLabelPlaceholder": "例如:發佈日",
|
||||
"countdownShowDays": "顯示天數",
|
||||
"countdownShowHours": "顯示小時",
|
||||
"targetDate": "目標日期",
|
||||
"searchEngine": "搜尋引擎",
|
||||
"customSearchUrl": "自訂搜尋 URL",
|
||||
"searchPlaceholder": "搜尋...",
|
||||
"searchGo": "搜尋",
|
||||
"searchPlaceholderLabel": "佔位文字",
|
||||
"searchPlaceholderHint": "顯示在搜尋輸入框內的文字",
|
||||
"openInNewTab": "在新分頁中開啟",
|
||||
"searchQueryPlaceholder": "輸入查詢...",
|
||||
"noSearchShortcuts": "尚未設定捷徑",
|
||||
"addSearchShortcut": "新增捷徑",
|
||||
"fontSize": "字型大小",
|
||||
"textAlign": "文字對齊",
|
||||
"fontWeight": "字型粗細",
|
||||
"clearColor": "清除顏色",
|
||||
"imageFit": "圖片填滿方式",
|
||||
"imageLinkUrl": "連結 URL",
|
||||
"noImage": "未設定圖片 URL",
|
||||
"altText": "替代文字",
|
||||
"altTextPlaceholder": "描述圖片",
|
||||
"renderMarkdown": "轉譯 Markdown",
|
||||
"displayMode": "顯示模式",
|
||||
"displayField": "顯示欄位",
|
||||
"jsonPath": "JSON 路徑",
|
||||
"customApiLabel": "標籤",
|
||||
"customApiLabelPlaceholder": "例如:溫度",
|
||||
"customApiUnit": "單位",
|
||||
"customApiNoUrl": "未設定 API URL",
|
||||
"customApiError": "擷取失敗",
|
||||
"customApiNotArray": "回應非陣列",
|
||||
"addService": "新增服務",
|
||||
"showLabels": "顯示標籤",
|
||||
"iconSize": "圖示大小",
|
||||
"noDashboardLinks": "未設定儀表板連結",
|
||||
"noLimit": "無限制",
|
||||
"sectionHeading": "區段標題",
|
||||
"addSection": "新增區段",
|
||||
"compactMode": "精簡模式",
|
||||
"selectHosts": "選取主機",
|
||||
"listLayout": "清單",
|
||||
"gridLayout": "格線"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"id": "homepage",
|
||||
"name": "Homepage",
|
||||
"version": "1.0.0",
|
||||
"description": "A customizable widget canvas and dashboard service links for your servers and services.",
|
||||
"author": {
|
||||
"name": "Termix"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/Termix-SSH/Termix",
|
||||
"category": "Productivity",
|
||||
"icon": "LayoutGrid",
|
||||
"engine": {
|
||||
"termix": ">=2.9.0",
|
||||
"api": "1"
|
||||
},
|
||||
"capabilities": ["db:own", "network:serve", "network:outbound", "ui:surface"],
|
||||
"provides": [
|
||||
{
|
||||
"service": "homepage.items",
|
||||
"version": "1.0.0",
|
||||
"permission": "homepage.use"
|
||||
}
|
||||
],
|
||||
"locales": "locales",
|
||||
"platforms": ["linux", "win32", "darwin"],
|
||||
"contributes": {
|
||||
"tabs": [
|
||||
{
|
||||
"id": "homepage",
|
||||
"titleKey": "nav.homepage",
|
||||
"icon": "LayoutGrid",
|
||||
"openFrom": ["palette"]
|
||||
}
|
||||
],
|
||||
"dashboardCards": [
|
||||
{ "id": "service_links", "titleKey": "dashboard.serviceLinks" },
|
||||
{ "id": "homepage_preview", "titleKey": "dashboard.homepagePreview" }
|
||||
],
|
||||
"permissions": [
|
||||
{
|
||||
"name": "use",
|
||||
"titleKey": "permissions.use.title",
|
||||
"descriptionKey": "permissions.use.description",
|
||||
"defaultRoles": ["admin", "user"]
|
||||
}
|
||||
],
|
||||
"actionSlots": [
|
||||
{ "id": "dashboard.secondaryView", "accepts": ["component"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
-- homepage 0001: adopt_homepage_tables
|
||||
-- Generated by termix-plugin migrations. Review before committing.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `homepage_items` (
|
||||
`id` int AUTO_INCREMENT PRIMARY KEY,
|
||||
`user_id` varchar(255) NOT NULL,
|
||||
`type_id` text NOT NULL,
|
||||
`title` text,
|
||||
`config` text NOT NULL DEFAULT ('{}'),
|
||||
`folder_id` int,
|
||||
`sync_id` varchar(255) UNIQUE,
|
||||
`created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
`updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
);
|
||||
RENAME TABLE `homepage_items` TO `p_homepage_homepage_items`;
|
||||
CREATE TABLE IF NOT EXISTS `homepage_layouts` (
|
||||
`id` int AUTO_INCREMENT PRIMARY KEY,
|
||||
`user_id` varchar(255) NOT NULL UNIQUE,
|
||||
`layout` text NOT NULL DEFAULT ('{}'),
|
||||
`updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
);
|
||||
RENAME TABLE `homepage_layouts` TO `p_homepage_homepage_layouts`;
|
||||
CREATE TABLE IF NOT EXISTS `dashboard_service_links` (
|
||||
`id` int AUTO_INCREMENT PRIMARY KEY,
|
||||
`user_id` varchar(255) NOT NULL,
|
||||
`label` text NOT NULL,
|
||||
`url` text NOT NULL,
|
||||
`order` int NOT NULL DEFAULT 0,
|
||||
`sync_id` varchar(255) UNIQUE,
|
||||
`created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
`updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
);
|
||||
RENAME TABLE `dashboard_service_links` TO `p_homepage_dashboard_service_links`;
|
||||
@@ -0,0 +1,38 @@
|
||||
-- homepage 0001: adopt_homepage_tables
|
||||
-- Generated by termix-plugin migrations. Review before committing.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "homepage_items" (
|
||||
"id" serial PRIMARY KEY,
|
||||
"user_id" varchar(255) NOT NULL,
|
||||
"type_id" text NOT NULL,
|
||||
"title" text,
|
||||
"config" text NOT NULL DEFAULT '{}',
|
||||
"folder_id" integer,
|
||||
"sync_id" varchar(255) UNIQUE,
|
||||
"created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS "idx_homepage_items_user_id" ON "homepage_items" ("user_id");
|
||||
ALTER TABLE "homepage_items" RENAME TO "p_homepage_homepage_items";
|
||||
CREATE TABLE IF NOT EXISTS "homepage_layouts" (
|
||||
"id" serial PRIMARY KEY,
|
||||
"user_id" varchar(255) NOT NULL UNIQUE,
|
||||
"layout" text NOT NULL DEFAULT '{}',
|
||||
"updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE
|
||||
);
|
||||
ALTER TABLE "homepage_layouts" RENAME TO "p_homepage_homepage_layouts";
|
||||
CREATE TABLE IF NOT EXISTS "dashboard_service_links" (
|
||||
"id" serial PRIMARY KEY,
|
||||
"user_id" varchar(255) NOT NULL,
|
||||
"label" text NOT NULL,
|
||||
"url" text NOT NULL,
|
||||
"order" integer NOT NULL DEFAULT 0,
|
||||
"sync_id" varchar(255) UNIQUE,
|
||||
"created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS "idx_dashboard_service_links_user_id" ON "dashboard_service_links" ("user_id");
|
||||
ALTER TABLE "dashboard_service_links" RENAME TO "p_homepage_dashboard_service_links";
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"version": 1,
|
||||
"tables": {
|
||||
"homepage_items": {
|
||||
"columns": {
|
||||
"id": {
|
||||
"type": "id",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"userId": {
|
||||
"type": "refUser",
|
||||
"length": 255,
|
||||
"notNull": true
|
||||
},
|
||||
"typeId": {
|
||||
"type": "text",
|
||||
"notNull": true
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"config": {
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"defaultValue": "{}"
|
||||
},
|
||||
"folderId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"syncId": {
|
||||
"type": "varchar",
|
||||
"length": 255,
|
||||
"unique": true
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "timestamp",
|
||||
"notNull": true,
|
||||
"defaultNow": true
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "timestamp",
|
||||
"notNull": true,
|
||||
"defaultNow": true
|
||||
}
|
||||
},
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_homepage_items_user_id",
|
||||
"columns": ["userId"]
|
||||
}
|
||||
],
|
||||
"adopts": "homepage_items"
|
||||
},
|
||||
"homepage_layouts": {
|
||||
"columns": {
|
||||
"id": {
|
||||
"type": "id",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"userId": {
|
||||
"type": "refUser",
|
||||
"length": 255,
|
||||
"notNull": true,
|
||||
"unique": true
|
||||
},
|
||||
"layout": {
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"defaultValue": "{}"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "timestamp",
|
||||
"notNull": true,
|
||||
"defaultNow": true
|
||||
}
|
||||
},
|
||||
"indexes": [],
|
||||
"adopts": "homepage_layouts"
|
||||
},
|
||||
"dashboard_service_links": {
|
||||
"columns": {
|
||||
"id": {
|
||||
"type": "id",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"userId": {
|
||||
"type": "refUser",
|
||||
"length": 255,
|
||||
"notNull": true
|
||||
},
|
||||
"label": {
|
||||
"type": "text",
|
||||
"notNull": true
|
||||
},
|
||||
"url": {
|
||||
"type": "text",
|
||||
"notNull": true
|
||||
},
|
||||
"order": {
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"defaultValue": 0
|
||||
},
|
||||
"syncId": {
|
||||
"type": "varchar",
|
||||
"length": 255,
|
||||
"unique": true
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "timestamp",
|
||||
"notNull": true,
|
||||
"defaultNow": true
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "timestamp",
|
||||
"notNull": true,
|
||||
"defaultNow": true
|
||||
}
|
||||
},
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_dashboard_service_links_user_id",
|
||||
"columns": ["userId"]
|
||||
}
|
||||
],
|
||||
"adopts": "dashboard_service_links"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
-- homepage 0001: adopt_homepage_tables
|
||||
-- Generated by termix-plugin migrations. Review before committing.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "homepage_items" (
|
||||
"id" integer PRIMARY KEY AUTOINCREMENT,
|
||||
"user_id" text NOT NULL,
|
||||
"type_id" text NOT NULL,
|
||||
"title" text,
|
||||
"config" text NOT NULL DEFAULT '{}',
|
||||
"folder_id" integer,
|
||||
"sync_id" text UNIQUE,
|
||||
"created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS "idx_homepage_items_user_id" ON "homepage_items" ("user_id");
|
||||
ALTER TABLE "homepage_items" RENAME TO "p_homepage_homepage_items";
|
||||
CREATE TABLE IF NOT EXISTS "homepage_layouts" (
|
||||
"id" integer PRIMARY KEY AUTOINCREMENT,
|
||||
"user_id" text NOT NULL UNIQUE,
|
||||
"layout" text NOT NULL DEFAULT '{}',
|
||||
"updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE
|
||||
);
|
||||
ALTER TABLE "homepage_layouts" RENAME TO "p_homepage_homepage_layouts";
|
||||
CREATE TABLE IF NOT EXISTS "dashboard_service_links" (
|
||||
"id" integer PRIMARY KEY AUTOINCREMENT,
|
||||
"user_id" text NOT NULL,
|
||||
"label" text NOT NULL,
|
||||
"url" text NOT NULL,
|
||||
"order" integer NOT NULL DEFAULT 0,
|
||||
"sync_id" text UNIQUE,
|
||||
"created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS "idx_dashboard_service_links_user_id" ON "dashboard_service_links" ("user_id");
|
||||
ALTER TABLE "dashboard_service_links" RENAME TO "p_homepage_dashboard_service_links";
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@termix-plugin/homepage",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "termix-plugin build",
|
||||
"test": "termix-plugin test",
|
||||
"typecheck": "tsc -p tsconfig.json",
|
||||
"validate": "termix-plugin validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@termix/plugin-sdk": "*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Router } from "express";
|
||||
import type { PluginContext } from "@termix/plugin-sdk/backend";
|
||||
import {
|
||||
homepageItems,
|
||||
homepageLayouts,
|
||||
dashboardServiceLinks,
|
||||
} from "./tables.js";
|
||||
import { createHomepageRepository } from "./repository.js";
|
||||
import { registerHomepageRoutes } from "./routes.js";
|
||||
import { registerOutboundRoutes } from "./outbound-routes.js";
|
||||
import { createHomepageItemsService } from "./services.js";
|
||||
|
||||
export type { HomepageItemsV1 } from "./services.js";
|
||||
|
||||
export async function activate(ctx: PluginContext) {
|
||||
const items = await ctx.db.define(homepageItems);
|
||||
const layouts = await ctx.db.define(homepageLayouts);
|
||||
const serviceLinks = await ctx.db.define(dashboardServiceLinks);
|
||||
const repo = createHomepageRepository(ctx.db, {
|
||||
items,
|
||||
layouts,
|
||||
serviceLinks,
|
||||
});
|
||||
|
||||
const router = ctx.http.router<Router>();
|
||||
registerHomepageRoutes(router, repo, ctx);
|
||||
registerOutboundRoutes(router, ctx);
|
||||
|
||||
ctx.sync.registerEntity({ type: "homepageItems", table: items, order: 80 });
|
||||
ctx.sync.registerEntity({
|
||||
type: "dashboardServiceLinks",
|
||||
table: serviceLinks,
|
||||
order: 70,
|
||||
});
|
||||
|
||||
ctx.services.provide("homepage.items", createHomepageItemsService(ctx, repo));
|
||||
|
||||
ctx.log.info("Homepage routes mounted at /plugin-api/homepage");
|
||||
}
|
||||
|
||||
export async function deactivate() {
|
||||
// Everything above was registered through ctx and is disposed by core.
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
import type { Request, Response, Router } from "express";
|
||||
import type { PluginContext } from "@termix/plugin-sdk/backend";
|
||||
|
||||
const FETCH_TIMEOUT_MS = 8000;
|
||||
const MAX_FAVICON_BYTES = 512 * 1024;
|
||||
const MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
||||
const MAX_CACHE_TTL_SECONDS = 24 * 60 * 60;
|
||||
|
||||
interface RssItem {
|
||||
title: string;
|
||||
link: string;
|
||||
pubDate: string | null;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
async function readBufferLimited(
|
||||
response: globalThis.Response,
|
||||
maxBytes: number,
|
||||
): Promise<Buffer> {
|
||||
const declaredLength = Number(response.headers.get("content-length"));
|
||||
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
||||
await response.body?.cancel().catch(() => {});
|
||||
throw new Error(`Response exceeds ${maxBytes} bytes`);
|
||||
}
|
||||
if (!response.body) return Buffer.alloc(0);
|
||||
const reader = response.body.getReader();
|
||||
const chunks: Uint8Array[] = [];
|
||||
let total = 0;
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
total += value.byteLength;
|
||||
if (total > maxBytes) {
|
||||
await reader.cancel().catch(() => {});
|
||||
throw new Error(`Response exceeds ${maxBytes} bytes`);
|
||||
}
|
||||
chunks.push(value);
|
||||
}
|
||||
return Buffer.concat(chunks, total);
|
||||
}
|
||||
|
||||
async function readTextLimited(
|
||||
response: globalThis.Response,
|
||||
maxBytes: number,
|
||||
): Promise<string> {
|
||||
const buffer = await readBufferLimited(response, maxBytes);
|
||||
return buffer.toString("utf8");
|
||||
}
|
||||
|
||||
function parseRss(xml: string): RssItem[] {
|
||||
const items: RssItem[] = [];
|
||||
const itemRegex = /<item[\s>]([\s\S]*?)<\/item>/gi;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
const getText = (tag: string, src: string): string | null => {
|
||||
const m = new RegExp(
|
||||
`<${tag}[^>]*><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/${tag}>|<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`,
|
||||
"i",
|
||||
).exec(src);
|
||||
if (!m) return null;
|
||||
return (m[1] ?? m[2]).trim();
|
||||
};
|
||||
|
||||
const getLink = (src: string): string => {
|
||||
// Self-closing <link rel="alternate" href="..." /> (BBC style)
|
||||
const selfClose = /<link[^>]+href="([^"]+)"[^>]*\/?>/i.exec(src);
|
||||
if (selfClose) return selfClose[1];
|
||||
// Plain text <link>url</link>
|
||||
return getText("link", src) ?? "";
|
||||
};
|
||||
|
||||
while ((match = itemRegex.exec(xml)) !== null) {
|
||||
const src = match[1];
|
||||
items.push({
|
||||
title: getText("title", src) ?? "(no title)",
|
||||
link: getLink(src),
|
||||
pubDate: getText("pubDate", src) ?? getText("updated", src),
|
||||
description: getText("description", src) ?? getText("summary", src),
|
||||
});
|
||||
if (items.length >= 50) break;
|
||||
}
|
||||
|
||||
// Atom feed fallback
|
||||
if (items.length === 0) {
|
||||
const entryRegex = /<entry[\s>]([\s\S]*?)<\/entry>/gi;
|
||||
while ((match = entryRegex.exec(xml)) !== null) {
|
||||
const src = match[1];
|
||||
const linkMatch = /<link[^>]+href="([^"]+)"/.exec(src);
|
||||
items.push({
|
||||
title: getText("title", src) ?? "(no title)",
|
||||
link: linkMatch?.[1] ?? "",
|
||||
pubDate: getText("published", src) ?? getText("updated", src),
|
||||
description: getText("summary", src) ?? getText("content", src),
|
||||
});
|
||||
if (items.length >= 50) break;
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* The favicon, RSS, ping and proxy routes all make outbound requests. Every
|
||||
* one goes through ctx.fetch, which keeps the existing safe-outbound rules
|
||||
* (http/https only, no embedded credentials, DNS pinned, private and
|
||||
* loopback addresses refused).
|
||||
*/
|
||||
export function registerOutboundRoutes(
|
||||
router: Router,
|
||||
ctx: PluginContext,
|
||||
): void {
|
||||
const faviconCache = new Map<
|
||||
string,
|
||||
{ data: Buffer; contentType: string; expires: number }
|
||||
>();
|
||||
const FAVICON_CACHE_SIZE = 100;
|
||||
const FAVICON_CACHE_TTL_MS = 1000 * 60 * 60 * 24;
|
||||
|
||||
const rssCache = new Map<string, { data: RssItem[]; expires: number }>();
|
||||
const RSS_CACHE_SIZE = 50;
|
||||
const RSS_CACHE_TTL_MS = 1000 * 60 * 15;
|
||||
|
||||
interface PingCacheEntry {
|
||||
ok: boolean;
|
||||
statusCode: number | null;
|
||||
latencyMs: number;
|
||||
expires: number;
|
||||
}
|
||||
const pingCache = new Map<string, PingCacheEntry>();
|
||||
const PING_CACHE_SIZE = 200;
|
||||
|
||||
const proxyCache = new Map<string, { data: unknown; expires: number }>();
|
||||
const PROXY_CACHE_SIZE = 50;
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/homepage/favicon:
|
||||
* get:
|
||||
* summary: Proxy favicon fetch
|
||||
* description: Fetches and caches a site favicon server-side to avoid CORS issues.
|
||||
* tags:
|
||||
* - Homepage
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: url
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Favicon image.
|
||||
* 400:
|
||||
* description: Invalid URL.
|
||||
* 500:
|
||||
* description: Failed to fetch favicon.
|
||||
*/
|
||||
router.get("/favicon", async (req: Request, res: Response) => {
|
||||
const rawUrl = req.query.url as string;
|
||||
if (!rawUrl) return res.status(400).json({ error: "url is required" });
|
||||
|
||||
let domain: string;
|
||||
try {
|
||||
domain = new URL(rawUrl).hostname;
|
||||
} catch {
|
||||
return res.status(400).json({ error: "Invalid URL" });
|
||||
}
|
||||
|
||||
const cached = faviconCache.get(domain);
|
||||
if (cached && cached.expires > Date.now()) {
|
||||
res.setHeader("Content-Type", cached.contentType);
|
||||
res.setHeader("Cache-Control", "public, max-age=86400");
|
||||
return res.send(cached.data);
|
||||
}
|
||||
|
||||
const faviconUrl = `https://www.google.com/s2/favicons?sz=64&domain_url=${encodeURIComponent(domain)}`;
|
||||
|
||||
try {
|
||||
const response = await ctx.fetch(faviconUrl, {
|
||||
timeoutMs: FETCH_TIMEOUT_MS,
|
||||
});
|
||||
if (!response.ok)
|
||||
throw new Error(`Favicon fetch failed: ${response.status}`);
|
||||
const data = await readBufferLimited(response, MAX_FAVICON_BYTES);
|
||||
const contentType =
|
||||
response.headers.get("content-type") || "image/x-icon";
|
||||
|
||||
if (faviconCache.size >= FAVICON_CACHE_SIZE) {
|
||||
const oldest = faviconCache.keys().next().value;
|
||||
if (oldest) faviconCache.delete(oldest);
|
||||
}
|
||||
faviconCache.set(domain, {
|
||||
data,
|
||||
contentType,
|
||||
expires: Date.now() + FAVICON_CACHE_TTL_MS,
|
||||
});
|
||||
res.setHeader("Content-Type", contentType);
|
||||
res.setHeader("Cache-Control", "public, max-age=86400");
|
||||
res.send(data);
|
||||
} catch {
|
||||
ctx.log.warn(`Failed to fetch favicon for ${domain}`);
|
||||
res.status(500).json({ error: "Failed to fetch favicon" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/homepage/rss:
|
||||
* get:
|
||||
* summary: Proxy and parse an RSS/Atom feed
|
||||
* tags:
|
||||
* - Homepage
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: url
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: query
|
||||
* name: max
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Array of feed items.
|
||||
* 400:
|
||||
* description: Invalid or missing URL.
|
||||
* 500:
|
||||
* description: Failed to fetch or parse the feed.
|
||||
*/
|
||||
router.get("/rss", async (req: Request, res: Response) => {
|
||||
const feedUrl = req.query.url as string;
|
||||
const max = Math.min(50, Math.max(1, Number(req.query.max) || 10));
|
||||
|
||||
if (!feedUrl) return res.status(400).json({ error: "url is required" });
|
||||
try {
|
||||
new URL(feedUrl);
|
||||
} catch {
|
||||
return res.status(400).json({ error: "Invalid URL" });
|
||||
}
|
||||
|
||||
const cached = rssCache.get(feedUrl);
|
||||
if (cached && cached.expires > Date.now()) {
|
||||
return res.json(cached.data.slice(0, max));
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await ctx.fetch(feedUrl, {
|
||||
timeoutMs: FETCH_TIMEOUT_MS,
|
||||
});
|
||||
if (!response.ok) throw new Error(`RSS fetch failed: ${response.status}`);
|
||||
const xml = await readTextLimited(response, MAX_RESPONSE_BYTES);
|
||||
const items = parseRss(xml);
|
||||
|
||||
if (rssCache.size >= RSS_CACHE_SIZE) {
|
||||
const oldest = rssCache.keys().next().value;
|
||||
if (oldest) rssCache.delete(oldest);
|
||||
}
|
||||
rssCache.set(feedUrl, {
|
||||
data: items,
|
||||
expires: Date.now() + RSS_CACHE_TTL_MS,
|
||||
});
|
||||
|
||||
res.json(items.slice(0, max));
|
||||
} catch {
|
||||
ctx.log.warn(`Failed to fetch RSS feed ${feedUrl}`);
|
||||
res.status(500).json({ error: "Failed to fetch feed" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/homepage/ping:
|
||||
* get:
|
||||
* summary: Check the HTTP reachability and latency of a URL
|
||||
* tags:
|
||||
* - Homepage
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: url
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: query
|
||||
* name: ttl
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Cache TTL in seconds (min 10)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Ping result with ok, statusCode and latencyMs.
|
||||
* 400:
|
||||
* description: Invalid or missing URL.
|
||||
*/
|
||||
router.get("/ping", async (req: Request, res: Response) => {
|
||||
let targetUrl = req.query.url as string;
|
||||
const ttl = Math.max(10, Number(req.query.ttl) || 30) * 1000;
|
||||
|
||||
if (!targetUrl) return res.status(400).json({ error: "url is required" });
|
||||
if (!/^https?:\/\//i.test(targetUrl)) targetUrl = `https://${targetUrl}`;
|
||||
try {
|
||||
new URL(targetUrl);
|
||||
} catch {
|
||||
return res.status(400).json({ error: "Invalid URL" });
|
||||
}
|
||||
|
||||
const cached = pingCache.get(targetUrl);
|
||||
if (cached && cached.expires > Date.now()) {
|
||||
return res.json({
|
||||
ok: cached.ok,
|
||||
statusCode: cached.statusCode,
|
||||
latencyMs: cached.latencyMs,
|
||||
});
|
||||
}
|
||||
|
||||
const requestStatus = async (
|
||||
url: string,
|
||||
method: "HEAD" | "GET",
|
||||
): Promise<number | null> => {
|
||||
const response = await ctx.fetch(url, {
|
||||
method,
|
||||
timeoutMs: FETCH_TIMEOUT_MS,
|
||||
});
|
||||
await response.body?.cancel().catch(() => {});
|
||||
return response.status ?? null;
|
||||
};
|
||||
|
||||
const start = performance.now();
|
||||
try {
|
||||
let code = await requestStatus(targetUrl, "HEAD");
|
||||
if (code === 405) {
|
||||
code = await requestStatus(targetUrl, "GET");
|
||||
}
|
||||
const result = {
|
||||
ok: code !== null && code < 400,
|
||||
statusCode: code,
|
||||
latencyMs: Math.round(performance.now() - start),
|
||||
};
|
||||
if (pingCache.size >= PING_CACHE_SIZE) {
|
||||
const oldest = pingCache.keys().next().value;
|
||||
if (oldest) pingCache.delete(oldest);
|
||||
}
|
||||
pingCache.set(targetUrl, { ...result, expires: Date.now() + ttl });
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
ctx.log.warn(`Ping failed for ${targetUrl}: ${String(err)}`);
|
||||
res.status(500).json({ error: "Ping failed" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/homepage/proxy:
|
||||
* get:
|
||||
* summary: Proxy a JSON API URL and return the parsed response
|
||||
* tags:
|
||||
* - Homepage
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: url
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* - in: query
|
||||
* name: ttl
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: Cache TTL in seconds (min 10, default 60)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The JSON body returned by the target URL.
|
||||
* 400:
|
||||
* description: Invalid or missing URL, or non-JSON response.
|
||||
* 500:
|
||||
* description: Failed to fetch the target URL.
|
||||
*/
|
||||
router.get("/proxy", async (req: Request, res: Response) => {
|
||||
const targetUrl = req.query.url as string;
|
||||
const ttl =
|
||||
Math.min(
|
||||
MAX_CACHE_TTL_SECONDS,
|
||||
Math.max(10, Number(req.query.ttl) || 60),
|
||||
) * 1000;
|
||||
|
||||
if (!targetUrl) return res.status(400).json({ error: "url is required" });
|
||||
try {
|
||||
new URL(targetUrl);
|
||||
} catch {
|
||||
return res.status(400).json({ error: "Invalid URL" });
|
||||
}
|
||||
|
||||
const cached = proxyCache.get(targetUrl);
|
||||
if (cached && cached.expires > Date.now()) {
|
||||
return res.json(cached.data);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await ctx.fetch(targetUrl, {
|
||||
timeoutMs: FETCH_TIMEOUT_MS,
|
||||
});
|
||||
if (!response.ok)
|
||||
throw new Error(`Proxy fetch failed: ${response.status}`);
|
||||
const text = await readTextLimited(response, MAX_RESPONSE_BYTES);
|
||||
let data: unknown;
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch {
|
||||
throw new Error("Response is not valid JSON");
|
||||
}
|
||||
|
||||
if (proxyCache.size >= PROXY_CACHE_SIZE) {
|
||||
const oldest = proxyCache.keys().next().value;
|
||||
if (oldest) proxyCache.delete(oldest);
|
||||
}
|
||||
proxyCache.set(targetUrl, { data, expires: Date.now() + ttl });
|
||||
res.json(data);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
ctx.log.warn(`Proxy fetch failed for ${targetUrl}: ${msg}`);
|
||||
if (msg.includes("not valid JSON")) {
|
||||
return res.status(400).json({ error: "Response is not valid JSON" });
|
||||
}
|
||||
res.status(500).json({ error: "Failed to fetch URL" });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { and, asc, eq } from "drizzle-orm";
|
||||
import type { PluginDatabase } from "@termix/plugin-sdk/backend";
|
||||
|
||||
export interface HomepageItemRecord {
|
||||
id: number;
|
||||
userId: string;
|
||||
typeId: string;
|
||||
title: string | null;
|
||||
config: string;
|
||||
folderId: number | null;
|
||||
syncId: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface HomepageLayoutRecord {
|
||||
id: number;
|
||||
userId: string;
|
||||
layout: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ServiceLinkRecord {
|
||||
id: number;
|
||||
userId: string;
|
||||
label: string;
|
||||
url: string;
|
||||
order: number;
|
||||
syncId: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// Tables come from ctx.db.define, which the SDK hands back untyped, and the
|
||||
// drizzle handle is the server's own. Typed at this module's edge instead.
|
||||
type Table = any;
|
||||
type Drizzle = any;
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export type HomepageRepository = ReturnType<typeof createHomepageRepository>;
|
||||
|
||||
export function createHomepageRepository(
|
||||
db: PluginDatabase,
|
||||
tables: { items: Table; layouts: Table; serviceLinks: Table },
|
||||
) {
|
||||
const client = () => db.client<Drizzle>();
|
||||
|
||||
async function findItemById(
|
||||
userId: string,
|
||||
id: number,
|
||||
): Promise<HomepageItemRecord | null> {
|
||||
const drizzle = await client();
|
||||
const rows = await drizzle
|
||||
.select()
|
||||
.from(tables.items)
|
||||
.where(and(eq(tables.items.id, id), eq(tables.items.userId, userId)))
|
||||
.limit(1);
|
||||
return (rows[0] as HomepageItemRecord) ?? null;
|
||||
}
|
||||
|
||||
async function findItemBySyncId(syncId: string): Promise<HomepageItemRecord> {
|
||||
const drizzle = await client();
|
||||
const rows = await drizzle
|
||||
.select()
|
||||
.from(tables.items)
|
||||
.where(eq(tables.items.syncId, syncId))
|
||||
.limit(1);
|
||||
return rows[0] as HomepageItemRecord;
|
||||
}
|
||||
|
||||
async function findServiceLinkById(
|
||||
userId: string,
|
||||
id: number,
|
||||
): Promise<ServiceLinkRecord | null> {
|
||||
const drizzle = await client();
|
||||
const rows = await drizzle
|
||||
.select()
|
||||
.from(tables.serviceLinks)
|
||||
.where(
|
||||
and(
|
||||
eq(tables.serviceLinks.id, id),
|
||||
eq(tables.serviceLinks.userId, userId),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
return (rows[0] as ServiceLinkRecord) ?? null;
|
||||
}
|
||||
|
||||
async function findServiceLinkBySyncId(
|
||||
syncId: string,
|
||||
): Promise<ServiceLinkRecord> {
|
||||
const drizzle = await client();
|
||||
const rows = await drizzle
|
||||
.select()
|
||||
.from(tables.serviceLinks)
|
||||
.where(eq(tables.serviceLinks.syncId, syncId))
|
||||
.limit(1);
|
||||
return rows[0] as ServiceLinkRecord;
|
||||
}
|
||||
|
||||
return {
|
||||
findItemById,
|
||||
|
||||
async listItemsByUser(userId: string): Promise<HomepageItemRecord[]> {
|
||||
const drizzle = await client();
|
||||
return drizzle
|
||||
.select()
|
||||
.from(tables.items)
|
||||
.where(eq(tables.items.userId, userId));
|
||||
},
|
||||
|
||||
async createItem(
|
||||
userId: string,
|
||||
input: { typeId: string; title: string | null; config: string },
|
||||
): Promise<HomepageItemRecord> {
|
||||
const syncId = randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
const drizzle = await client();
|
||||
await drizzle.insert(tables.items).values({
|
||||
userId,
|
||||
typeId: input.typeId,
|
||||
title: input.title,
|
||||
config: input.config,
|
||||
syncId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
await db.persist();
|
||||
return findItemBySyncId(syncId);
|
||||
},
|
||||
|
||||
async updateItem(
|
||||
userId: string,
|
||||
id: number,
|
||||
updates: { title?: string | null; config?: string },
|
||||
): Promise<HomepageItemRecord | null> {
|
||||
const existing = await findItemById(userId, id);
|
||||
if (!existing) return null;
|
||||
const drizzle = await client();
|
||||
await drizzle
|
||||
.update(tables.items)
|
||||
.set({ ...updates, updatedAt: new Date().toISOString() })
|
||||
.where(and(eq(tables.items.id, id), eq(tables.items.userId, userId)));
|
||||
await db.persist();
|
||||
return findItemById(userId, id);
|
||||
},
|
||||
|
||||
async deleteItem(
|
||||
userId: string,
|
||||
id: number,
|
||||
): Promise<HomepageItemRecord | null> {
|
||||
const existing = await findItemById(userId, id);
|
||||
if (!existing) return null;
|
||||
const drizzle = await client();
|
||||
await drizzle
|
||||
.delete(tables.items)
|
||||
.where(and(eq(tables.items.id, id), eq(tables.items.userId, userId)));
|
||||
await db.persist();
|
||||
return existing;
|
||||
},
|
||||
|
||||
async findLayoutByUser(
|
||||
userId: string,
|
||||
): Promise<HomepageLayoutRecord | null> {
|
||||
const drizzle = await client();
|
||||
const rows = await drizzle
|
||||
.select()
|
||||
.from(tables.layouts)
|
||||
.where(eq(tables.layouts.userId, userId))
|
||||
.limit(1);
|
||||
return (rows[0] as HomepageLayoutRecord) ?? null;
|
||||
},
|
||||
|
||||
async upsertLayout(
|
||||
userId: string,
|
||||
layout: string,
|
||||
now: string,
|
||||
): Promise<HomepageLayoutRecord> {
|
||||
const drizzle = await client();
|
||||
const existing = await drizzle
|
||||
.select()
|
||||
.from(tables.layouts)
|
||||
.where(eq(tables.layouts.userId, userId))
|
||||
.limit(1);
|
||||
|
||||
if (existing[0]) {
|
||||
await drizzle
|
||||
.update(tables.layouts)
|
||||
.set({ layout, updatedAt: now })
|
||||
.where(eq(tables.layouts.userId, userId));
|
||||
} else {
|
||||
await drizzle.insert(tables.layouts).values({
|
||||
userId,
|
||||
layout,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
await db.persist();
|
||||
const rows = await drizzle
|
||||
.select()
|
||||
.from(tables.layouts)
|
||||
.where(eq(tables.layouts.userId, userId))
|
||||
.limit(1);
|
||||
return rows[0] as HomepageLayoutRecord;
|
||||
},
|
||||
|
||||
findServiceLinkById,
|
||||
|
||||
async listServiceLinksByUser(userId: string): Promise<ServiceLinkRecord[]> {
|
||||
const drizzle = await client();
|
||||
return drizzle
|
||||
.select()
|
||||
.from(tables.serviceLinks)
|
||||
.where(eq(tables.serviceLinks.userId, userId))
|
||||
.orderBy(asc(tables.serviceLinks.order), asc(tables.serviceLinks.id));
|
||||
},
|
||||
|
||||
async createServiceLink(
|
||||
userId: string,
|
||||
input: { label: string; url: string },
|
||||
): Promise<ServiceLinkRecord> {
|
||||
const drizzle = await client();
|
||||
const existing = await drizzle
|
||||
.select({ order: tables.serviceLinks.order })
|
||||
.from(tables.serviceLinks)
|
||||
.where(eq(tables.serviceLinks.userId, userId))
|
||||
.orderBy(asc(tables.serviceLinks.order));
|
||||
const nextOrder =
|
||||
existing.length > 0 ? existing[existing.length - 1].order + 1 : 0;
|
||||
|
||||
const syncId = randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
await drizzle.insert(tables.serviceLinks).values({
|
||||
userId,
|
||||
label: input.label,
|
||||
url: input.url,
|
||||
order: nextOrder,
|
||||
syncId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
await db.persist();
|
||||
return findServiceLinkBySyncId(syncId);
|
||||
},
|
||||
|
||||
async updateServiceLink(
|
||||
userId: string,
|
||||
id: number,
|
||||
updates: { label?: string; url?: string },
|
||||
): Promise<ServiceLinkRecord | null> {
|
||||
const existing = await findServiceLinkById(userId, id);
|
||||
if (!existing) return null;
|
||||
const drizzle = await client();
|
||||
await drizzle
|
||||
.update(tables.serviceLinks)
|
||||
.set({ ...updates, updatedAt: new Date().toISOString() })
|
||||
.where(
|
||||
and(
|
||||
eq(tables.serviceLinks.id, id),
|
||||
eq(tables.serviceLinks.userId, userId),
|
||||
),
|
||||
);
|
||||
await db.persist();
|
||||
return findServiceLinkById(userId, id);
|
||||
},
|
||||
|
||||
async deleteServiceLink(
|
||||
userId: string,
|
||||
id: number,
|
||||
): Promise<ServiceLinkRecord | null> {
|
||||
const existing = await findServiceLinkById(userId, id);
|
||||
if (!existing) return null;
|
||||
const drizzle = await client();
|
||||
await drizzle
|
||||
.delete(tables.serviceLinks)
|
||||
.where(
|
||||
and(
|
||||
eq(tables.serviceLinks.id, id),
|
||||
eq(tables.serviceLinks.userId, userId),
|
||||
),
|
||||
);
|
||||
await db.persist();
|
||||
return existing;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
import type { Request, Response, Router } from "express";
|
||||
import type { PluginContext } from "@termix/plugin-sdk/backend";
|
||||
import type { HomepageRepository } from "./repository.js";
|
||||
import { isValidServiceLinkUrl, normalizeServiceLinkUrl } from "./url.js";
|
||||
|
||||
function actor(ctx: PluginContext): string {
|
||||
// Core's plugin router authenticates every request and runs it as that user.
|
||||
return ctx.currentActor() as string;
|
||||
}
|
||||
|
||||
function parseId(raw: unknown): number | null {
|
||||
const id = typeof raw === "string" ? parseInt(raw, 10) : NaN;
|
||||
return Number.isInteger(id) ? id : null;
|
||||
}
|
||||
|
||||
function isNonEmptyString(val: unknown): val is string {
|
||||
return typeof val === "string" && val.trim().length > 0;
|
||||
}
|
||||
|
||||
function logError(
|
||||
ctx: PluginContext,
|
||||
message: string,
|
||||
error: unknown,
|
||||
operation: string,
|
||||
): void {
|
||||
ctx.log.error(
|
||||
`${message} (${operation})`,
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mounts the homepage routes on the plugin's router, served at
|
||||
* /plugin-api/homepage with core auth in front.
|
||||
*/
|
||||
export function registerHomepageRoutes(
|
||||
router: Router,
|
||||
repo: HomepageRepository,
|
||||
ctx: PluginContext,
|
||||
): void {
|
||||
router.use(ctx.rbac.require("use") as never);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/homepage/items:
|
||||
* get:
|
||||
* summary: Get homepage items
|
||||
* description: Returns all homepage widget items for the authenticated user.
|
||||
* tags:
|
||||
* - Homepage
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of homepage items.
|
||||
* 500:
|
||||
* description: Failed to fetch homepage items.
|
||||
*/
|
||||
router.get("/items", async (req: Request, res: Response) => {
|
||||
const userId = actor(ctx);
|
||||
try {
|
||||
const items = await repo.listItemsByUser(userId);
|
||||
res.json(items);
|
||||
} catch (err) {
|
||||
logError(
|
||||
ctx,
|
||||
"Failed to fetch homepage items",
|
||||
err,
|
||||
"homepage_items_list_failed",
|
||||
);
|
||||
res.status(500).json({ error: "Failed to fetch homepage items" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/homepage/items:
|
||||
* post:
|
||||
* summary: Create homepage item
|
||||
* description: Creates a new homepage widget item for the authenticated user.
|
||||
* tags:
|
||||
* - Homepage
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - typeId
|
||||
* properties:
|
||||
* typeId:
|
||||
* type: string
|
||||
* title:
|
||||
* type: string
|
||||
* config:
|
||||
* type: object
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Homepage item created.
|
||||
* 400:
|
||||
* description: Invalid data.
|
||||
* 500:
|
||||
* description: Failed to create homepage item.
|
||||
*/
|
||||
router.post("/items", async (req: Request, res: Response) => {
|
||||
const userId = actor(ctx);
|
||||
const { typeId, title, config } = req.body ?? {};
|
||||
|
||||
if (!isNonEmptyString(typeId)) {
|
||||
return res.status(400).json({ error: "typeId is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const created = await repo.createItem(userId, {
|
||||
typeId,
|
||||
title: title ?? null,
|
||||
config: config ? JSON.stringify(config) : "{}",
|
||||
});
|
||||
res.status(201).json(created);
|
||||
} catch (err) {
|
||||
logError(
|
||||
ctx,
|
||||
"Failed to create homepage item",
|
||||
err,
|
||||
"homepage_item_create_failed",
|
||||
);
|
||||
res.status(500).json({ error: "Failed to create homepage item" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/homepage/items/{id}:
|
||||
* put:
|
||||
* summary: Update homepage item
|
||||
* description: Updates a homepage widget item's title or config.
|
||||
* tags:
|
||||
* - Homepage
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* title:
|
||||
* type: string
|
||||
* config:
|
||||
* type: object
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Homepage item updated.
|
||||
* 400:
|
||||
* description: Invalid id.
|
||||
* 404:
|
||||
* description: Not found.
|
||||
* 500:
|
||||
* description: Failed to update homepage item.
|
||||
*/
|
||||
router.put("/items/:id", async (req: Request, res: Response) => {
|
||||
const userId = actor(ctx);
|
||||
const id = parseId(req.params.id);
|
||||
if (id === null) return res.status(400).json({ error: "Invalid id" });
|
||||
|
||||
const { title, config } = req.body ?? {};
|
||||
const updates: { title?: string | null; config?: string } = {};
|
||||
if (title !== undefined) updates.title = title;
|
||||
if (config !== undefined) updates.config = JSON.stringify(config);
|
||||
|
||||
try {
|
||||
const updated = await repo.updateItem(userId, id, updates);
|
||||
if (!updated) return res.status(404).json({ error: "Not found" });
|
||||
res.json(updated);
|
||||
} catch (err) {
|
||||
logError(
|
||||
ctx,
|
||||
"Failed to update homepage item",
|
||||
err,
|
||||
"homepage_item_update_failed",
|
||||
);
|
||||
res.status(500).json({ error: "Failed to update homepage item" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/homepage/items/{id}:
|
||||
* delete:
|
||||
* summary: Delete homepage item
|
||||
* description: Deletes a homepage widget item.
|
||||
* tags:
|
||||
* - Homepage
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Homepage item deleted.
|
||||
* 400:
|
||||
* description: Invalid id.
|
||||
* 404:
|
||||
* description: Not found.
|
||||
* 500:
|
||||
* description: Failed to delete homepage item.
|
||||
*/
|
||||
router.delete("/items/:id", async (req: Request, res: Response) => {
|
||||
const userId = actor(ctx);
|
||||
const id = parseId(req.params.id);
|
||||
if (id === null) return res.status(400).json({ error: "Invalid id" });
|
||||
|
||||
try {
|
||||
const deleted = await repo.deleteItem(userId, id);
|
||||
if (!deleted) return res.status(404).json({ error: "Not found" });
|
||||
await ctx.sync.recordTombstone(userId, "homepageItems", deleted.syncId);
|
||||
res.json({ message: "Homepage item deleted" });
|
||||
} catch (err) {
|
||||
logError(
|
||||
ctx,
|
||||
"Failed to delete homepage item",
|
||||
err,
|
||||
"homepage_item_delete_failed",
|
||||
);
|
||||
res.status(500).json({ error: "Failed to delete homepage item" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/homepage/layout:
|
||||
* get:
|
||||
* summary: Get homepage layout
|
||||
* description: Returns the homepage canvas layout (widget positions, pan, zoom) for the authenticated user.
|
||||
* tags:
|
||||
* - Homepage
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Layout data or null.
|
||||
* 500:
|
||||
* description: Failed to fetch homepage layout.
|
||||
*/
|
||||
router.get("/layout", async (req: Request, res: Response) => {
|
||||
const userId = actor(ctx);
|
||||
try {
|
||||
const row = await repo.findLayoutByUser(userId);
|
||||
if (!row) return res.json(null);
|
||||
const parsed = JSON.parse(row.layout || "{}");
|
||||
res.json({ ...row, layout: parsed });
|
||||
} catch (err) {
|
||||
logError(
|
||||
ctx,
|
||||
"Failed to fetch homepage layout",
|
||||
err,
|
||||
"homepage_layout_get_failed",
|
||||
);
|
||||
res.status(500).json({ error: "Failed to fetch homepage layout" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/homepage/layout:
|
||||
* put:
|
||||
* summary: Save homepage layout
|
||||
* description: Saves or updates the homepage canvas layout for the authenticated user.
|
||||
* tags:
|
||||
* - Homepage
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* entries:
|
||||
* type: array
|
||||
* pan:
|
||||
* type: object
|
||||
* zoom:
|
||||
* type: number
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Layout saved.
|
||||
* 500:
|
||||
* description: Failed to save homepage layout.
|
||||
*/
|
||||
router.put("/layout", async (req: Request, res: Response) => {
|
||||
const userId = actor(ctx);
|
||||
try {
|
||||
const layoutJson = JSON.stringify(req.body ?? {});
|
||||
const now = new Date().toISOString();
|
||||
const updated = await repo.upsertLayout(userId, layoutJson, now);
|
||||
const parsed = JSON.parse(updated.layout);
|
||||
res.json({ ...updated, layout: parsed });
|
||||
} catch (err) {
|
||||
logError(
|
||||
ctx,
|
||||
"Failed to save homepage layout",
|
||||
err,
|
||||
"homepage_layout_save_failed",
|
||||
);
|
||||
res.status(500).json({ error: "Failed to save homepage layout" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/homepage/service-links:
|
||||
* get:
|
||||
* summary: Get service links
|
||||
* description: Returns all dashboard service links for the authenticated user.
|
||||
* tags:
|
||||
* - Homepage
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of service links.
|
||||
* 500:
|
||||
* description: Failed to fetch service links.
|
||||
*/
|
||||
router.get("/service-links", async (req: Request, res: Response) => {
|
||||
const userId = actor(ctx);
|
||||
try {
|
||||
const links = await repo.listServiceLinksByUser(userId);
|
||||
res.json(links);
|
||||
} catch (err) {
|
||||
logError(
|
||||
ctx,
|
||||
"Failed to fetch service links",
|
||||
err,
|
||||
"service_links_list_failed",
|
||||
);
|
||||
res.status(500).json({ error: "Failed to fetch service links" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/homepage/service-links:
|
||||
* post:
|
||||
* summary: Create service link
|
||||
* description: Creates a new dashboard service link for the authenticated user.
|
||||
* tags:
|
||||
* - Homepage
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - label
|
||||
* - url
|
||||
* properties:
|
||||
* label:
|
||||
* type: string
|
||||
* url:
|
||||
* type: string
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Service link created.
|
||||
* 400:
|
||||
* description: Invalid data.
|
||||
* 500:
|
||||
* description: Failed to create service link.
|
||||
*/
|
||||
router.post("/service-links", async (req: Request, res: Response) => {
|
||||
const userId = actor(ctx);
|
||||
const { label, url } = req.body ?? {};
|
||||
|
||||
if (!isNonEmptyString(label) || !isNonEmptyString(url)) {
|
||||
return res.status(400).json({ error: "label and url are required" });
|
||||
}
|
||||
const normalizedUrl = normalizeServiceLinkUrl(url);
|
||||
if (!isValidServiceLinkUrl(normalizedUrl)) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "url must be a valid http or https URL" });
|
||||
}
|
||||
|
||||
try {
|
||||
const created = await repo.createServiceLink(userId, {
|
||||
label: label.trim(),
|
||||
url: normalizedUrl,
|
||||
});
|
||||
res.status(201).json(created);
|
||||
} catch (err) {
|
||||
logError(
|
||||
ctx,
|
||||
"Failed to create service link",
|
||||
err,
|
||||
"service_link_create_failed",
|
||||
);
|
||||
res.status(500).json({ error: "Failed to create service link" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/homepage/service-links/{id}:
|
||||
* put:
|
||||
* summary: Update service link
|
||||
* description: Updates label or url of a dashboard service link.
|
||||
* tags:
|
||||
* - Homepage
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* label:
|
||||
* type: string
|
||||
* url:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Service link updated.
|
||||
* 400:
|
||||
* description: Invalid data.
|
||||
* 404:
|
||||
* description: Not found.
|
||||
* 500:
|
||||
* description: Failed to update service link.
|
||||
*/
|
||||
router.put("/service-links/:id", async (req: Request, res: Response) => {
|
||||
const userId = actor(ctx);
|
||||
const id = parseId(req.params.id);
|
||||
if (id === null) return res.status(400).json({ error: "Invalid id" });
|
||||
|
||||
const { label, url } = req.body ?? {};
|
||||
if (
|
||||
url !== undefined &&
|
||||
!isValidServiceLinkUrl(normalizeServiceLinkUrl(url))
|
||||
) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "url must be a valid http or https URL" });
|
||||
}
|
||||
|
||||
const updates: { label?: string; url?: string } = {};
|
||||
if (isNonEmptyString(label)) updates.label = label.trim();
|
||||
if (isNonEmptyString(url)) updates.url = normalizeServiceLinkUrl(url);
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
return res.status(400).json({ error: "Nothing to update" });
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await repo.updateServiceLink(userId, id, updates);
|
||||
if (!updated) return res.status(404).json({ error: "Not found" });
|
||||
res.json(updated);
|
||||
} catch (err) {
|
||||
logError(
|
||||
ctx,
|
||||
"Failed to update service link",
|
||||
err,
|
||||
"service_link_update_failed",
|
||||
);
|
||||
res.status(500).json({ error: "Failed to update service link" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/homepage/service-links/{id}:
|
||||
* delete:
|
||||
* summary: Delete service link
|
||||
* description: Deletes a dashboard service link by id.
|
||||
* tags:
|
||||
* - Homepage
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Service link deleted.
|
||||
* 400:
|
||||
* description: Invalid id.
|
||||
* 404:
|
||||
* description: Not found.
|
||||
* 500:
|
||||
* description: Failed to delete service link.
|
||||
*/
|
||||
router.delete("/service-links/:id", async (req: Request, res: Response) => {
|
||||
const userId = actor(ctx);
|
||||
const id = parseId(req.params.id);
|
||||
if (id === null) return res.status(400).json({ error: "Invalid id" });
|
||||
|
||||
try {
|
||||
const deleted = await repo.deleteServiceLink(userId, id);
|
||||
if (!deleted) return res.status(404).json({ error: "Not found" });
|
||||
await ctx.sync.recordTombstone(
|
||||
userId,
|
||||
"dashboardServiceLinks",
|
||||
deleted.syncId,
|
||||
);
|
||||
res.json({ message: "Service link deleted" });
|
||||
} catch (err) {
|
||||
logError(
|
||||
ctx,
|
||||
"Failed to delete service link",
|
||||
err,
|
||||
"service_link_delete_failed",
|
||||
);
|
||||
res.status(500).json({ error: "Failed to delete service link" });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { PluginContext } from "@termix/plugin-sdk/backend";
|
||||
import type { HomepageRepository } from "./repository.js";
|
||||
|
||||
/** The "homepage.items" service, version 1. Runs as the caller's user. */
|
||||
export interface HomepageItemsV1 {
|
||||
list: () => Promise<
|
||||
Array<{ id: number; typeId: string; title: string | null }>
|
||||
>;
|
||||
}
|
||||
|
||||
export function createHomepageItemsService(
|
||||
ctx: PluginContext,
|
||||
repo: HomepageRepository,
|
||||
): HomepageItemsV1 {
|
||||
return {
|
||||
list: async () => {
|
||||
const userId = ctx.currentActor();
|
||||
if (!userId) return [];
|
||||
const rows = await repo.listItemsByUser(userId);
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
typeId: row.typeId,
|
||||
title: row.title,
|
||||
}));
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
adoptLegacyTable,
|
||||
defineTable,
|
||||
id,
|
||||
integer,
|
||||
refUser,
|
||||
text,
|
||||
timestamp,
|
||||
varchar,
|
||||
} from "@termix/plugin-sdk/db";
|
||||
|
||||
/** One homepage widget tile. folderId points at another row when it sits in a folder. */
|
||||
export const homepageItems = adoptLegacyTable(
|
||||
"homepage_items",
|
||||
defineTable(
|
||||
"homepage_items",
|
||||
{
|
||||
id: id(),
|
||||
userId: refUser(),
|
||||
typeId: text().notNull(),
|
||||
title: text(),
|
||||
config: text().notNull().default("{}"),
|
||||
folderId: integer(),
|
||||
syncId: varchar().unique(),
|
||||
createdAt: timestamp().notNull().defaultNow(),
|
||||
updatedAt: timestamp().notNull().defaultNow(),
|
||||
},
|
||||
{
|
||||
indexes: [{ name: "idx_homepage_items_user_id", columns: ["userId"] }],
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
/** One canvas layout (widget positions, pan, zoom) per user. */
|
||||
export const homepageLayouts = adoptLegacyTable(
|
||||
"homepage_layouts",
|
||||
defineTable("homepage_layouts", {
|
||||
id: id(),
|
||||
userId: refUser().unique(),
|
||||
layout: text().notNull().default("{}"),
|
||||
updatedAt: timestamp().notNull().defaultNow(),
|
||||
}),
|
||||
);
|
||||
|
||||
/** Clickable dashboard service link buttons. */
|
||||
export const dashboardServiceLinks = adoptLegacyTable(
|
||||
"dashboard_service_links",
|
||||
defineTable(
|
||||
"dashboard_service_links",
|
||||
{
|
||||
id: id(),
|
||||
userId: refUser(),
|
||||
label: text().notNull(),
|
||||
url: text().notNull(),
|
||||
order: integer().notNull().default(0),
|
||||
syncId: varchar().unique(),
|
||||
createdAt: timestamp().notNull().defaultNow(),
|
||||
updatedAt: timestamp().notNull().defaultNow(),
|
||||
},
|
||||
{
|
||||
indexes: [
|
||||
{ name: "idx_dashboard_service_links_user_id", columns: ["userId"] },
|
||||
],
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export const tables = [homepageItems, homepageLayouts, dashboardServiceLinks];
|
||||
@@ -0,0 +1,16 @@
|
||||
export function normalizeServiceLinkUrl(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return "";
|
||||
return /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)
|
||||
? trimmed
|
||||
: `http://${trimmed}`;
|
||||
}
|
||||
|
||||
export function isValidServiceLinkUrl(value: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(normalizeServiceLinkUrl(value));
|
||||
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useState } from "react";
|
||||
import { Check, ExternalLink, Link } from "lucide-react";
|
||||
import { useTranslation, type ShellApi } from "@termix/plugin-sdk/frontend";
|
||||
import { HomepageCanvas } from "./HomepageCanvas.js";
|
||||
|
||||
/** The homepage plugin's contribution to the dashboard's "dashboard.secondaryView" slot. */
|
||||
export function DashboardHomepageView({
|
||||
onOpenSingletonTab,
|
||||
}: {
|
||||
onOpenSingletonTab: ShellApi["openSingletonTab"];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [linkCopied, setLinkCopied] = useState(false);
|
||||
|
||||
const handleCopyLink = () => {
|
||||
navigator.clipboard
|
||||
.writeText(`${window.location.origin}?view=homepage`)
|
||||
.catch(() => {});
|
||||
setLinkCopied(true);
|
||||
setTimeout(() => setLinkCopied(false), 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between px-3 py-1.5 border-b border-border shrink-0 bg-muted/20">
|
||||
<span className="text-[10px] text-muted-foreground/50 uppercase tracking-widest font-semibold">
|
||||
{t("nav.homepage")}
|
||||
</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
className="text-[10px] text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
|
||||
onClick={handleCopyLink}
|
||||
>
|
||||
{linkCopied ? (
|
||||
<>
|
||||
<Check size={10} className="text-accent-brand" />
|
||||
<span className="text-accent-brand">
|
||||
{t("homepage.linkCopied")}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link size={10} />
|
||||
{t("homepage.copyLink")}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className="text-[10px] text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
|
||||
onClick={() => onOpenSingletonTab("homepage")}
|
||||
>
|
||||
<ExternalLink size={10} />
|
||||
{t("homepage.openFullView")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
<HomepageCanvas />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
import { HomepageCanvas } from "./HomepageCanvas";
|
||||
|
||||
const HomepageApp: React.FC = () => {
|
||||
return (
|
||||
<div className="h-screen w-screen overflow-hidden">
|
||||
<HomepageCanvas fitOnLoad={true} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomepageApp;
|
||||
@@ -0,0 +1,562 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { LayoutGrid } from "lucide-react";
|
||||
import type { CanvasWidget, ContextMenuState, WidgetTypeId } from "./types.js";
|
||||
import { GRID_SIZE, MIN_ZOOM, MAX_ZOOM } from "./types.js";
|
||||
import {
|
||||
getHomepageItems,
|
||||
getHomepageLayout,
|
||||
createHomepageItem,
|
||||
updateHomepageItem,
|
||||
deleteHomepageItem,
|
||||
saveHomepageLayout,
|
||||
} from "./api.js";
|
||||
import { snapToGrid } from "./canvas/snapToGrid";
|
||||
import { screenToCanvas } from "./canvas/canvasGeometry";
|
||||
import { useCanvasInput } from "./canvas/useCanvasInput";
|
||||
import { useBoxDrag } from "./canvas/useBoxDrag";
|
||||
import { useBoxResize } from "./canvas/useBoxResize";
|
||||
import { CanvasDotBackground } from "./canvas/CanvasDotBackground";
|
||||
import { WidgetShell } from "./widgets/WidgetShell";
|
||||
import { AddWidgetMenu } from "./dialogs/AddWidgetMenu";
|
||||
import { WidgetEditDialog } from "./dialogs/WidgetEditDialog";
|
||||
import { HomepageToolbar } from "./toolbar/HomepageToolbar";
|
||||
import { getWidgetType } from "./widgets/WidgetRegistry";
|
||||
|
||||
// Side-effect imports so widgets register themselves
|
||||
import "./widgets/ServiceLinkWidget";
|
||||
import "./widgets/ClockWidget";
|
||||
import "./widgets/NotesWidget";
|
||||
import "./widgets/BookmarkListWidget";
|
||||
import "./widgets/HostStatusWidget";
|
||||
import "./widgets/FolderWidget";
|
||||
import "./widgets/WeatherWidget";
|
||||
import "./widgets/IframeWidget";
|
||||
import "./widgets/RssFeedWidget";
|
||||
import "./widgets/HostGridWidget";
|
||||
import "./widgets/PingStatusWidget";
|
||||
import "./widgets/RecentActivityWidget";
|
||||
import "./widgets/TermixUptimeWidget";
|
||||
import "./widgets/SystemOverviewWidget";
|
||||
import "./widgets/SshTerminalWidget";
|
||||
import "./widgets/QuickConnectWidget";
|
||||
import "./widgets/CalendarWidget";
|
||||
import "./widgets/CountdownWidget";
|
||||
import "./widgets/SearchBarWidget";
|
||||
import "./widgets/TextBannerWidget";
|
||||
import "./widgets/ImageWidget";
|
||||
import "./widgets/MarkdownNotesWidget";
|
||||
import "./widgets/CustomApiWidget";
|
||||
import "./widgets/ServiceGridWidget";
|
||||
import "./widgets/DashboardLinksWidget";
|
||||
import "./widgets/SearchLinksWidget";
|
||||
import "./widgets/LinkTreeWidget";
|
||||
|
||||
const CANVAS_SIZE = 100_000;
|
||||
const DEFAULT_PAN = { x: CANVAS_SIZE / 2 - 600, y: CANVAS_SIZE / 2 - 400 };
|
||||
const DEFAULT_ZOOM = 1.0;
|
||||
|
||||
interface HomepageCanvasProps {
|
||||
isReadOnly?: boolean;
|
||||
fitOnLoad?: boolean;
|
||||
onOpenFullscreen?: () => void;
|
||||
}
|
||||
|
||||
function nextZOrder(widgets: CanvasWidget[]): number {
|
||||
const nonFolders = widgets.filter((w) => w.typeId !== "folder");
|
||||
return nonFolders.length === 0
|
||||
? 1
|
||||
: Math.max(...nonFolders.map((w) => w.zOrder)) + 1;
|
||||
}
|
||||
|
||||
export function HomepageCanvas({
|
||||
isReadOnly,
|
||||
fitOnLoad,
|
||||
onOpenFullscreen,
|
||||
}: HomepageCanvasProps) {
|
||||
const { t } = useTranslation();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [widgets, setWidgets] = useState<CanvasWidget[]>([]);
|
||||
const [pan, setPan] = useState(DEFAULT_PAN);
|
||||
const [zoom, setZoom] = useState(DEFAULT_ZOOM);
|
||||
const [isLocked, setIsLocked] = useState(
|
||||
() => localStorage.getItem("homepage.locked") === "true",
|
||||
);
|
||||
const [draggingId, setDraggingId] = useState<number | null>(null);
|
||||
const [resizingId, setResizingId] = useState<number | null>(null);
|
||||
const [editingWidget, setEditingWidget] = useState<CanvasWidget | null>(null);
|
||||
const [dialogKey, setDialogKey] = useState(0);
|
||||
const [contextMenu, setContextMenu] = useState<ContextMenuState>({
|
||||
visible: false,
|
||||
screenX: 0,
|
||||
screenY: 0,
|
||||
canvasX: 0,
|
||||
canvasY: 0,
|
||||
});
|
||||
const [containerSize, setContainerSize] = useState({ w: 800, h: 600 });
|
||||
const [loading, setLoading] = useState(true);
|
||||
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const panRef = useRef(pan);
|
||||
const zoomRef = useRef(zoom);
|
||||
const widgetsRef = useRef(widgets);
|
||||
panRef.current = pan;
|
||||
zoomRef.current = zoom;
|
||||
widgetsRef.current = widgets;
|
||||
|
||||
const hasFitRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
const ro = new ResizeObserver(() => {
|
||||
setContainerSize({ w: el.offsetWidth, h: el.offsetHeight });
|
||||
});
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
Promise.all([getHomepageItems(), getHomepageLayout()])
|
||||
.then(([items, layoutRow]) => {
|
||||
if (cancelled) return;
|
||||
const layoutEntries = layoutRow?.layout?.entries ?? [];
|
||||
const merged: CanvasWidget[] = items.map((item) => {
|
||||
const entry = layoutEntries.find((e) => e.itemId === item.id);
|
||||
const typeDef = getWidgetType(item.typeId as WidgetTypeId);
|
||||
return {
|
||||
id: item.id,
|
||||
typeId: item.typeId as WidgetTypeId,
|
||||
title: item.title,
|
||||
config: JSON.parse(item.config || "{}"),
|
||||
x: entry?.x ?? snapToGrid(Math.random() * 600 + 100),
|
||||
y: entry?.y ?? snapToGrid(Math.random() * 400 + 100),
|
||||
w: entry?.w ?? typeDef?.defaultSize.w ?? GRID_SIZE * 8,
|
||||
h: entry?.h ?? typeDef?.defaultSize.h ?? GRID_SIZE * 6,
|
||||
zOrder: entry?.zOrder ?? 0,
|
||||
};
|
||||
});
|
||||
setWidgets(merged);
|
||||
if (layoutRow?.layout?.pan) setPan(layoutRow.layout.pan);
|
||||
if (layoutRow?.layout?.zoom) setZoom(layoutRow.layout.zoom);
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const scheduleSave = useCallback(
|
||||
(newWidgets?: CanvasWidget[], newPan?: typeof pan, newZoom?: number) => {
|
||||
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
||||
saveTimerRef.current = setTimeout(() => {
|
||||
const ws = newWidgets ?? widgetsRef.current;
|
||||
const p = newPan ?? panRef.current;
|
||||
const z = newZoom ?? zoomRef.current;
|
||||
saveHomepageLayout({
|
||||
entries: ws.map((w) => ({
|
||||
itemId: w.id,
|
||||
x: w.x,
|
||||
y: w.y,
|
||||
w: w.w,
|
||||
h: w.h,
|
||||
zOrder: w.zOrder,
|
||||
})),
|
||||
pan: p,
|
||||
zoom: z,
|
||||
}).catch(() => {});
|
||||
}, 500);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fitOnLoad || loading || widgets.length === 0 || hasFitRef.current)
|
||||
return;
|
||||
const el = containerRef.current;
|
||||
if (!el || el.offsetWidth === 0 || el.offsetHeight === 0) return;
|
||||
hasFitRef.current = true;
|
||||
const vw = el.offsetWidth;
|
||||
const vh = el.offsetHeight;
|
||||
const minX = Math.min(...widgets.map((w) => w.x));
|
||||
const minY = Math.min(...widgets.map((w) => w.y));
|
||||
const maxX = Math.max(...widgets.map((w) => w.x + w.w));
|
||||
const maxY = Math.max(...widgets.map((w) => w.y + w.h));
|
||||
const contentW = maxX - minX;
|
||||
const contentH = maxY - minY;
|
||||
const PADDING = 48;
|
||||
const fz = Math.min(
|
||||
MAX_ZOOM,
|
||||
Math.max(
|
||||
MIN_ZOOM,
|
||||
Math.min((vw - PADDING * 2) / contentW, (vh - PADDING * 2) / contentH),
|
||||
),
|
||||
);
|
||||
setPan({
|
||||
x: (vw - contentW * fz) / 2 - minX * fz,
|
||||
y: (vh - contentH * fz) / 2 - minY * fz,
|
||||
});
|
||||
setZoom(fz);
|
||||
}, [fitOnLoad, loading, widgets, containerSize]);
|
||||
|
||||
const handlePanChange = useCallback(
|
||||
(newPan: typeof pan) => {
|
||||
setPan(newPan);
|
||||
scheduleSave(undefined, newPan, undefined);
|
||||
},
|
||||
[scheduleSave],
|
||||
);
|
||||
|
||||
const handleZoomChange = useCallback(
|
||||
(newZoom: number, newPan: typeof pan) => {
|
||||
setZoom(newZoom);
|
||||
setPan(newPan);
|
||||
scheduleSave(undefined, newPan, newZoom);
|
||||
},
|
||||
[scheduleSave],
|
||||
);
|
||||
|
||||
const canvasInput = useCanvasInput({
|
||||
isDraggingWidget: draggingId !== null,
|
||||
isResizing: resizingId !== null,
|
||||
isContextMenuVisible: contextMenu.visible,
|
||||
onPanChange: handlePanChange,
|
||||
onZoomChange: handleZoomChange,
|
||||
getPan: () => panRef.current,
|
||||
getZoom: () => zoomRef.current,
|
||||
containerRef,
|
||||
});
|
||||
|
||||
const handleWidgetMove = useCallback(
|
||||
(id: number, x: number, y: number) => {
|
||||
setWidgets((prev) => {
|
||||
const next = prev.map((w) => (w.id === id ? { ...w, x, y } : w));
|
||||
scheduleSave(next);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[scheduleSave],
|
||||
);
|
||||
|
||||
const handleWidgetResize = useCallback(
|
||||
(id: number, w: number, h: number) => {
|
||||
setWidgets((prev) => {
|
||||
const next = prev.map((widget) =>
|
||||
widget.id === id ? { ...widget, w, h } : widget,
|
||||
);
|
||||
scheduleSave(next);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[scheduleSave],
|
||||
);
|
||||
|
||||
const handleDragStart = useCallback(
|
||||
(id: number) => {
|
||||
setDraggingId(id);
|
||||
setWidgets((prev) => {
|
||||
const widget = prev.find((w) => w.id === id);
|
||||
if (!widget || widget.typeId === "folder") return prev;
|
||||
const top = nextZOrder(prev);
|
||||
const next = prev.map((w) => (w.id === id ? { ...w, zOrder: top } : w));
|
||||
scheduleSave(next);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[scheduleSave],
|
||||
);
|
||||
|
||||
const { startDrag } = useBoxDrag({
|
||||
widgets,
|
||||
zoom,
|
||||
getZoom: () => zoomRef.current,
|
||||
onWidgetMove: handleWidgetMove,
|
||||
onDragStart: handleDragStart,
|
||||
onDragEnd: () => setDraggingId(null),
|
||||
});
|
||||
|
||||
const { startResize } = useBoxResize({
|
||||
widgets,
|
||||
getZoom: () => zoomRef.current,
|
||||
onWidgetResize: handleWidgetResize,
|
||||
onResizeStart: setResizingId,
|
||||
onResizeEnd: () => setResizingId(null),
|
||||
});
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (isLocked || isReadOnly) return;
|
||||
if ((e.target as HTMLElement).closest("[data-widget]")) return;
|
||||
e.preventDefault();
|
||||
const rect = containerRef.current!.getBoundingClientRect();
|
||||
const sx = e.clientX - rect.left;
|
||||
const sy = e.clientY - rect.top;
|
||||
const { x, y } = screenToCanvas(sx, sy, panRef.current, zoomRef.current);
|
||||
setContextMenu({
|
||||
visible: true,
|
||||
screenX: e.clientX,
|
||||
screenY: e.clientY,
|
||||
canvasX: snapToGrid(x),
|
||||
canvasY: snapToGrid(y),
|
||||
});
|
||||
},
|
||||
[isLocked, isReadOnly],
|
||||
);
|
||||
|
||||
const handleAddWidget = useCallback(
|
||||
async (typeId: WidgetTypeId, canvasX: number, canvasY: number) => {
|
||||
const typeDef = getWidgetType(typeId);
|
||||
if (!typeDef) return;
|
||||
try {
|
||||
const item = await createHomepageItem({
|
||||
typeId,
|
||||
config: typeDef.defaultConfig as Record<string, unknown>,
|
||||
});
|
||||
setWidgets((prev) => {
|
||||
const zOrder = typeId === "folder" ? 0 : nextZOrder(prev);
|
||||
const newWidget: CanvasWidget = {
|
||||
id: item.id,
|
||||
typeId,
|
||||
title: item.title,
|
||||
config: typeDef.defaultConfig as Record<string, unknown>,
|
||||
x: canvasX,
|
||||
y: canvasY,
|
||||
w: typeDef.defaultSize.w,
|
||||
h: typeDef.defaultSize.h,
|
||||
zOrder,
|
||||
};
|
||||
const next = [...prev, newWidget];
|
||||
scheduleSave(next);
|
||||
return next;
|
||||
});
|
||||
} catch {}
|
||||
},
|
||||
[scheduleSave],
|
||||
);
|
||||
|
||||
const handleOpenAddMenu = useCallback(
|
||||
(anchorRect: {
|
||||
top: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
right: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => {
|
||||
const w = containerSize.w;
|
||||
const h = containerSize.h;
|
||||
const { x, y } = screenToCanvas(
|
||||
w / 2,
|
||||
h / 2,
|
||||
panRef.current,
|
||||
zoomRef.current,
|
||||
);
|
||||
setContextMenu({
|
||||
visible: true,
|
||||
screenX: anchorRect.left,
|
||||
screenY: anchorRect.top,
|
||||
canvasX: snapToGrid(x),
|
||||
canvasY: snapToGrid(y),
|
||||
anchorRect,
|
||||
});
|
||||
},
|
||||
[containerSize],
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
async (id: number) => {
|
||||
try {
|
||||
await deleteHomepageItem(id);
|
||||
setWidgets((prev) => {
|
||||
const next = prev.filter((w) => w.id !== id);
|
||||
scheduleSave(next);
|
||||
return next;
|
||||
});
|
||||
} catch {}
|
||||
},
|
||||
[scheduleSave],
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(id: number) => {
|
||||
const w = widgets.find((x) => x.id === id);
|
||||
if (w) {
|
||||
setEditingWidget(w);
|
||||
setDialogKey((k) => k + 1);
|
||||
}
|
||||
},
|
||||
[widgets],
|
||||
);
|
||||
|
||||
const handleSaveEdit = useCallback(
|
||||
async (
|
||||
id: number,
|
||||
title: string | null,
|
||||
config: Record<string, unknown>,
|
||||
) => {
|
||||
try {
|
||||
await updateHomepageItem(id, { title, config });
|
||||
setWidgets((prev) =>
|
||||
prev.map((w) => (w.id === id ? { ...w, title, config } : w)),
|
||||
);
|
||||
} catch {}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const resetView = useCallback(() => {
|
||||
const ws = widgetsRef.current;
|
||||
const el = containerRef.current;
|
||||
const vw = el?.offsetWidth ?? 800;
|
||||
const vh = el?.offsetHeight ?? 600;
|
||||
|
||||
if (ws.length === 0) {
|
||||
setPan(DEFAULT_PAN);
|
||||
setZoom(DEFAULT_ZOOM);
|
||||
return;
|
||||
}
|
||||
|
||||
const minX = Math.min(...ws.map((w) => w.x));
|
||||
const minY = Math.min(...ws.map((w) => w.y));
|
||||
const maxX = Math.max(...ws.map((w) => w.x + w.w));
|
||||
const maxY = Math.max(...ws.map((w) => w.y + w.h));
|
||||
const contentW = maxX - minX;
|
||||
const contentH = maxY - minY;
|
||||
|
||||
const PADDING = 48;
|
||||
const fitZoom = Math.min(
|
||||
MAX_ZOOM,
|
||||
Math.max(
|
||||
MIN_ZOOM,
|
||||
Math.min((vw - PADDING * 2) / contentW, (vh - PADDING * 2) / contentH),
|
||||
),
|
||||
);
|
||||
|
||||
// Place the content bbox center at the viewport center
|
||||
const newPan = {
|
||||
x: (vw - contentW * fitZoom) / 2 - minX * fitZoom,
|
||||
y: (vh - contentH * fitZoom) / 2 - minY * fitZoom,
|
||||
};
|
||||
|
||||
setZoom(fitZoom);
|
||||
setPan(newPan);
|
||||
scheduleSave(undefined, newPan, fitZoom);
|
||||
}, [scheduleSave]);
|
||||
|
||||
// Folders always render first (lowest z), non-folders sorted by zOrder ascending
|
||||
const sortedWidgets = [...widgets].sort((a, b) => {
|
||||
const aFolder = a.typeId === "folder";
|
||||
const bFolder = b.typeId === "folder";
|
||||
if (aFolder && !bFolder) return -1;
|
||||
if (!aFolder && bFolder) return 1;
|
||||
return a.zOrder - b.zOrder;
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full h-full text-muted-foreground text-sm">
|
||||
{t("common.loading")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative w-full h-full overflow-hidden bg-background"
|
||||
onMouseDown={editingWidget ? undefined : canvasInput.handleMouseDown}
|
||||
onMouseMove={canvasInput.handleMouseMove}
|
||||
onMouseUp={canvasInput.handleMouseUp}
|
||||
onMouseLeave={canvasInput.handleMouseUp}
|
||||
onWheel={canvasInput.handleWheel}
|
||||
onContextMenu={handleContextMenu}
|
||||
style={{ cursor: draggingId ? "grabbing" : "default" }}
|
||||
>
|
||||
<CanvasDotBackground pan={pan} zoom={zoom} />
|
||||
|
||||
<div
|
||||
className="absolute origin-top-left"
|
||||
style={{
|
||||
width: CANVAS_SIZE,
|
||||
height: CANVAS_SIZE,
|
||||
transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,
|
||||
willChange: "transform",
|
||||
}}
|
||||
>
|
||||
{sortedWidgets.map((widget) => (
|
||||
<WidgetShell
|
||||
key={widget.id}
|
||||
widget={widget}
|
||||
isLocked={isLocked}
|
||||
isReadOnly={isReadOnly}
|
||||
isDragging={draggingId === widget.id}
|
||||
isResizing={resizingId === widget.id}
|
||||
onStartDrag={startDrag}
|
||||
onStartResize={startResize}
|
||||
onDelete={handleDelete}
|
||||
onEdit={handleEdit}
|
||||
onConfigUpdate={(config) =>
|
||||
handleSaveEdit(widget.id, widget.title, config)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{widgets.length === 0 && !isReadOnly && (
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none select-none">
|
||||
<div className="flex items-center gap-2 bg-card border border-border px-3 py-2 shadow-sm">
|
||||
<LayoutGrid className="size-3.5 text-muted-foreground/50 shrink-0" />
|
||||
<span className="text-xs text-muted-foreground/70">
|
||||
{t("homepage.noWidgets")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isReadOnly && onOpenFullscreen && (
|
||||
<button
|
||||
className="absolute top-2 right-2 z-20 text-xs text-muted-foreground hover:text-foreground bg-card/80 border border-border px-2 py-1"
|
||||
onClick={onOpenFullscreen}
|
||||
>
|
||||
{t("homepage.openFullView")}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!isReadOnly && (
|
||||
<HomepageToolbar
|
||||
zoom={zoom}
|
||||
isLocked={isLocked}
|
||||
pan={pan}
|
||||
containerSize={containerSize}
|
||||
onZoomChange={handleZoomChange}
|
||||
onLockToggle={() =>
|
||||
setIsLocked((v) => {
|
||||
const next = !v;
|
||||
localStorage.setItem("homepage.locked", String(next));
|
||||
return next;
|
||||
})
|
||||
}
|
||||
onResetView={resetView}
|
||||
onAddWidget={handleOpenAddMenu}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AddWidgetMenu
|
||||
state={contextMenu}
|
||||
onAdd={handleAddWidget}
|
||||
onClose={() => setContextMenu((s) => ({ ...s, visible: false }))}
|
||||
/>
|
||||
|
||||
<WidgetEditDialog
|
||||
key={dialogKey}
|
||||
widget={editingWidget}
|
||||
onSave={handleSaveEdit}
|
||||
onClose={() => setEditingWidget(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { LayoutGrid } from "lucide-react";
|
||||
import {
|
||||
useTranslation,
|
||||
type DashboardCardProps,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { Card } from "@termix/plugin-sdk/ui";
|
||||
import { HomepageCanvas } from "./HomepageCanvas.js";
|
||||
|
||||
export function HomepagePreviewCard({ shell }: DashboardCardProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Card className="relative overflow-hidden w-full h-full flex flex-col p-0 gap-0">
|
||||
<div className="flex items-center gap-2 px-3 py-2 shrink-0 border-b border-border">
|
||||
<LayoutGrid className="size-3.5 text-muted-foreground" />
|
||||
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
{t("homepage.previewTitle")}
|
||||
</span>
|
||||
<button
|
||||
className="ml-auto text-[10px] text-muted-foreground hover:text-foreground transition-colors"
|
||||
onClick={() => shell.openSingletonTab("homepage")}
|
||||
>
|
||||
{t("homepage.openFullView")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 relative overflow-hidden">
|
||||
<HomepageCanvas isReadOnly={true} fitOnLoad={true} />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ExternalLink, Link, Trash2 } from "lucide-react";
|
||||
import {
|
||||
usePluginApi,
|
||||
useTranslation,
|
||||
type DashboardCardProps,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { Button, Card } from "@termix/plugin-sdk/ui";
|
||||
import type { ServiceLinkRecord } from "./types.js";
|
||||
import { isValidServiceLinkUrl, normalizeServiceLinkUrl } from "./url.js";
|
||||
|
||||
export function ServiceLinksCard({ isVisible }: DashboardCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const api = usePluginApi();
|
||||
const [links, setLinks] = useState<ServiceLinkRecord[]>([]);
|
||||
const [label, setLabel] = useState("");
|
||||
const [url, setUrl] = useState("");
|
||||
const [urlError, setUrlError] = useState(false);
|
||||
const [addError, setAddError] = useState("");
|
||||
const [adding, setAdding] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible) return;
|
||||
api
|
||||
.get<ServiceLinkRecord[]>("/service-links")
|
||||
.then((res) => setLinks(res.data))
|
||||
.catch(() => {});
|
||||
}, [isVisible]);
|
||||
|
||||
const handleAdd = async () => {
|
||||
const normalizedUrl = normalizeServiceLinkUrl(url);
|
||||
if (!isValidServiceLinkUrl(normalizedUrl)) {
|
||||
setUrlError(true);
|
||||
setAddError("");
|
||||
return;
|
||||
}
|
||||
setUrlError(false);
|
||||
setAddError("");
|
||||
setAdding(true);
|
||||
try {
|
||||
const res = await api.post<ServiceLinkRecord>("/service-links", {
|
||||
label: label.trim(),
|
||||
url: normalizedUrl,
|
||||
});
|
||||
setLinks((prev) => [...prev, res.data]);
|
||||
setLabel("");
|
||||
setUrl("");
|
||||
} catch (error) {
|
||||
setAddError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t("dashboardTab.serviceLinksAddFailed"),
|
||||
);
|
||||
} finally {
|
||||
setAdding(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await api.delete(`/service-links/${id}`);
|
||||
setLinks((prev) => prev.filter((l) => l.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="flex flex-col overflow-hidden w-full h-full py-0 gap-0">
|
||||
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-border shrink-0">
|
||||
<Link className="size-3.5 text-muted-foreground" />
|
||||
<span className="text-xs text-muted-foreground uppercase tracking-widest font-semibold">
|
||||
{t("dashboardTab.serviceLinksTitle")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col overflow-auto flex-1">
|
||||
{links.length === 0 && (
|
||||
<div className="flex-1 flex items-center justify-center text-xs text-muted-foreground/40 py-4">
|
||||
{t("dashboardTab.serviceLinksEmpty")}
|
||||
</div>
|
||||
)}
|
||||
{links.map((link) => (
|
||||
<div
|
||||
key={link.id}
|
||||
className="flex items-center justify-between px-4 py-2 border-b border-border last:border-0 group/link"
|
||||
>
|
||||
<a
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="flex items-center gap-2 min-w-0 flex-1 hover:text-accent-brand transition-colors"
|
||||
>
|
||||
<ExternalLink className="size-3 text-muted-foreground shrink-0" />
|
||||
<span className="text-xs font-semibold truncate">
|
||||
{link.label}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground truncate">
|
||||
{link.url}
|
||||
</span>
|
||||
</a>
|
||||
<button
|
||||
onClick={() => handleDelete(link.id)}
|
||||
className="ml-2 opacity-0 group-hover/link:opacity-100 transition-opacity size-5 flex items-center justify-center hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-4 py-2 border-t border-border shrink-0">
|
||||
<input
|
||||
type="text"
|
||||
value={label}
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
placeholder={t("dashboardTab.serviceLinksLabelPlaceholder")}
|
||||
className="flex-1 min-w-0 text-xs bg-transparent border border-border px-2 py-1 focus:outline-none focus:border-accent-brand/60"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={url}
|
||||
onChange={(e) => {
|
||||
setUrl(e.target.value);
|
||||
setUrlError(false);
|
||||
setAddError("");
|
||||
}}
|
||||
placeholder={t("dashboardTab.serviceLinksUrlPlaceholder")}
|
||||
className={`flex-[2] min-w-0 text-xs bg-transparent border px-2 py-1 focus:outline-none ${urlError ? "border-destructive" : "border-border focus:border-accent-brand/60"}`}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
className="text-xs bg-accent-brand hover:bg-accent-brand/90 text-white h-6 px-2 shrink-0"
|
||||
onClick={handleAdd}
|
||||
disabled={!label.trim() || !url.trim() || adding}
|
||||
>
|
||||
{t("dashboardTab.serviceLinksAdd")}
|
||||
</Button>
|
||||
</div>
|
||||
{urlError && (
|
||||
<div className="px-4 pb-2 text-[10px] text-destructive shrink-0">
|
||||
{t("dashboardTab.serviceLinksInvalidUrl")}
|
||||
</div>
|
||||
)}
|
||||
{addError && (
|
||||
<div className="px-4 pb-2 text-[10px] text-destructive shrink-0">
|
||||
{addError}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { PluginApiClient } from "@termix/plugin-sdk/frontend";
|
||||
import type {
|
||||
HomepageItemRow,
|
||||
HomepageLayoutData,
|
||||
HomepageLayoutRow,
|
||||
WidgetTypeId,
|
||||
} from "./types.js";
|
||||
|
||||
let api: PluginApiClient | null = null;
|
||||
|
||||
/** Set once from activate(app); the widget canvas calls the functions below. */
|
||||
export function setHomepageApi(client: PluginApiClient | null): void {
|
||||
api = client;
|
||||
}
|
||||
|
||||
function client(): PluginApiClient {
|
||||
if (!api) throw new Error("Homepage plugin API is not ready");
|
||||
return api;
|
||||
}
|
||||
|
||||
export async function getHomepageItems(): Promise<HomepageItemRow[]> {
|
||||
const res = await client().get<HomepageItemRow[]>("/items");
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function createHomepageItem(data: {
|
||||
typeId: WidgetTypeId;
|
||||
title?: string | null;
|
||||
config?: Record<string, unknown>;
|
||||
}): Promise<HomepageItemRow> {
|
||||
const res = await client().post<HomepageItemRow>("/items", data);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function updateHomepageItem(
|
||||
id: number,
|
||||
data: { title?: string | null; config?: Record<string, unknown> },
|
||||
): Promise<HomepageItemRow> {
|
||||
const res = await client().put<HomepageItemRow>(`/items/${id}`, data);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function deleteHomepageItem(id: number): Promise<void> {
|
||||
await client().delete(`/items/${id}`);
|
||||
}
|
||||
|
||||
export async function getHomepageLayout(): Promise<HomepageLayoutRow | null> {
|
||||
const res = await client().get<HomepageLayoutRow | null>("/layout");
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function saveHomepageLayout(
|
||||
layout: HomepageLayoutData,
|
||||
): Promise<HomepageLayoutRow> {
|
||||
const res = await client().put<HomepageLayoutRow>("/layout", layout);
|
||||
return res.data;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
interface Props {
|
||||
pan: { x: number; y: number };
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
export function CanvasDotBackground({ pan, zoom }: Props) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const w = canvas.offsetWidth;
|
||||
const h = canvas.offsetHeight;
|
||||
canvas.width = w * dpr;
|
||||
canvas.height = h * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// Read dot color from CSS variable for theme support
|
||||
const dotColor =
|
||||
getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--color-muted-foreground")
|
||||
.trim() || "#6b7280";
|
||||
|
||||
// Effective grid spacing in screen pixels
|
||||
const spacing = 30 * zoom;
|
||||
|
||||
// Skip rendering dots that are too close together or too far apart
|
||||
if (spacing < 8 || spacing > 200) return;
|
||||
|
||||
// Offset so dots align with the canvas pan position
|
||||
const offsetX = ((pan.x % spacing) + spacing) % spacing;
|
||||
const offsetY = ((pan.y % spacing) + spacing) % spacing;
|
||||
|
||||
const dotRadius = Math.max(0.8, Math.min(1.5, zoom * 1.2));
|
||||
|
||||
ctx.fillStyle = dotColor;
|
||||
ctx.globalAlpha = 0.35;
|
||||
|
||||
for (let x = offsetX; x < w; x += spacing) {
|
||||
for (let y = offsetY; y < h; y += spacing) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, dotRadius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
}, [pan, zoom]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="absolute inset-0 w-full h-full pointer-events-none"
|
||||
style={{ zIndex: 0 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
export function screenToCanvas(
|
||||
screenX: number,
|
||||
screenY: number,
|
||||
pan: { x: number; y: number },
|
||||
zoom: number,
|
||||
): { x: number; y: number } {
|
||||
return {
|
||||
x: (screenX - pan.x) / zoom,
|
||||
y: (screenY - pan.y) / zoom,
|
||||
};
|
||||
}
|
||||
|
||||
export function canvasToScreen(
|
||||
canvasX: number,
|
||||
canvasY: number,
|
||||
pan: { x: number; y: number },
|
||||
zoom: number,
|
||||
): { x: number; y: number } {
|
||||
return {
|
||||
x: canvasX * zoom + pan.x,
|
||||
y: canvasY * zoom + pan.y,
|
||||
};
|
||||
}
|
||||
|
||||
/** Pan value that keeps the canvas point (gridX, gridY) under (mouseX, mouseY) after zoom changes. */
|
||||
export function zoomAroundPoint(
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
pan: { x: number; y: number },
|
||||
oldZoom: number,
|
||||
newZoom: number,
|
||||
): { x: number; y: number } {
|
||||
const gridX = (mouseX - pan.x) / oldZoom;
|
||||
const gridY = (mouseY - pan.y) / oldZoom;
|
||||
return {
|
||||
x: mouseX - gridX * newZoom,
|
||||
y: mouseY - gridY * newZoom,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { GRID_SIZE } from "../types.js";
|
||||
|
||||
export function snapToGrid(value: number): number {
|
||||
return Math.round(value / GRID_SIZE) * GRID_SIZE;
|
||||
}
|
||||
|
||||
export function snapToGridFloor(value: number): number {
|
||||
return Math.floor(value / GRID_SIZE) * GRID_SIZE;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
import type { CanvasWidget, DragState } from "../types.js";
|
||||
import { snapToGrid } from "./snapToGrid";
|
||||
|
||||
interface UseBoxDragOptions {
|
||||
widgets: CanvasWidget[];
|
||||
zoom: number;
|
||||
getZoom: () => number;
|
||||
onWidgetMove: (id: number, x: number, y: number) => void;
|
||||
onDragStart: (id: number) => void;
|
||||
onDragEnd: () => void;
|
||||
}
|
||||
|
||||
export function useBoxDrag({
|
||||
widgets,
|
||||
getZoom,
|
||||
onWidgetMove,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
}: UseBoxDragOptions) {
|
||||
const dragRef = useRef<DragState | null>(null);
|
||||
|
||||
const startDrag = useCallback(
|
||||
(e: React.MouseEvent, widget: CanvasWidget) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
dragRef.current = {
|
||||
widgetId: widget.id,
|
||||
startMouseX: e.clientX,
|
||||
startMouseY: e.clientY,
|
||||
startWidgetX: widget.x,
|
||||
startWidgetY: widget.y,
|
||||
};
|
||||
onDragStart(widget.id);
|
||||
|
||||
const handleMove = (ev: MouseEvent) => {
|
||||
if (!dragRef.current) return;
|
||||
const zoom = getZoom();
|
||||
const dx = (ev.clientX - dragRef.current.startMouseX) / zoom;
|
||||
const dy = (ev.clientY - dragRef.current.startMouseY) / zoom;
|
||||
const newX = snapToGrid(dragRef.current.startWidgetX + dx);
|
||||
const newY = snapToGrid(dragRef.current.startWidgetY + dy);
|
||||
|
||||
const moving = widgets.find((w) => w.id === dragRef.current!.widgetId);
|
||||
if (!moving) return;
|
||||
|
||||
if (newX !== moving.x || newY !== moving.y) {
|
||||
onWidgetMove(moving.id, newX, newY);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUp = () => {
|
||||
dragRef.current = null;
|
||||
onDragEnd();
|
||||
window.removeEventListener("mousemove", handleMove);
|
||||
window.removeEventListener("mouseup", handleUp);
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", handleMove);
|
||||
window.addEventListener("mouseup", handleUp);
|
||||
},
|
||||
[widgets, getZoom, onWidgetMove, onDragStart, onDragEnd],
|
||||
);
|
||||
|
||||
return { startDrag };
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
import { GRID_SIZE, type CanvasWidget, type ResizeState } from "../types.js";
|
||||
import { snapToGrid } from "./snapToGrid";
|
||||
import { getWidgetType } from "../widgets/WidgetRegistry";
|
||||
|
||||
interface UseBoxResizeOptions {
|
||||
widgets: CanvasWidget[];
|
||||
getZoom: () => number;
|
||||
onWidgetResize: (id: number, w: number, h: number) => void;
|
||||
onResizeStart: (id: number) => void;
|
||||
onResizeEnd: () => void;
|
||||
}
|
||||
|
||||
export function useBoxResize({
|
||||
widgets,
|
||||
getZoom,
|
||||
onWidgetResize,
|
||||
onResizeStart,
|
||||
onResizeEnd,
|
||||
}: UseBoxResizeOptions) {
|
||||
const resizeRef = useRef<ResizeState | null>(null);
|
||||
|
||||
const startResize = useCallback(
|
||||
(e: React.MouseEvent, widget: CanvasWidget) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
resizeRef.current = {
|
||||
widgetId: widget.id,
|
||||
startMouseX: e.clientX,
|
||||
startMouseY: e.clientY,
|
||||
startW: widget.w,
|
||||
startH: widget.h,
|
||||
};
|
||||
onResizeStart(widget.id);
|
||||
|
||||
const handleMove = (ev: MouseEvent) => {
|
||||
if (!resizeRef.current) return;
|
||||
const zoom = getZoom();
|
||||
const dx = (ev.clientX - resizeRef.current.startMouseX) / zoom;
|
||||
const dy = (ev.clientY - resizeRef.current.startMouseY) / zoom;
|
||||
const newW = Math.max(
|
||||
GRID_SIZE * 2,
|
||||
snapToGrid(resizeRef.current.startW + dx),
|
||||
);
|
||||
const newH = Math.max(
|
||||
GRID_SIZE * 2,
|
||||
snapToGrid(resizeRef.current.startH + dy),
|
||||
);
|
||||
|
||||
const moving = widgets.find(
|
||||
(w) => w.id === resizeRef.current!.widgetId,
|
||||
);
|
||||
if (!moving) return;
|
||||
|
||||
const typeDef = getWidgetType(moving.typeId);
|
||||
const minW = typeDef?.minSize.w ?? GRID_SIZE * 4;
|
||||
const minH = typeDef?.minSize.h ?? GRID_SIZE * 4;
|
||||
const clampedW = Math.max(minW, newW);
|
||||
const clampedH = Math.max(minH, newH);
|
||||
|
||||
if (clampedW !== moving.w || clampedH !== moving.h) {
|
||||
onWidgetResize(moving.id, clampedW, clampedH);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUp = () => {
|
||||
resizeRef.current = null;
|
||||
onResizeEnd();
|
||||
window.removeEventListener("mousemove", handleMove);
|
||||
window.removeEventListener("mouseup", handleUp);
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", handleMove);
|
||||
window.removeEventListener("mouseup", handleUp);
|
||||
window.addEventListener("mouseup", handleUp);
|
||||
},
|
||||
[widgets, getZoom, onWidgetResize, onResizeStart, onResizeEnd],
|
||||
);
|
||||
|
||||
return { startResize };
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
import { MIN_ZOOM, MAX_ZOOM } from "../types.js";
|
||||
import { zoomAroundPoint } from "./canvasGeometry";
|
||||
|
||||
interface UseCanvasInputOptions {
|
||||
isDraggingWidget: boolean;
|
||||
isResizing: boolean;
|
||||
isContextMenuVisible: boolean;
|
||||
onPanChange: (pan: { x: number; y: number }) => void;
|
||||
onZoomChange: (zoom: number, pan: { x: number; y: number }) => void;
|
||||
getPan: () => { x: number; y: number };
|
||||
getZoom: () => number;
|
||||
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||
}
|
||||
|
||||
export function useCanvasInput({
|
||||
isDraggingWidget,
|
||||
isResizing,
|
||||
isContextMenuVisible,
|
||||
onPanChange,
|
||||
onZoomChange,
|
||||
getPan,
|
||||
getZoom,
|
||||
containerRef,
|
||||
}: UseCanvasInputOptions) {
|
||||
const isPanningRef = useRef(false);
|
||||
const lastMouseRef = useRef({ x: 0, y: 0 });
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
// Only pan on the background (not on widgets), left button
|
||||
if (e.button !== 0) return;
|
||||
if ((e.target as HTMLElement).closest("[data-widget]")) return;
|
||||
if (isDraggingWidget || isResizing || isContextMenuVisible) return;
|
||||
|
||||
isPanningRef.current = true;
|
||||
lastMouseRef.current = { x: e.clientX, y: e.clientY };
|
||||
e.preventDefault();
|
||||
},
|
||||
[isDraggingWidget, isResizing, isContextMenuVisible],
|
||||
);
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (!isPanningRef.current) return;
|
||||
const dx = e.clientX - lastMouseRef.current.x;
|
||||
const dy = e.clientY - lastMouseRef.current.y;
|
||||
lastMouseRef.current = { x: e.clientX, y: e.clientY };
|
||||
const pan = getPan();
|
||||
onPanChange({ x: pan.x + dx, y: pan.y + dy });
|
||||
},
|
||||
[getPan, onPanChange],
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
isPanningRef.current = false;
|
||||
}, []);
|
||||
|
||||
const handleWheel = useCallback(
|
||||
(e: React.WheelEvent) => {
|
||||
e.preventDefault();
|
||||
const zoom = getZoom();
|
||||
const pan = getPan();
|
||||
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
const rect = container.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
|
||||
// Multiplicative zoom so each step is proportional regardless of current zoom level.
|
||||
// Normalize deltaY to avoid trackpad sending huge values.
|
||||
const rawDelta = Math.sign(e.deltaY) * Math.min(Math.abs(e.deltaY), 100);
|
||||
const factor = 1 - rawDelta * 0.001;
|
||||
const newZoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom * factor));
|
||||
if (newZoom === zoom) return;
|
||||
|
||||
const newPan = zoomAroundPoint(mouseX, mouseY, pan, zoom, newZoom);
|
||||
onZoomChange(newZoom, newPan);
|
||||
},
|
||||
[getZoom, getPan, containerRef, onZoomChange],
|
||||
);
|
||||
|
||||
return { handleMouseDown, handleMouseMove, handleMouseUp, handleWheel };
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/** IANA zone names use underscores, but people type and paste spaces. */
|
||||
export function normalizeTimezone(timezone: string): string {
|
||||
return timezone.trim().replace(/\s+/g, "_");
|
||||
}
|
||||
|
||||
export function isValidTimezone(timezone: string): boolean {
|
||||
try {
|
||||
new Intl.DateTimeFormat("en-US", { timeZone: timezone });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the timezone a user typed and reports whether it is usable.
|
||||
* A blank field is valid: it means "use local time".
|
||||
*/
|
||||
export function validateClockTimezone(timezone: unknown): {
|
||||
timezone?: string;
|
||||
valid: boolean;
|
||||
} {
|
||||
if (typeof timezone !== "string" || timezone.trim() === "") {
|
||||
return { timezone: undefined, valid: true };
|
||||
}
|
||||
const normalized = normalizeTimezone(timezone);
|
||||
return { timezone: normalized, valid: isValidTimezone(normalized) };
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { useWidgetTypes } from "../widgets/WidgetRegistry";
|
||||
import type { ContextMenuState, WidgetTypeId } from "../types.js";
|
||||
|
||||
interface AddWidgetMenuProps {
|
||||
state: ContextMenuState;
|
||||
onAdd: (typeId: WidgetTypeId, canvasX: number, canvasY: number) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const CATEGORY_ORDER = ["links", "info", "system", "monitoring"] as const;
|
||||
|
||||
const MARGIN = 8;
|
||||
|
||||
export function AddWidgetMenu({ state, onAdd, onClose }: AddWidgetMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const allTypes = useWidgetTypes();
|
||||
const [pos, setPos] = useState({ left: state.screenX, top: state.screenY });
|
||||
const [selectedCat, setSelectedCat] = useState<
|
||||
(typeof CATEGORY_ORDER)[number]
|
||||
>(CATEGORY_ORDER[0]);
|
||||
const categoryLabels: Record<(typeof CATEGORY_ORDER)[number], string> = {
|
||||
links: t("homepage.categoryLinks"),
|
||||
info: t("homepage.categoryInfo"),
|
||||
system: t("homepage.categorySystem"),
|
||||
monitoring: t("homepage.categoryMonitoring"),
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!state.visible || !menuRef.current) return;
|
||||
const rect = menuRef.current.getBoundingClientRect();
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
|
||||
let left = state.screenX;
|
||||
let top = state.screenY;
|
||||
|
||||
if (state.anchorRect) {
|
||||
left = state.anchorRect.right - rect.width;
|
||||
top = state.anchorRect.top - rect.height - MARGIN;
|
||||
}
|
||||
|
||||
if (left + rect.width > vw - MARGIN) left = vw - rect.width - MARGIN;
|
||||
if (left < MARGIN) left = MARGIN;
|
||||
|
||||
if (top < MARGIN) {
|
||||
top = state.anchorRect
|
||||
? state.anchorRect.bottom + MARGIN
|
||||
: state.screenY + MARGIN;
|
||||
}
|
||||
if (top + rect.height > vh - MARGIN) top = vh - rect.height - MARGIN;
|
||||
if (top < MARGIN) top = MARGIN;
|
||||
|
||||
setPos({ left, top });
|
||||
}, [state.visible, state.screenX, state.screenY, state.anchorRect]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handler);
|
||||
return () => document.removeEventListener("mousedown", handler);
|
||||
}, [onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
};
|
||||
document.addEventListener("keydown", handler);
|
||||
return () => document.removeEventListener("keydown", handler);
|
||||
}, [onClose]);
|
||||
|
||||
const visibleCategories = CATEGORY_ORDER.filter((cat) =>
|
||||
allTypes.some((type) => type.category === cat),
|
||||
);
|
||||
|
||||
const selectedTypes = allTypes.filter(
|
||||
(type) => type.category === selectedCat,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="fixed z-50 bg-card border border-border shadow-xl min-w-[220px] flex flex-col"
|
||||
style={{
|
||||
left: pos.left,
|
||||
top: pos.top,
|
||||
visibility: state.visible ? "visible" : "hidden",
|
||||
}}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-3 py-2 border-b border-border shrink-0">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-accent-brand">
|
||||
{t("homepage.addWidget")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Category tabs */}
|
||||
<div className="flex items-center gap-0.5 px-2 pt-2 pb-1 shrink-0">
|
||||
{visibleCategories.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setSelectedCat(cat)}
|
||||
className={`px-2.5 py-1 text-[10px] font-semibold uppercase tracking-wider transition-colors ${
|
||||
selectedCat === cat
|
||||
? "bg-accent-brand text-white"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/60"
|
||||
}`}
|
||||
>
|
||||
{categoryLabels[cat]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Widget list */}
|
||||
<div className="overflow-y-auto" style={{ maxHeight: "240px" }}>
|
||||
{selectedTypes.length === 0 ? (
|
||||
<div className="px-3 py-4 text-center text-xs text-muted-foreground/60">
|
||||
No widgets in this category
|
||||
</div>
|
||||
) : (
|
||||
selectedTypes.map((type) => (
|
||||
<button
|
||||
key={type.id}
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2 text-left text-xs text-foreground hover:bg-muted/60 transition-colors"
|
||||
onClick={() => {
|
||||
onAdd(type.id, state.canvasX, state.canvasY);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<span className="text-accent-brand shrink-0">{type.icon}</span>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="font-medium truncate">{type.name}</span>
|
||||
<span className="text-[10px] text-muted-foreground truncate">
|
||||
{type.description}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Input, Button } from "@termix/plugin-sdk/ui";
|
||||
|
||||
import { Trash2, Plus } from "lucide-react";
|
||||
import type {
|
||||
BookmarkListConfig,
|
||||
BookmarkLink,
|
||||
WidgetEditFormProps,
|
||||
} from "../types.js";
|
||||
|
||||
export function BookmarkListEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<BookmarkListConfig>) {
|
||||
const { t } = useTranslation();
|
||||
const { links } = config;
|
||||
|
||||
const updateLink = (i: number, patch: Partial<BookmarkLink>) => {
|
||||
const next = links.map((l, idx) => (idx === i ? { ...l, ...patch } : l));
|
||||
onChange({ ...config, links: next });
|
||||
};
|
||||
|
||||
const removeLink = (i: number) => {
|
||||
onChange({ ...config, links: links.filter((_, idx) => idx !== i) });
|
||||
};
|
||||
|
||||
const addLink = () => {
|
||||
onChange({ ...config, links: [...links, { label: "", url: "" }] });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 max-h-60 overflow-y-auto pr-1">
|
||||
{links.map((link, i) => (
|
||||
<div key={i} className="flex items-center gap-1.5">
|
||||
<Input
|
||||
value={link.label}
|
||||
onChange={(e) => updateLink(i, { label: e.target.value })}
|
||||
placeholder={t("homepage.linkLabel")}
|
||||
className="h-7 text-xs flex-1"
|
||||
/>
|
||||
<Input
|
||||
value={link.url}
|
||||
onChange={(e) => updateLink(i, { url: e.target.value })}
|
||||
placeholder="https://..."
|
||||
className="h-7 text-xs flex-1"
|
||||
/>
|
||||
<button
|
||||
onClick={() => removeLink(i)}
|
||||
className="p-1 hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addLink}
|
||||
className="mt-1 h-7 text-xs gap-1"
|
||||
>
|
||||
<Plus size={11} /> {t("homepage.addLink")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Input } from "@termix/plugin-sdk/ui";
|
||||
import type { CalendarConfig, WidgetEditFormProps } from "../types.js";
|
||||
|
||||
export function CalendarEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<CalendarConfig>) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.timezone")}
|
||||
</label>
|
||||
<Input
|
||||
value={config.timezone ?? ""}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, timezone: e.target.value || undefined })
|
||||
}
|
||||
placeholder="UTC"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.startOnMonday}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, startOnMonday: e.target.checked })
|
||||
}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{t("homepage.startOnMonday")}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Input, Switch } from "@termix/plugin-sdk/ui";
|
||||
|
||||
import type { ClockConfig, WidgetEditFormProps } from "../types.js";
|
||||
import { validateClockTimezone } from "../clock-timezone";
|
||||
|
||||
export function ClockEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<ClockConfig>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const invalidTimezone = !validateClockTimezone(config.timezone).valid;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.timezone")}
|
||||
</label>
|
||||
<Input
|
||||
value={config.timezone ?? ""}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, timezone: e.target.value || undefined })
|
||||
}
|
||||
placeholder="America/New_York (leave blank for local)"
|
||||
aria-invalid={invalidTimezone}
|
||||
className={
|
||||
invalidTimezone ? "h-8 text-sm border-destructive" : "h-8 text-sm"
|
||||
}
|
||||
/>
|
||||
{invalidTimezone && (
|
||||
<p className="text-[10px] text-destructive">
|
||||
{t("homepage.invalidTimezone")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs font-medium text-foreground">
|
||||
{t("homepage.showSeconds")}
|
||||
</label>
|
||||
<Switch
|
||||
checked={config.showSeconds}
|
||||
onCheckedChange={(v) => onChange({ ...config, showSeconds: v })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs font-medium text-foreground">
|
||||
{t("homepage.format12h")}
|
||||
</label>
|
||||
<Switch
|
||||
checked={config.format === "12h"}
|
||||
onCheckedChange={(v) =>
|
||||
onChange({ ...config, format: v ? "12h" : "24h" })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Input } from "@termix/plugin-sdk/ui";
|
||||
import type { CountdownConfig, WidgetEditFormProps } from "../types.js";
|
||||
|
||||
export function CountdownEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<CountdownConfig>) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.countdownLabel")}
|
||||
</label>
|
||||
<Input
|
||||
value={config.label}
|
||||
onChange={(e) => onChange({ ...config, label: e.target.value })}
|
||||
placeholder={t("homepage.countdownLabelPlaceholder")}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.targetDate")}
|
||||
</label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={config.targetDate}
|
||||
onChange={(e) => onChange({ ...config, targetDate: e.target.value })}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.showDays}
|
||||
onChange={(e) => onChange({ ...config, showDays: e.target.checked })}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{t("homepage.countdownShowDays")}
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.showHours}
|
||||
onChange={(e) => onChange({ ...config, showHours: e.target.checked })}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{t("homepage.countdownShowHours")}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Input } from "@termix/plugin-sdk/ui";
|
||||
import type {
|
||||
CustomApiConfig,
|
||||
CustomApiDisplayMode,
|
||||
WidgetEditFormProps,
|
||||
} from "../types.js";
|
||||
|
||||
const MODES: { id: CustomApiDisplayMode; label: string }[] = [
|
||||
{ id: "value", label: "Value" },
|
||||
{ id: "json", label: "JSON" },
|
||||
{ id: "table", label: "Table" },
|
||||
];
|
||||
|
||||
export function CustomApiEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<CustomApiConfig>) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.url")}
|
||||
</label>
|
||||
<Input
|
||||
value={config.url}
|
||||
onChange={(e) => onChange({ ...config, url: e.target.value })}
|
||||
placeholder="https://api.example.com/data"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.displayMode")}
|
||||
</label>
|
||||
<div className="flex gap-1">
|
||||
{MODES.map((m) => (
|
||||
<button
|
||||
key={m.id}
|
||||
type="button"
|
||||
onClick={() => onChange({ ...config, displayMode: m.id })}
|
||||
className={`px-2 py-0.5 text-[10px] border transition-colors ${config.displayMode === m.id ? "bg-accent-brand border-accent-brand text-white" : "border-border text-muted-foreground"}`}
|
||||
>
|
||||
{m.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config.displayMode === "value" && (
|
||||
<>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.displayField")}
|
||||
</label>
|
||||
<Input
|
||||
value={config.displayField ?? ""}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...config,
|
||||
displayField: e.target.value || undefined,
|
||||
})
|
||||
}
|
||||
placeholder="data.temperature"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col gap-1 flex-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.customApiLabel")}
|
||||
</label>
|
||||
<Input
|
||||
value={config.label ?? ""}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, label: e.target.value || undefined })
|
||||
}
|
||||
placeholder={t("homepage.customApiLabelPlaceholder")}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 w-20">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.customApiUnit")}
|
||||
</label>
|
||||
<Input
|
||||
value={config.unit ?? ""}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, unit: e.target.value || undefined })
|
||||
}
|
||||
placeholder="°C"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{config.displayMode === "table" && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.jsonPath")}
|
||||
</label>
|
||||
<Input
|
||||
value={config.jsonPath ?? ""}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, jsonPath: e.target.value || undefined })
|
||||
}
|
||||
placeholder="data.items"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.refreshInterval")} ({t("homepage.seconds")})
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={10}
|
||||
max={86400}
|
||||
value={config.refreshInterval}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...config,
|
||||
refreshInterval: Math.max(10, Number(e.target.value)),
|
||||
})
|
||||
}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Input } from "@termix/plugin-sdk/ui";
|
||||
import type { DashboardLinksConfig, WidgetEditFormProps } from "../types.js";
|
||||
|
||||
export function DashboardLinksEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<DashboardLinksConfig>) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.columns")}
|
||||
</label>
|
||||
<div className="flex gap-1">
|
||||
{([1, 2, 3] as const).map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
type="button"
|
||||
onClick={() => onChange({ ...config, columns: c })}
|
||||
className={`px-3 py-0.5 text-[10px] border transition-colors ${config.columns === c ? "bg-accent-brand border-accent-brand text-white" : "border-border text-muted-foreground"}`}
|
||||
>
|
||||
{c}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.maxItems")}
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={100}
|
||||
value={config.maxItems ?? ""}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...config,
|
||||
maxItems: e.target.value ? Number(e.target.value) : undefined,
|
||||
})
|
||||
}
|
||||
placeholder={t("homepage.noLimit")}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.showIcons}
|
||||
onChange={(e) => onChange({ ...config, showIcons: e.target.checked })}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{t("homepage.showImage")}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Input } from "@termix/plugin-sdk/ui";
|
||||
import type { FolderConfig, WidgetEditFormProps } from "../types.js";
|
||||
|
||||
function getAccentColor(): string {
|
||||
return (
|
||||
getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--accent-brand")
|
||||
.trim() || "#f59145"
|
||||
);
|
||||
}
|
||||
|
||||
export function FolderEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<FolderConfig>) {
|
||||
const { t } = useTranslation();
|
||||
const accent = getAccentColor();
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.color")}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="color"
|
||||
value={config.color || accent}
|
||||
onChange={(e) => onChange({ ...config, color: e.target.value })}
|
||||
className="w-8 h-8 border border-border cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
value={config.color ?? ""}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, color: e.target.value || undefined })
|
||||
}
|
||||
placeholder={accent}
|
||||
className="h-8 text-sm flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useHosts, useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import type { HostGridConfig, WidgetEditFormProps } from "../types.js";
|
||||
|
||||
export function HostGridEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<HostGridConfig>) {
|
||||
const { t } = useTranslation();
|
||||
const { hosts } = useHosts();
|
||||
|
||||
const toggleHost = (id: number) => {
|
||||
const next = config.hostIds.includes(id)
|
||||
? config.hostIds.filter((x) => x !== id)
|
||||
: [...config.hostIds, id];
|
||||
onChange({ ...config, hostIds: next });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.hostGridHosts")}
|
||||
</label>
|
||||
<div className="flex flex-col gap-0.5 max-h-40 overflow-y-auto border border-border/40 p-1">
|
||||
{hosts.length === 0 && (
|
||||
<span className="text-[10px] text-muted-foreground p-1">
|
||||
{t("homepage.noHosts")}
|
||||
</span>
|
||||
)}
|
||||
{hosts.map((h) => {
|
||||
const id = Number(h.id);
|
||||
return (
|
||||
<label
|
||||
key={h.id}
|
||||
className="flex items-center gap-2 text-[10px] cursor-pointer px-1 py-0.5 hover:bg-muted/40"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.hostIds.includes(id)}
|
||||
onChange={() => toggleHost(id)}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{h.name}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<span className="text-[9px] text-muted-foreground">
|
||||
{t("homepage.hostGridAllHint")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.columns")}
|
||||
</label>
|
||||
<div className="flex gap-1">
|
||||
{([2, 3, 4] as const).map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
type="button"
|
||||
onClick={() => onChange({ ...config, columns: c })}
|
||||
className={`px-3 py-0.5 text-[10px] font-medium border transition-colors ${config.columns === c ? "bg-accent-brand border-accent-brand text-white" : "border-border text-muted-foreground"}`}
|
||||
>
|
||||
{c}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.showIp}
|
||||
onChange={(e) => onChange({ ...config, showIp: e.target.checked })}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{t("homepage.showIp")}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { useHosts, useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import type {
|
||||
HostMetricKey,
|
||||
HostStatusConfig,
|
||||
WidgetEditFormProps,
|
||||
} from "../types.js";
|
||||
import { Select2 } from "@termix/plugin-sdk/ui";
|
||||
|
||||
const METRIC_OPTIONS: { key: HostMetricKey; labelKey: string }[] = [
|
||||
{ key: "cpu", labelKey: "homepage.metricCpu" },
|
||||
{ key: "memory", labelKey: "homepage.metricMemory" },
|
||||
{ key: "disk", labelKey: "homepage.metricDisk" },
|
||||
{ key: "uptime", labelKey: "homepage.metricUptime" },
|
||||
{ key: "system", labelKey: "homepage.metricSystem" },
|
||||
{ key: "network", labelKey: "homepage.metricNetwork" },
|
||||
{ key: "processes", labelKey: "homepage.metricProcesses" },
|
||||
];
|
||||
|
||||
export function HostStatusEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<HostStatusConfig>) {
|
||||
const { t } = useTranslation();
|
||||
const { hosts: allHosts } = useHosts();
|
||||
const hosts = allHosts.filter((h) => h.statusCheckEnabled !== false);
|
||||
|
||||
const shownMetrics: HostMetricKey[] = config.shownMetrics?.length
|
||||
? config.shownMetrics
|
||||
: config.showMetrics !== false
|
||||
? ["cpu", "memory", ...(config.showDisk ? ["disk" as HostMetricKey] : [])]
|
||||
: [];
|
||||
|
||||
function toggleMetric(key: HostMetricKey) {
|
||||
const next = shownMetrics.includes(key)
|
||||
? shownMetrics.filter((k) => k !== key)
|
||||
: [...shownMetrics, key];
|
||||
onChange({ ...config, shownMetrics: next });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("common.host")}
|
||||
</label>
|
||||
<Select2
|
||||
className="h-8 border border-border bg-background text-sm px-2"
|
||||
value={config.hostId || ""}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, hostId: parseInt(e.target.value) || 0 })
|
||||
}
|
||||
>
|
||||
<option value="">{t("common.selectHost")}</option>
|
||||
{hosts.map((h) => (
|
||||
<option key={h.id} value={h.id}>
|
||||
{h.name}
|
||||
</option>
|
||||
))}
|
||||
</Select2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.displayedMetrics")}
|
||||
</label>
|
||||
<div className="flex flex-col gap-1">
|
||||
{METRIC_OPTIONS.map(({ key, labelKey }) => (
|
||||
<label
|
||||
key={key}
|
||||
className="flex items-center gap-2 cursor-pointer select-none"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={shownMetrics.includes(key)}
|
||||
onChange={() => toggleMetric(key)}
|
||||
className="w-3.5 h-3.5 accent-primary"
|
||||
/>
|
||||
<span className="text-xs text-foreground">{t(labelKey)}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Input, Switch } from "@termix/plugin-sdk/ui";
|
||||
|
||||
import type { IframeConfig, WidgetEditFormProps } from "../types.js";
|
||||
|
||||
export function IframeEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<IframeConfig>) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.url")}
|
||||
</label>
|
||||
<Input
|
||||
value={config.url}
|
||||
onChange={(e) => onChange({ ...config, url: e.target.value })}
|
||||
placeholder="https://example.com"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs font-medium text-foreground">
|
||||
{t("homepage.scrolling")}
|
||||
</label>
|
||||
<Switch
|
||||
checked={config.scrolling}
|
||||
onCheckedChange={(v) => onChange({ ...config, scrolling: v })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Input } from "@termix/plugin-sdk/ui";
|
||||
import type { ImageWidgetConfig, WidgetEditFormProps } from "../types.js";
|
||||
|
||||
export function ImageWidgetEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<ImageWidgetConfig>) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.imageUrl")}
|
||||
</label>
|
||||
<Input
|
||||
value={config.imageUrl}
|
||||
onChange={(e) => onChange({ ...config, imageUrl: e.target.value })}
|
||||
placeholder="https://example.com/image.png"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.imageLinkUrl")}
|
||||
</label>
|
||||
<Input
|
||||
value={config.linkUrl ?? ""}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, linkUrl: e.target.value || undefined })
|
||||
}
|
||||
placeholder="https://example.com"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.imageFit")}
|
||||
</label>
|
||||
<div className="flex gap-1">
|
||||
{(["contain", "cover", "fill"] as const).map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
type="button"
|
||||
onClick={() => onChange({ ...config, fit: f })}
|
||||
className={`px-2 py-0.5 text-[10px] border transition-colors capitalize ${config.fit === f ? "bg-accent-brand border-accent-brand text-white" : "border-border text-muted-foreground"}`}
|
||||
>
|
||||
{f}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.altText")}
|
||||
</label>
|
||||
<Input
|
||||
value={config.alt ?? ""}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, alt: e.target.value || undefined })
|
||||
}
|
||||
placeholder={t("homepage.altTextPlaceholder")}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Trash2, Plus } from "lucide-react";
|
||||
import { Input, Button } from "@termix/plugin-sdk/ui";
|
||||
|
||||
import type {
|
||||
LinkTreeConfig,
|
||||
LinkTreeSection,
|
||||
LinkTreeLink,
|
||||
WidgetEditFormProps,
|
||||
} from "../types.js";
|
||||
|
||||
export function LinkTreeEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<LinkTreeConfig>) {
|
||||
const { t } = useTranslation();
|
||||
const { sections, compact } = config;
|
||||
|
||||
const updateSection = (si: number, patch: Partial<LinkTreeSection>) => {
|
||||
onChange({
|
||||
...config,
|
||||
sections: sections.map((s, i) => (i === si ? { ...s, ...patch } : s)),
|
||||
});
|
||||
};
|
||||
const removeSection = (si: number) =>
|
||||
onChange({ ...config, sections: sections.filter((_, i) => i !== si) });
|
||||
const addSection = () =>
|
||||
onChange({
|
||||
...config,
|
||||
sections: [...sections, { heading: "", links: [] }],
|
||||
});
|
||||
|
||||
const updateLink = (si: number, li: number, patch: Partial<LinkTreeLink>) => {
|
||||
const newSections = sections.map((s, i) => {
|
||||
if (i !== si) return s;
|
||||
return {
|
||||
...s,
|
||||
links: s.links.map((l, j) => (j === li ? { ...l, ...patch } : l)),
|
||||
};
|
||||
});
|
||||
onChange({ ...config, sections: newSections });
|
||||
};
|
||||
const removeLink = (si: number, li: number) => {
|
||||
const newSections = sections.map((s, i) =>
|
||||
i !== si ? s : { ...s, links: s.links.filter((_, j) => j !== li) },
|
||||
);
|
||||
onChange({ ...config, sections: newSections });
|
||||
};
|
||||
const addLink = (si: number) => {
|
||||
const newSections = sections.map((s, i) =>
|
||||
i !== si ? s : { ...s, links: [...s.links, { label: "", url: "" }] },
|
||||
);
|
||||
onChange({ ...config, sections: newSections });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-2 max-h-64 overflow-y-auto pr-1">
|
||||
{sections.map((section, si) => (
|
||||
<div
|
||||
key={si}
|
||||
className="flex flex-col gap-1 border border-border/40 p-2"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
value={section.heading}
|
||||
onChange={(e) => updateSection(si, { heading: e.target.value })}
|
||||
placeholder={t("homepage.sectionHeading")}
|
||||
className="h-6 text-xs flex-1 font-semibold"
|
||||
/>
|
||||
<button
|
||||
onClick={() => removeSection(si)}
|
||||
className="p-1 hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 pl-2">
|
||||
{section.links.map((link, li) => (
|
||||
<div key={li} className="flex items-center gap-1">
|
||||
<Input
|
||||
value={link.label}
|
||||
onChange={(e) =>
|
||||
updateLink(si, li, { label: e.target.value })
|
||||
}
|
||||
placeholder={t("homepage.linkLabel")}
|
||||
className="h-6 text-[10px] flex-1"
|
||||
/>
|
||||
<Input
|
||||
value={link.url}
|
||||
onChange={(e) =>
|
||||
updateLink(si, li, { url: e.target.value })
|
||||
}
|
||||
placeholder="https://..."
|
||||
className="h-6 text-[10px] flex-1"
|
||||
/>
|
||||
<button
|
||||
onClick={() => removeLink(si, li)}
|
||||
className="p-0.5 hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 size={10} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={() => addLink(si)}
|
||||
className="flex items-center gap-0.5 text-[9px] text-accent-brand hover:opacity-80 mt-0.5 self-start"
|
||||
>
|
||||
<Plus size={9} /> {t("homepage.addLink")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addSection}
|
||||
className="mt-1 h-7 text-xs gap-1"
|
||||
>
|
||||
<Plus size={11} /> {t("homepage.addSection")}
|
||||
</Button>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={compact}
|
||||
onChange={(e) => onChange({ ...config, compact: e.target.checked })}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{t("homepage.compactMode")}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Input, Textarea } from "@termix/plugin-sdk/ui";
|
||||
|
||||
import type { MarkdownNotesConfig, WidgetEditFormProps } from "../types.js";
|
||||
|
||||
export function MarkdownNotesEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<MarkdownNotesConfig>) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.content")}
|
||||
</label>
|
||||
<Textarea
|
||||
value={config.content}
|
||||
onChange={(e) => onChange({ ...config, content: e.target.value })}
|
||||
className="text-sm min-h-[120px] resize-none rounded-none font-mono"
|
||||
/>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.renderMarkdown}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, renderMarkdown: e.target.checked })
|
||||
}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{t("homepage.renderMarkdown")}
|
||||
</label>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.backgroundColor")}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="color"
|
||||
value={config.backgroundColor ?? "#1e1e2e"}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, backgroundColor: e.target.value })
|
||||
}
|
||||
className="w-8 h-8 border border-border cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
value={config.backgroundColor ?? ""}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...config,
|
||||
backgroundColor: e.target.value || undefined,
|
||||
})
|
||||
}
|
||||
placeholder="#1e1e2e"
|
||||
className="h-8 text-sm flex-1"
|
||||
/>
|
||||
{config.backgroundColor && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onChange({ ...config, backgroundColor: undefined })
|
||||
}
|
||||
className="text-[10px] text-muted-foreground hover:text-foreground shrink-0"
|
||||
>
|
||||
{t("homepage.clearColor")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Textarea } from "@termix/plugin-sdk/ui";
|
||||
import type { NotesConfig, WidgetEditFormProps } from "../types.js";
|
||||
|
||||
export function NotesEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<NotesConfig>) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.content")}
|
||||
</label>
|
||||
<Textarea
|
||||
value={config.content}
|
||||
onChange={(e) => onChange({ ...config, content: e.target.value })}
|
||||
placeholder="Write your notes here..."
|
||||
className="text-sm min-h-[120px] resize-none rounded-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Trash2, Plus } from "lucide-react";
|
||||
import { Input, Button } from "@termix/plugin-sdk/ui";
|
||||
|
||||
import type {
|
||||
PingStatusConfig,
|
||||
PingUrl,
|
||||
WidgetEditFormProps,
|
||||
} from "../types.js";
|
||||
|
||||
export function PingStatusEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<PingStatusConfig>) {
|
||||
const { t } = useTranslation();
|
||||
const { urls, refreshInterval, showLatency } = config;
|
||||
|
||||
const updateUrl = (i: number, patch: Partial<PingUrl>) => {
|
||||
onChange({
|
||||
...config,
|
||||
urls: urls.map((u, idx) => (idx === i ? { ...u, ...patch } : u)),
|
||||
});
|
||||
};
|
||||
const removeUrl = (i: number) =>
|
||||
onChange({ ...config, urls: urls.filter((_, idx) => idx !== i) });
|
||||
const addUrl = () =>
|
||||
onChange({ ...config, urls: [...urls, { label: "", url: "" }] });
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-2 max-h-48 overflow-y-auto pr-1">
|
||||
{urls.map((u, i) => (
|
||||
<div key={i} className="flex items-center gap-1.5">
|
||||
<Input
|
||||
value={u.label}
|
||||
onChange={(e) => updateUrl(i, { label: e.target.value })}
|
||||
placeholder={t("homepage.pingLabel")}
|
||||
className="h-7 text-xs flex-1"
|
||||
/>
|
||||
<Input
|
||||
value={u.url}
|
||||
onChange={(e) => updateUrl(i, { url: e.target.value })}
|
||||
placeholder="https://..."
|
||||
className="h-7 text-xs flex-1"
|
||||
/>
|
||||
<button
|
||||
onClick={() => removeUrl(i)}
|
||||
className="p-1 hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addUrl}
|
||||
className="mt-1 h-7 text-xs gap-1"
|
||||
>
|
||||
<Plus size={11} /> {t("homepage.addPingUrl")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.refreshInterval")} ({t("homepage.seconds")})
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={10}
|
||||
max={3600}
|
||||
value={refreshInterval}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...config,
|
||||
refreshInterval: Math.max(10, Number(e.target.value)),
|
||||
})
|
||||
}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showLatency}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, showLatency: e.target.checked })
|
||||
}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{t("homepage.showLatency")}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
useHostActions,
|
||||
useHosts,
|
||||
useTranslation,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import type {
|
||||
QuickConnectConfig,
|
||||
QuickConnectType,
|
||||
WidgetEditFormProps,
|
||||
} from "../types.js";
|
||||
import { quickConnectTargets } from "../quick-connect-targets.js";
|
||||
|
||||
export function QuickConnectEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<QuickConnectConfig>) {
|
||||
const { t } = useTranslation();
|
||||
const { hosts } = useHosts();
|
||||
const targets = quickConnectTargets(useHostActions());
|
||||
|
||||
const toggleHost = (id: number) => {
|
||||
const current = config.hostIds;
|
||||
const ids = current.includes(id)
|
||||
? current.filter((x) => x !== id)
|
||||
: [...current, id];
|
||||
onChange({ ...config, hostIds: ids });
|
||||
};
|
||||
|
||||
const toggleType = (type: QuickConnectType) => {
|
||||
const types = config.connectionTypes.includes(type)
|
||||
? config.connectionTypes.filter((x) => x !== type)
|
||||
: [...config.connectionTypes, type];
|
||||
onChange({ ...config, connectionTypes: types });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.selectHosts")}
|
||||
</label>
|
||||
<div className="flex flex-col gap-1 max-h-36 overflow-y-auto border border-border/30 p-2">
|
||||
{hosts.length === 0 && (
|
||||
<span className="text-xs text-muted-foreground/60">
|
||||
{t("homepage.noHosts")}
|
||||
</span>
|
||||
)}
|
||||
{hosts.map((h) => {
|
||||
const id = Number(h.id);
|
||||
return (
|
||||
<label
|
||||
key={h.id}
|
||||
className="flex items-center gap-2 text-xs cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.hostIds.includes(id)}
|
||||
onChange={() => toggleHost(id)}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
<span className="truncate">{h.name || h.ip}</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground/60">
|
||||
{t("homepage.hostGridAllHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.connectionTypes")}
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{targets.map(({ type, labelKey }) => (
|
||||
<label
|
||||
key={type}
|
||||
className="flex items-center gap-2 text-xs cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.connectionTypes.includes(type)}
|
||||
onChange={() => toggleType(type)}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{t(labelKey, type)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.layout")}
|
||||
</label>
|
||||
<div className="flex gap-3">
|
||||
{(["list", "grid"] as const).map((l) => (
|
||||
<label
|
||||
key={l}
|
||||
className="flex items-center gap-1.5 text-xs cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="layout"
|
||||
value={l}
|
||||
checked={config.layout === l}
|
||||
onChange={() => onChange({ ...config, layout: l })}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{l === "list"
|
||||
? t("homepage.listLayout")
|
||||
: t("homepage.gridLayout")}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.showStatus}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, showStatus: e.target.checked })
|
||||
}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{t("homepage.showStatus")}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useActivityTypes, useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Input } from "@termix/plugin-sdk/ui";
|
||||
import type {
|
||||
RecentActivityConfig,
|
||||
ActivityType,
|
||||
WidgetEditFormProps,
|
||||
} from "../types.js";
|
||||
|
||||
export function RecentActivityEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<RecentActivityConfig>) {
|
||||
const { t } = useTranslation();
|
||||
const activityTypes = useActivityTypes();
|
||||
|
||||
const toggleType = (type: ActivityType) => {
|
||||
const next = config.filterTypes.includes(type)
|
||||
? config.filterTypes.filter((x) => x !== type)
|
||||
: [...config.filterTypes, type];
|
||||
onChange({ ...config, filterTypes: next });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.maxItems")}
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={50}
|
||||
value={config.maxItems}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...config,
|
||||
maxItems: Math.max(1, Number(e.target.value)),
|
||||
})
|
||||
}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.filterActivityTypes")}
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{activityTypes.map((type) => (
|
||||
<button
|
||||
key={type}
|
||||
type="button"
|
||||
onClick={() => toggleType(type)}
|
||||
className={`px-2 py-0.5 text-[10px] border transition-colors ${
|
||||
config.filterTypes.includes(type) ||
|
||||
config.filterTypes.length === 0
|
||||
? "bg-accent-brand border-accent-brand text-white"
|
||||
: "border-border text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{type.replace("_", " ")}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-[9px] text-muted-foreground">
|
||||
{t("homepage.filterTypesHint")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.showTimestamp}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, showTimestamp: e.target.checked })
|
||||
}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{t("homepage.showTimestamp")}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Input, Switch } from "@termix/plugin-sdk/ui";
|
||||
|
||||
import type { RssFeedConfig, WidgetEditFormProps } from "../types.js";
|
||||
|
||||
export function RssFeedEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<RssFeedConfig>) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.feedUrl")}
|
||||
</label>
|
||||
<Input
|
||||
value={config.feedUrl}
|
||||
onChange={(e) => onChange({ ...config, feedUrl: e.target.value })}
|
||||
placeholder="https://example.com/feed.xml"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.maxItems")}
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={50}
|
||||
value={config.maxItems}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...config,
|
||||
maxItems: Math.max(1, Math.min(50, Number(e.target.value))),
|
||||
})
|
||||
}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs font-medium text-foreground">
|
||||
{t("homepage.showDescription")}
|
||||
</label>
|
||||
<Switch
|
||||
checked={config.showDescription}
|
||||
onCheckedChange={(v) => onChange({ ...config, showDescription: v })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Input } from "@termix/plugin-sdk/ui";
|
||||
import type {
|
||||
SearchBarConfig,
|
||||
SearchEngine,
|
||||
WidgetEditFormProps,
|
||||
} from "../types.js";
|
||||
|
||||
const ENGINES: { id: SearchEngine; label: string }[] = [
|
||||
{ id: "google", label: "Google" },
|
||||
{ id: "duckduckgo", label: "DuckDuckGo" },
|
||||
{ id: "bing", label: "Bing" },
|
||||
{ id: "custom", label: "Custom" },
|
||||
];
|
||||
|
||||
export function SearchBarEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<SearchBarConfig>) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.searchEngine")}
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{ENGINES.map((e) => (
|
||||
<button
|
||||
key={e.id}
|
||||
type="button"
|
||||
onClick={() => onChange({ ...config, engine: e.id })}
|
||||
className={`px-2 py-0.5 text-[10px] border transition-colors ${config.engine === e.id ? "bg-accent-brand border-accent-brand text-white" : "border-border text-muted-foreground"}`}
|
||||
>
|
||||
{e.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config.engine === "custom" && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.customSearchUrl")}
|
||||
</label>
|
||||
<Input
|
||||
value={config.customUrl ?? ""}
|
||||
onChange={(e) => onChange({ ...config, customUrl: e.target.value })}
|
||||
placeholder="https://example.com/search?q={q}"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.searchPlaceholderLabel")}
|
||||
</label>
|
||||
<Input
|
||||
value={config.placeholder ?? ""}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, placeholder: e.target.value || undefined })
|
||||
}
|
||||
placeholder={t("homepage.searchPlaceholderHint")}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.openInNewTab}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, openInNewTab: e.target.checked })
|
||||
}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{t("homepage.openInNewTab")}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Trash2, Plus } from "lucide-react";
|
||||
import { Input, Button } from "@termix/plugin-sdk/ui";
|
||||
|
||||
import type {
|
||||
SearchLinksConfig,
|
||||
SearchLinkShortcut,
|
||||
WidgetEditFormProps,
|
||||
} from "../types.js";
|
||||
|
||||
export function SearchLinksEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<SearchLinksConfig>) {
|
||||
const { t } = useTranslation();
|
||||
const { shortcuts } = config;
|
||||
|
||||
const update = (i: number, patch: Partial<SearchLinkShortcut>) => {
|
||||
onChange({
|
||||
...config,
|
||||
shortcuts: shortcuts.map((s, idx) =>
|
||||
idx === i ? { ...s, ...patch } : s,
|
||||
),
|
||||
});
|
||||
};
|
||||
const remove = (i: number) =>
|
||||
onChange({ ...config, shortcuts: shortcuts.filter((_, idx) => idx !== i) });
|
||||
const add = () =>
|
||||
onChange({
|
||||
...config,
|
||||
shortcuts: [...shortcuts, { label: "", queryTemplate: "" }],
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-2 max-h-56 overflow-y-auto pr-1">
|
||||
{shortcuts.map((s, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex flex-col gap-1 border border-border/40 p-2"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
value={s.icon ?? ""}
|
||||
onChange={(e) =>
|
||||
update(i, { icon: e.target.value || undefined })
|
||||
}
|
||||
placeholder="🔍"
|
||||
className="h-6 text-xs w-10 text-center"
|
||||
/>
|
||||
<Input
|
||||
value={s.label}
|
||||
onChange={(e) => update(i, { label: e.target.value })}
|
||||
placeholder={t("homepage.linkLabel")}
|
||||
className="h-6 text-xs flex-1"
|
||||
/>
|
||||
<button
|
||||
onClick={() => remove(i)}
|
||||
className="p-1 hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</div>
|
||||
<Input
|
||||
value={s.queryTemplate}
|
||||
onChange={(e) => update(i, { queryTemplate: e.target.value })}
|
||||
placeholder="https://example.com/search?q={q}"
|
||||
className="h-6 text-xs"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={add}
|
||||
className="mt-1 h-7 text-xs gap-1"
|
||||
>
|
||||
<Plus size={11} /> {t("homepage.addSearchShortcut")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Trash2, Plus } from "lucide-react";
|
||||
import { Input, Button } from "@termix/plugin-sdk/ui";
|
||||
|
||||
import type {
|
||||
ServiceGridConfig,
|
||||
ServiceGridItem,
|
||||
WidgetEditFormProps,
|
||||
} from "../types.js";
|
||||
|
||||
export function ServiceGridEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<ServiceGridConfig>) {
|
||||
const { t } = useTranslation();
|
||||
const { services, columns, showLabels, iconSize } = config;
|
||||
|
||||
const updateItem = (i: number, patch: Partial<ServiceGridItem>) => {
|
||||
onChange({
|
||||
...config,
|
||||
services: services.map((s, idx) => (idx === i ? { ...s, ...patch } : s)),
|
||||
});
|
||||
};
|
||||
const removeItem = (i: number) =>
|
||||
onChange({ ...config, services: services.filter((_, idx) => idx !== i) });
|
||||
const addItem = () =>
|
||||
onChange({ ...config, services: [...services, { label: "", url: "" }] });
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1.5 max-h-48 overflow-y-auto pr-1">
|
||||
{services.map((s, i) => (
|
||||
<div key={i} className="flex items-center gap-1.5">
|
||||
<Input
|
||||
value={s.label}
|
||||
onChange={(e) => updateItem(i, { label: e.target.value })}
|
||||
placeholder={t("homepage.linkLabel")}
|
||||
className="h-7 text-xs flex-1"
|
||||
/>
|
||||
<Input
|
||||
value={s.url}
|
||||
onChange={(e) => updateItem(i, { url: e.target.value })}
|
||||
placeholder="https://..."
|
||||
className="h-7 text-xs flex-1"
|
||||
/>
|
||||
<button
|
||||
onClick={() => removeItem(i)}
|
||||
className="p-1 hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addItem}
|
||||
className="mt-1 h-7 text-xs gap-1"
|
||||
>
|
||||
<Plus size={11} /> {t("homepage.addService")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.columns")}
|
||||
</label>
|
||||
<div className="flex gap-1">
|
||||
{([2, 3, 4] as const).map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
type="button"
|
||||
onClick={() => onChange({ ...config, columns: c })}
|
||||
className={`px-3 py-0.5 text-[10px] border transition-colors ${columns === c ? "bg-accent-brand border-accent-brand text-white" : "border-border text-muted-foreground"}`}
|
||||
>
|
||||
{c}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.iconSize")}
|
||||
</label>
|
||||
<div className="flex gap-1">
|
||||
{(["sm", "md", "lg"] as const).map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
type="button"
|
||||
onClick={() => onChange({ ...config, iconSize: s })}
|
||||
className={`px-2 py-0.5 text-[10px] border transition-colors uppercase ${iconSize === s ? "bg-accent-brand border-accent-brand text-white" : "border-border text-muted-foreground"}`}
|
||||
>
|
||||
{s}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showLabels}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, showLabels: e.target.checked })
|
||||
}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{t("homepage.showLabels")}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Input, Switch } from "@termix/plugin-sdk/ui";
|
||||
|
||||
import type { ServiceLinkConfig, WidgetEditFormProps } from "../types.js";
|
||||
|
||||
function getAccentColor(): string {
|
||||
return (
|
||||
getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--accent-brand")
|
||||
.trim() || "#f59145"
|
||||
);
|
||||
}
|
||||
|
||||
export function ServiceLinkEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<ServiceLinkConfig>) {
|
||||
const { t } = useTranslation();
|
||||
const accent = getAccentColor();
|
||||
const showImage = config.showImage ?? true;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.url")}
|
||||
</label>
|
||||
<Input
|
||||
value={config.url}
|
||||
onChange={(e) => onChange({ ...config, url: e.target.value })}
|
||||
placeholder="https://example.com"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.description")}
|
||||
</label>
|
||||
<Input
|
||||
value={config.description ?? ""}
|
||||
onChange={(e) => onChange({ ...config, description: e.target.value })}
|
||||
placeholder="My service"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs font-medium text-foreground">
|
||||
{t("homepage.showImage")}
|
||||
</label>
|
||||
<Switch
|
||||
checked={showImage}
|
||||
onCheckedChange={(v) => onChange({ ...config, showImage: v })}
|
||||
/>
|
||||
</div>
|
||||
{showImage && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.imageUrl")}
|
||||
</label>
|
||||
<Input
|
||||
value={config.imageUrl ?? ""}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, imageUrl: e.target.value || undefined })
|
||||
}
|
||||
placeholder="https://example.com/logo.png"
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
<span className="text-[10px] text-muted-foreground/60">
|
||||
{t("homepage.imageUrlHint")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.color")}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="color"
|
||||
value={config.accentColor || accent}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, accentColor: e.target.value })
|
||||
}
|
||||
className="w-8 h-8 border border-border cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
value={config.accentColor ?? ""}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, accentColor: e.target.value || undefined })
|
||||
}
|
||||
placeholder={accent}
|
||||
className="h-8 text-sm flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
useTranslation,
|
||||
useHosts,
|
||||
type PluginHostRecord,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { Select2 } from "@termix/plugin-sdk/ui";
|
||||
|
||||
interface SingleHostEditFormProps {
|
||||
hostId: number;
|
||||
onChange: (hostId: number) => void;
|
||||
filter?: (host: PluginHostRecord) => boolean;
|
||||
}
|
||||
|
||||
export function SingleHostEditForm({
|
||||
hostId,
|
||||
onChange,
|
||||
filter,
|
||||
}: SingleHostEditFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const { hosts: allHosts } = useHosts();
|
||||
const hosts = filter ? allHosts.filter(filter) : allHosts;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("common.host")}
|
||||
</label>
|
||||
<Select2
|
||||
value={hostId || ""}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
className="h-8 text-xs border border-border bg-background px-2"
|
||||
>
|
||||
<option value="">{t("common.selectHost")}</option>
|
||||
{hosts.map((h) => (
|
||||
<option key={h.id} value={h.id}>
|
||||
{h.name || h.ip}
|
||||
</option>
|
||||
))}
|
||||
</Select2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import type { SshTerminalConfig, WidgetEditFormProps } from "../types.js";
|
||||
import { SingleHostEditForm } from "./SingleHostEditForm";
|
||||
|
||||
export function SshTerminalEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<SshTerminalConfig>) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<SingleHostEditForm
|
||||
hostId={config.hostId}
|
||||
onChange={(hostId) => onChange({ ...config, hostId })}
|
||||
filter={(h) => !!(h.enableSsh && h.enableTerminal)}
|
||||
/>
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.autoConnect}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, autoConnect: e.target.checked })
|
||||
}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{t("homepage.sshTerminalAutoConnect")}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import type { SystemOverviewConfig, WidgetEditFormProps } from "../types.js";
|
||||
|
||||
export function SystemOverviewEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<SystemOverviewConfig>) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.showVersion}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, showVersion: e.target.checked })
|
||||
}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{t("homepage.overviewVersion")}
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.showDbHealth}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, showDbHealth: e.target.checked })
|
||||
}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{t("homepage.overviewDatabase")}
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.showUptime}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, showUptime: e.target.checked })
|
||||
}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{t("homepage.overviewUptime")}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import type { TermixUptimeConfig, WidgetEditFormProps } from "../types.js";
|
||||
|
||||
export function TermixUptimeEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<TermixUptimeConfig>) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.showDetailed}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, showDetailed: e.target.checked })
|
||||
}
|
||||
className="accent-accent-brand"
|
||||
/>
|
||||
{t("homepage.showSeconds")}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Input, Textarea } from "@termix/plugin-sdk/ui";
|
||||
|
||||
import type {
|
||||
TextBannerConfig,
|
||||
TextBannerFontSize,
|
||||
TextBannerAlign,
|
||||
TextBannerWeight,
|
||||
WidgetEditFormProps,
|
||||
} from "../types.js";
|
||||
|
||||
const SIZES: { id: TextBannerFontSize; label: string }[] = [
|
||||
{ id: "sm", label: "SM" },
|
||||
{ id: "md", label: "MD" },
|
||||
{ id: "lg", label: "LG" },
|
||||
{ id: "xl", label: "XL" },
|
||||
];
|
||||
|
||||
const ALIGNS: { id: TextBannerAlign; label: string }[] = [
|
||||
{ id: "left", label: "Left" },
|
||||
{ id: "center", label: "Center" },
|
||||
{ id: "right", label: "Right" },
|
||||
];
|
||||
|
||||
const WEIGHTS: { id: TextBannerWeight; label: string }[] = [
|
||||
{ id: "normal", label: "Normal" },
|
||||
{ id: "semibold", label: "Semibold" },
|
||||
{ id: "bold", label: "Bold" },
|
||||
];
|
||||
|
||||
export function TextBannerEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<TextBannerConfig>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const ButtonGroup = <T extends string>({
|
||||
opts,
|
||||
value,
|
||||
onSelect,
|
||||
}: {
|
||||
opts: { id: T; label: string }[];
|
||||
value: T;
|
||||
onSelect: (v: T) => void;
|
||||
}) => (
|
||||
<div className="flex gap-1">
|
||||
{opts.map((o) => (
|
||||
<button
|
||||
key={o.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(o.id)}
|
||||
className={`px-2 py-0.5 text-[10px] border transition-colors ${value === o.id ? "bg-accent-brand border-accent-brand text-white" : "border-border text-muted-foreground"}`}
|
||||
>
|
||||
{o.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.content")}
|
||||
</label>
|
||||
<Textarea
|
||||
value={config.text}
|
||||
onChange={(e) => onChange({ ...config, text: e.target.value })}
|
||||
rows={2}
|
||||
className="text-sm min-h-[60px] resize-none rounded-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.fontSize")}
|
||||
</label>
|
||||
<ButtonGroup
|
||||
opts={SIZES}
|
||||
value={config.fontSize}
|
||||
onSelect={(v) => onChange({ ...config, fontSize: v })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.textAlign")}
|
||||
</label>
|
||||
<ButtonGroup
|
||||
opts={ALIGNS}
|
||||
value={config.textAlign}
|
||||
onSelect={(v) => onChange({ ...config, textAlign: v })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.fontWeight")}
|
||||
</label>
|
||||
<ButtonGroup
|
||||
opts={WEIGHTS}
|
||||
value={config.fontWeight}
|
||||
onSelect={(v) => onChange({ ...config, fontWeight: v })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.backgroundColor")}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="color"
|
||||
value={config.backgroundColor ?? "#1e1e2e"}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, backgroundColor: e.target.value })
|
||||
}
|
||||
className="w-8 h-8 border border-border cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
value={config.backgroundColor ?? ""}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...config,
|
||||
backgroundColor: e.target.value || undefined,
|
||||
})
|
||||
}
|
||||
placeholder="#1e1e2e"
|
||||
className="h-8 text-sm flex-1"
|
||||
/>
|
||||
{config.backgroundColor && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onChange({ ...config, backgroundColor: undefined })
|
||||
}
|
||||
className="text-[10px] text-muted-foreground hover:text-foreground shrink-0"
|
||||
>
|
||||
{t("homepage.clearColor")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Input, Switch } from "@termix/plugin-sdk/ui";
|
||||
|
||||
import type { WeatherConfig, WidgetEditFormProps } from "../types.js";
|
||||
|
||||
export function WeatherEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<WeatherConfig>) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.location")}
|
||||
</label>
|
||||
<Input
|
||||
value={config.location}
|
||||
onChange={(e) => onChange({ ...config, location: e.target.value })}
|
||||
placeholder="New York, London, Tokyo..."
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.temperatureUnit")}
|
||||
</label>
|
||||
<div className="flex gap-0 border border-border">
|
||||
{(["C", "F"] as const).map((u) => (
|
||||
<button
|
||||
key={u}
|
||||
type="button"
|
||||
onClick={() => onChange({ ...config, unit: u })}
|
||||
className={`flex-1 h-8 text-sm transition-colors ${
|
||||
config.unit === u
|
||||
? "bg-accent-brand text-white"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/60"
|
||||
}`}
|
||||
>
|
||||
°{u}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs font-medium text-foreground">
|
||||
{t("homepage.showForecast")}
|
||||
</label>
|
||||
<Switch
|
||||
checked={config.showForecast}
|
||||
onCheckedChange={(v) => onChange({ ...config, showForecast: v })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
import { useState } from "react";
|
||||
import { getWidgetType } from "../widgets/WidgetRegistry";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
Input,
|
||||
} from "@termix/plugin-sdk/ui";
|
||||
|
||||
import type {
|
||||
CanvasWidget,
|
||||
ServiceLinkConfig,
|
||||
FolderConfig,
|
||||
NotesConfig,
|
||||
BookmarkListConfig,
|
||||
ClockConfig,
|
||||
HostStatusConfig,
|
||||
WeatherConfig,
|
||||
IframeConfig,
|
||||
RssFeedConfig,
|
||||
HostGridConfig,
|
||||
PingStatusConfig,
|
||||
RecentActivityConfig,
|
||||
TermixUptimeConfig,
|
||||
SystemOverviewConfig,
|
||||
SshTerminalConfig,
|
||||
QuickConnectConfig,
|
||||
CalendarConfig,
|
||||
CountdownConfig,
|
||||
SearchBarConfig,
|
||||
TextBannerConfig,
|
||||
ImageWidgetConfig,
|
||||
MarkdownNotesConfig,
|
||||
CustomApiConfig,
|
||||
ServiceGridConfig,
|
||||
DashboardLinksConfig,
|
||||
SearchLinksConfig,
|
||||
LinkTreeConfig,
|
||||
} from "../types.js";
|
||||
import { ServiceLinkEditForm } from "./ServiceLinkEditForm";
|
||||
import { FolderEditForm } from "./FolderEditForm";
|
||||
import { NotesEditForm } from "./NotesEditForm";
|
||||
import { BookmarkListEditForm } from "./BookmarkListEditForm";
|
||||
import { ClockEditForm } from "./ClockEditForm";
|
||||
import { HostStatusEditForm } from "./HostStatusEditForm";
|
||||
import { WeatherEditForm } from "./WeatherEditForm";
|
||||
import { IframeEditForm } from "./IframeEditForm";
|
||||
import { RssFeedEditForm } from "./RssFeedEditForm";
|
||||
import { HostGridEditForm } from "./HostGridEditForm";
|
||||
import { PingStatusEditForm } from "./PingStatusEditForm";
|
||||
import { RecentActivityEditForm } from "./RecentActivityEditForm";
|
||||
import { TermixUptimeEditForm } from "./TermixUptimeEditForm";
|
||||
import { SystemOverviewEditForm } from "./SystemOverviewEditForm";
|
||||
import { SshTerminalEditForm } from "./SshTerminalEditForm";
|
||||
import { QuickConnectEditForm } from "./QuickConnectEditForm";
|
||||
import { CalendarEditForm } from "./CalendarEditForm";
|
||||
import { CountdownEditForm } from "./CountdownEditForm";
|
||||
import { SearchBarEditForm } from "./SearchBarEditForm";
|
||||
import { TextBannerEditForm } from "./TextBannerEditForm";
|
||||
import { ImageWidgetEditForm } from "./ImageWidgetEditForm";
|
||||
import { MarkdownNotesEditForm } from "./MarkdownNotesEditForm";
|
||||
import { CustomApiEditForm } from "./CustomApiEditForm";
|
||||
import { ServiceGridEditForm } from "./ServiceGridEditForm";
|
||||
import { DashboardLinksEditForm } from "./DashboardLinksEditForm";
|
||||
import { SearchLinksEditForm } from "./SearchLinksEditForm";
|
||||
import { LinkTreeEditForm } from "./LinkTreeEditForm";
|
||||
import { validateClockTimezone } from "../clock-timezone";
|
||||
|
||||
interface WidgetEditDialogProps {
|
||||
widget: CanvasWidget | null;
|
||||
onSave: (
|
||||
id: number,
|
||||
title: string | null,
|
||||
config: Record<string, unknown>,
|
||||
) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function WidgetEditDialog({
|
||||
widget,
|
||||
onSave,
|
||||
onClose,
|
||||
}: WidgetEditDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [title, setTitle] = useState(widget?.title ?? "");
|
||||
const [config, setConfig] = useState<Record<string, unknown>>(
|
||||
widget?.config ?? {},
|
||||
);
|
||||
|
||||
if (!widget) return null;
|
||||
|
||||
// Configs that need checking before they can be stored. The clock keeps the
|
||||
// normalized zone rather than the raw input, so "America/New York" is filed
|
||||
// as "America/New_York"; the form flags an unusable one and Save stays off.
|
||||
const clockTimezone =
|
||||
widget.typeId === "clock" ? validateClockTimezone(config.timezone) : null;
|
||||
const configInvalid = clockTimezone ? !clockTimezone.valid : false;
|
||||
|
||||
const handleSave = () => {
|
||||
if (configInvalid) return;
|
||||
onSave(
|
||||
widget.id,
|
||||
title || null,
|
||||
clockTimezone ? { ...config, timezone: clockTimezone.timezone } : config,
|
||||
);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const renderForm = () => {
|
||||
switch (widget.typeId) {
|
||||
case "service_link":
|
||||
return (
|
||||
<ServiceLinkEditForm
|
||||
config={config as unknown as ServiceLinkConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "folder":
|
||||
return (
|
||||
<FolderEditForm
|
||||
config={config as unknown as FolderConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "notes":
|
||||
return (
|
||||
<NotesEditForm
|
||||
config={config as unknown as NotesConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "bookmark_list":
|
||||
return (
|
||||
<BookmarkListEditForm
|
||||
config={config as unknown as BookmarkListConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "clock":
|
||||
return (
|
||||
<ClockEditForm
|
||||
config={config as unknown as ClockConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "host_status":
|
||||
return (
|
||||
<HostStatusEditForm
|
||||
config={config as unknown as HostStatusConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "weather":
|
||||
return (
|
||||
<WeatherEditForm
|
||||
config={config as unknown as WeatherConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "iframe_embed":
|
||||
return (
|
||||
<IframeEditForm
|
||||
config={config as unknown as IframeConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "rss_feed":
|
||||
return (
|
||||
<RssFeedEditForm
|
||||
config={config as unknown as RssFeedConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "host_grid":
|
||||
return (
|
||||
<HostGridEditForm
|
||||
config={config as unknown as HostGridConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "ping_status":
|
||||
return (
|
||||
<PingStatusEditForm
|
||||
config={config as unknown as PingStatusConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "recent_activity":
|
||||
return (
|
||||
<RecentActivityEditForm
|
||||
config={config as unknown as RecentActivityConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "termix_uptime":
|
||||
return (
|
||||
<TermixUptimeEditForm
|
||||
config={config as unknown as TermixUptimeConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "system_overview":
|
||||
return (
|
||||
<SystemOverviewEditForm
|
||||
config={config as unknown as SystemOverviewConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "ssh_terminal":
|
||||
return (
|
||||
<SshTerminalEditForm
|
||||
config={config as unknown as SshTerminalConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "quick_connect":
|
||||
return (
|
||||
<QuickConnectEditForm
|
||||
config={config as unknown as QuickConnectConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "calendar":
|
||||
return (
|
||||
<CalendarEditForm
|
||||
config={config as unknown as CalendarConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "countdown":
|
||||
return (
|
||||
<CountdownEditForm
|
||||
config={config as unknown as CountdownConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "search_bar":
|
||||
return (
|
||||
<SearchBarEditForm
|
||||
config={config as unknown as SearchBarConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "text_banner":
|
||||
return (
|
||||
<TextBannerEditForm
|
||||
config={config as unknown as TextBannerConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "image_widget":
|
||||
return (
|
||||
<ImageWidgetEditForm
|
||||
config={config as unknown as ImageWidgetConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "markdown_notes":
|
||||
return (
|
||||
<MarkdownNotesEditForm
|
||||
config={config as unknown as MarkdownNotesConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "custom_api":
|
||||
return (
|
||||
<CustomApiEditForm
|
||||
config={config as unknown as CustomApiConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "service_grid":
|
||||
return (
|
||||
<ServiceGridEditForm
|
||||
config={config as unknown as ServiceGridConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "dashboard_links":
|
||||
return (
|
||||
<DashboardLinksEditForm
|
||||
config={config as unknown as DashboardLinksConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "search_links":
|
||||
return (
|
||||
<SearchLinksEditForm
|
||||
config={config as unknown as SearchLinksConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
case "link_tree":
|
||||
return (
|
||||
<LinkTreeEditForm
|
||||
config={config as unknown as LinkTreeConfig}
|
||||
onChange={(c) => setConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
);
|
||||
default: {
|
||||
// Plugin widgets bring their own form.
|
||||
const EditForm = getWidgetType(widget.typeId)?.editFormComponent;
|
||||
return EditForm ? (
|
||||
<EditForm config={config} onChange={setConfig} />
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={!!widget}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) onClose();
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className="max-w-sm"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("homepage.editWidget")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{t("homepage.title_label")}
|
||||
</label>
|
||||
<Input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder={t("homepage.widgetTitlePlaceholder")}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
{renderForm()}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" size="sm" onClick={onClose}>
|
||||
{t("homepage.cancel")}
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSave} disabled={configInvalid}>
|
||||
{t("homepage.save")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { LayoutGrid } from "lucide-react";
|
||||
import type { ComponentType } from "react";
|
||||
import type {
|
||||
StandaloneViewProps,
|
||||
TabProps,
|
||||
TermixApp,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { HomepageCanvas } from "./HomepageCanvas.js";
|
||||
import { HomepagePreviewCard } from "./HomepagePreviewCard.js";
|
||||
import { ServiceLinksCard } from "./ServiceLinksCard.js";
|
||||
import { DashboardHomepageView } from "./DashboardHomepageView.js";
|
||||
import { setHomepageApi } from "./api.js";
|
||||
import { drainQueuedWidgets } from "./widgets/WidgetRegistry.js";
|
||||
|
||||
// Side-effect imports so widgets register themselves.
|
||||
import "./widgets/ServiceLinkWidget";
|
||||
import "./widgets/ClockWidget";
|
||||
import "./widgets/NotesWidget";
|
||||
import "./widgets/BookmarkListWidget";
|
||||
import "./widgets/HostStatusWidget";
|
||||
import "./widgets/FolderWidget";
|
||||
import "./widgets/WeatherWidget";
|
||||
import "./widgets/IframeWidget";
|
||||
import "./widgets/RssFeedWidget";
|
||||
import "./widgets/HostGridWidget";
|
||||
import "./widgets/PingStatusWidget";
|
||||
import "./widgets/RecentActivityWidget";
|
||||
import "./widgets/TermixUptimeWidget";
|
||||
import "./widgets/SystemOverviewWidget";
|
||||
import "./widgets/SshTerminalWidget";
|
||||
import "./widgets/QuickConnectWidget";
|
||||
import "./widgets/CalendarWidget";
|
||||
import "./widgets/CountdownWidget";
|
||||
import "./widgets/SearchBarWidget";
|
||||
import "./widgets/TextBannerWidget";
|
||||
import "./widgets/ImageWidget";
|
||||
import "./widgets/MarkdownNotesWidget";
|
||||
import "./widgets/CustomApiWidget";
|
||||
import "./widgets/ServiceGridWidget";
|
||||
import "./widgets/DashboardLinksWidget";
|
||||
import "./widgets/SearchLinksWidget";
|
||||
import "./widgets/LinkTreeWidget";
|
||||
|
||||
function HomepageTab(_props: TabProps) {
|
||||
return <HomepageCanvas />;
|
||||
}
|
||||
|
||||
function HomepageStandalone(_props: StandaloneViewProps) {
|
||||
return <HomepageCanvas fitOnLoad={true} />;
|
||||
}
|
||||
|
||||
export function activate(app: TermixApp): void {
|
||||
setHomepageApi(app.api);
|
||||
app.onDispose(() => setHomepageApi(null));
|
||||
|
||||
for (const widget of drainQueuedWidgets()) {
|
||||
app.registerHomepageWidget(widget as never);
|
||||
}
|
||||
|
||||
app.registerTab("homepage", HomepageTab as ComponentType<TabProps>, {
|
||||
icon: LayoutGrid,
|
||||
titleKey: "nav.homepage",
|
||||
singleton: true,
|
||||
hostless: true,
|
||||
standalone: HomepageStandalone,
|
||||
});
|
||||
|
||||
app.registerPaletteEntry({
|
||||
id: "homepage.open",
|
||||
titleKey: "nav.homepage",
|
||||
icon: LayoutGrid,
|
||||
keywords: ["homepage", "widgets", "canvas"],
|
||||
scope: "global",
|
||||
run: (shell) => shell.openSingletonTab("homepage"),
|
||||
});
|
||||
|
||||
app.registerDashboardCard({
|
||||
id: "service_links",
|
||||
titleKey: "dashboard.serviceLinks",
|
||||
defaultHeight: 200,
|
||||
defaultPanel: "side",
|
||||
component: ServiceLinksCard,
|
||||
});
|
||||
|
||||
app.registerDashboardCard({
|
||||
id: "homepage_preview",
|
||||
titleKey: "dashboard.homepagePreview",
|
||||
defaultHeight: 320,
|
||||
component: HomepagePreviewCard,
|
||||
});
|
||||
|
||||
app.declareActionSlot({
|
||||
id: "dashboard.secondaryView",
|
||||
accepts: ["component"],
|
||||
});
|
||||
app.registerSlotContribution("dashboard.secondaryView", {
|
||||
actionId: "homepage",
|
||||
titleKey: "nav.homepage",
|
||||
kind: "component",
|
||||
component: DashboardHomepageView as ComponentType<Record<string, unknown>>,
|
||||
});
|
||||
}
|
||||
|
||||
export function deactivate(): void {
|
||||
// Everything above was registered through app and is disposed by core.
|
||||
}
|
||||
Loaded 100 of 256 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user