feat(editor): F4.5e a router's connections in the device sheet
SheetRun now clicks through Java's JConnectionsDialog with a router, a computer and a switch cabled in the GUIContainer: a click marks a card, the same click unmarks it, another card swaps the cables or moves the one; "+" adds 192.168.0.10/24 up to eight cards, "-" removes the last card with its cable down to none, and a router without a card keeps no gateway. The document adds, removes and swaps a router's cards (the same cables, the same devices), Java's click logic is one function for the test and the page, and the sheet names the address at a cable's far end. The page "Verbindungen verwalten" shows Java's RJ45 buttons in Java's colours with each card's far end below. Not adopted: the last card stays. A card tab or a mark whose card is undone away falls back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -400,6 +400,102 @@ test("sets up a router's firewall on its own page: the switches, the default, ru
|
||||
await expect(sheet(page).getByRole('heading')).toHaveText('Vermittlungsrechner');
|
||||
});
|
||||
|
||||
test("manages a router's connections on its own page: cables swapped with two taps, cards added and removed, the last kept", async ({ page }) => {
|
||||
await openEditor(page, server.base, 'zwei_rechnernetze_komplett.fls');
|
||||
const id = (await drawing(page)).nodes.find((node) => node.kind === 'router')!.id;
|
||||
await choose(page, id);
|
||||
const joined = async () => ((await sheetOf(page, id)) as RouterSheet).interfaces.map((card) => card.connected?.device ?? '');
|
||||
const values = (await sheetOf(page, id)) as RouterSheet;
|
||||
const [one, two] = await joined();
|
||||
const cables = (await drawing(page)).links.length;
|
||||
await sheet(page).getByRole('button', { name: 'Verbindungen verwalten' }).click();
|
||||
await expect(sheet(page).getByRole('heading', { level: 2 })).toHaveText('Verbindungen verwalten');
|
||||
const items = sheet(page).locator('ol.connections li');
|
||||
const port = (index: number) => items.nth(index).getByRole('button');
|
||||
await expect(items.locator('.connection-local')).toHaveText(values.interfaces.map((card, index) => `NIC ${index + 1}: ${card.ipAddress}`));
|
||||
await expect(items.locator('.connection-remote')).toHaveText([one!, two!]);
|
||||
// Java's colours: green for a plugged card, yellow for the marked one, red for a free one.
|
||||
await expect(port(0)).toHaveCSS('background-color', 'rgb(0, 255, 0)');
|
||||
await port(0).click();
|
||||
await expect(port(0)).toHaveAttribute('aria-pressed', 'true');
|
||||
await expect(port(0)).toHaveCSS('background-color', 'rgb(255, 255, 0)');
|
||||
await port(0).click();
|
||||
await expect(port(0)).toHaveAttribute('aria-pressed', 'false');
|
||||
await port(0).click();
|
||||
await port(1).click();
|
||||
await expect.poll(joined).toEqual([two, one]);
|
||||
await expect(items.nth(0)).toHaveAttribute('data-state', 'plugged');
|
||||
|
||||
await sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' }).click();
|
||||
await expect(items).toHaveCount(3);
|
||||
await expect(items.nth(2).locator('.connection-local')).toHaveText('NIC 3: 192.168.0.10');
|
||||
await expect(port(2)).toHaveCSS('background-color', 'rgb(255, 0, 0)');
|
||||
await expect(port(2)).toHaveAccessibleName('NIC 3: 192.168.0.10, Keine Verbindung');
|
||||
// A free card takes the marked card's cable.
|
||||
await port(0).click();
|
||||
await port(2).click();
|
||||
await expect.poll(joined).toEqual(['', one, two]);
|
||||
await sheet(page).getByRole('button', { name: 'Schnittstelle entfernen' }).click();
|
||||
await expect.poll(joined).toEqual(['', one]);
|
||||
expect((await drawing(page)).links).toHaveLength(cables - 1);
|
||||
|
||||
for (let count = 2; count < 8; count++) await sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' }).click();
|
||||
await expect(items).toHaveCount(8);
|
||||
await expect(sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' })).toBeDisabled();
|
||||
await expect(sheet(page).getByText('Maximal 8 Schnittstellen erlaubt!')).toBeVisible();
|
||||
for (let count = 8; count > 1; count--) await sheet(page).getByRole('button', { name: 'Schnittstelle entfernen' }).click();
|
||||
await expect(items).toHaveCount(1);
|
||||
await expect(sheet(page).getByRole('button', { name: 'Schnittstelle entfernen' })).toBeDisabled();
|
||||
await expect(sheet(page).getByText('Eine Schnittstelle bleibt immer.')).toBeVisible();
|
||||
|
||||
await sheet(page).getByRole('button', { name: 'Zurück' }).click();
|
||||
const tabs = sheet(page).getByRole('tab');
|
||||
await expect(tabs).toHaveText(['Allgemein', values.interfaces[0]!.ipAddress, 'Weiterleitungstabelle']);
|
||||
// Undone, the second card comes back with its cable; a card's tab whose card goes again shows the general tab.
|
||||
await page.keyboard.press('Control+z');
|
||||
await expect(tabs).toHaveCount(4);
|
||||
await tabs.nth(2).click();
|
||||
await expect(sheet(page).locator('.sheet-connected')).toHaveText(`Verbunden mit ${one}`);
|
||||
await page.keyboard.press('Control+Shift+z');
|
||||
await expect(tabs).toHaveCount(3);
|
||||
await expect(tabs.first()).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(field(page, 'name')).toHaveValue(values.name);
|
||||
});
|
||||
|
||||
test("names a router's computer by its address, and lets go of a marked card when cards change", async ({ page }) => {
|
||||
await openEditor(page, server.base, 'internetwork_komplett.fls');
|
||||
const routers = (await drawing(page)).nodes.filter((node) => node.kind === 'router');
|
||||
let id = '';
|
||||
for (const each of routers) if (((await sheetOf(page, each.id)) as RouterSheet).interfaces.some((card) => card.connected?.address)) id = each.id;
|
||||
await choose(page, id);
|
||||
const values = (await sheetOf(page, id)) as RouterSheet;
|
||||
await sheet(page).getByRole('button', { name: 'Verbindungen verwalten' }).click();
|
||||
const items = sheet(page).locator('ol.connections li');
|
||||
const port = (index: number) => items.nth(index).getByRole('button');
|
||||
// Java: a computer's or a router's name with the address of its card, a switch's name alone.
|
||||
await expect(items.locator('.connection-remote')).toHaveText(
|
||||
values.interfaces.flatMap((card) => (card.connected === null ? [] : [card.connected.address === null ? card.connected.device : `${card.connected.device} (${card.connected.address})`])),
|
||||
);
|
||||
expect(values.interfaces.some((card) => card.connected?.address !== null && card.connected?.address !== undefined)).toBe(true);
|
||||
const pressed = () => sheet(page).locator('.connection-port[aria-pressed="true"]');
|
||||
await port(0).click();
|
||||
await sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' }).click();
|
||||
await expect(pressed()).toHaveCount(0);
|
||||
await port(0).click();
|
||||
await sheet(page).getByRole('button', { name: 'Schnittstelle entfernen' }).click();
|
||||
await expect(pressed()).toHaveCount(0);
|
||||
// A marked card undone away is marked no more: the next tap marks, it swaps nothing.
|
||||
await sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' }).click();
|
||||
const count = values.interfaces.length + 1;
|
||||
await port(count - 1).click();
|
||||
await page.keyboard.press('Control+z');
|
||||
await expect(items).toHaveCount(count - 1);
|
||||
const before = ((await sheetOf(page, id)) as RouterSheet).interfaces.map((card) => card.connected?.link ?? null);
|
||||
await port(0).click();
|
||||
await expect(port(0)).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(((await sheetOf(page, id)) as RouterSheet).interfaces.map((card) => card.connected?.link ?? null)).toEqual(before);
|
||||
});
|
||||
|
||||
test("opens a home router's firewall from its general tab", async ({ page }) => {
|
||||
await openEditor(page, server.base, 'new');
|
||||
await page.locator('[data-palette="gateway"]').click();
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 714 B |
@@ -7,7 +7,11 @@ import laptop from '../assets/hardware/laptop.png';
|
||||
import modem from '../assets/hardware/modem.png';
|
||||
import router from '../assets/hardware/router.png';
|
||||
import server from '../assets/hardware/server.png';
|
||||
import rj45 from '../assets/hardware/rj45.png';
|
||||
import networkSwitch from '../assets/hardware/switch.png';
|
||||
import type { IconName } from './layout.js';
|
||||
|
||||
export const ICON_URLS: Readonly<Record<IconName, string>> = { cloud, gateway, laptop, modem, router, server, switch: networkSwitch };
|
||||
|
||||
/** A card's socket, on the buttons of a router's connections (JConnectionsDialog). */
|
||||
export const RJ45_URL = rj45;
|
||||
|
||||
@@ -11,6 +11,13 @@
|
||||
"cable.wanOccupied": "WAN-Anschluss (extern) bereits belegt!",
|
||||
"cable.wirelessNotPossible": "Drahtlose Verbindung nicht möglich",
|
||||
"canvas.label": "Netzwerk",
|
||||
"connections.add": "Schnittstelle hinzufügen",
|
||||
"connections.fewest": "Eine Schnittstelle bleibt immer.",
|
||||
"connections.hint": "Hinweis: Zum Umstecken von Verbindungen:\n1. Verbindung auswählen durch Klick auf Schaltfläche links neben der Schnittstellenbezeichnung\n2. Auswählen der neuen Schnittstelle durch Klick auf die entsprechende Schaltfläche\nSollte die Zielschnittstelle bereits belegt sein, so werden die Verbindungen vertauscht.",
|
||||
"connections.local": "Lokale Schnittstellen",
|
||||
"connections.most": "Maximal 8 Schnittstellen erlaubt!",
|
||||
"connections.nic": "NIC {number}: {address}",
|
||||
"connections.remove": "Schnittstelle entfernen",
|
||||
"design.actions": "Aktionen",
|
||||
"design.palette": "Komponenten",
|
||||
"design.redo": "Wiederholen",
|
||||
@@ -88,6 +95,7 @@
|
||||
"sheet.cloud": "als Wolke darstellen",
|
||||
"sheet.connectedTo": "Verbunden mit {device}",
|
||||
"sheet.connection": "Anschluss",
|
||||
"sheet.connections": "Verbindungen verwalten",
|
||||
"sheet.defaultGateway": "Gateway",
|
||||
"sheet.deleteEntry": "Eintrag löschen",
|
||||
"sheet.dhcpClient": "DHCP zur Konfiguration verwenden",
|
||||
|
||||
@@ -11,6 +11,13 @@
|
||||
"cable.wanOccupied": "WAN port (external) already occupied!",
|
||||
"cable.wirelessNotPossible": "Wireless connection not possible",
|
||||
"canvas.label": "Network",
|
||||
"connections.add": "Add interface",
|
||||
"connections.fewest": "One interface always stays.",
|
||||
"connections.hint": "Note: For reorganising cable connections:\n1. Choose cable connection by clicking the button next to the interface name\n2. Choose new interface by clicking according button\nIf the target interface is already occupied by another connection, then those cables will be swapped.",
|
||||
"connections.local": "Local interfaces",
|
||||
"connections.most": "At most 8 interfaces supported!",
|
||||
"connections.nic": "NIC {number}: {address}",
|
||||
"connections.remove": "Remove interface",
|
||||
"design.actions": "Actions",
|
||||
"design.palette": "Components",
|
||||
"design.redo": "Redo",
|
||||
@@ -88,6 +95,7 @@
|
||||
"sheet.cloud": "Display as Cloud",
|
||||
"sheet.connectedTo": "Connected to {device}",
|
||||
"sheet.connection": "Connection",
|
||||
"sheet.connections": "Manage connections",
|
||||
"sheet.defaultGateway": "Gateway",
|
||||
"sheet.deleteEntry": "Delete entry",
|
||||
"sheet.dhcpClient": "Use DHCP for configuration",
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// A router's connections as a page of the device sheet, after Java's dialog
|
||||
// (JConnectionsDialog): each card with its RJ45 button left of its name and,
|
||||
// below, what its cable joins; a card's button marks it, another card's
|
||||
// button then swaps the two cards' cables (or moves the one), as Java's
|
||||
// buttons do; "+" adds a card after the last, "−" removes the last with its
|
||||
// cable — down to one card, not to none as in Java. Java's colours on the
|
||||
// buttons: green plugged, red free, yellow marked. Java draws its cables
|
||||
// between two columns; a narrow sheet has room for one.
|
||||
|
||||
import { useState } from 'react';
|
||||
import { addRouterInterface, connectionsClick, removeRouterInterface, ROUTER_INTERFACES, swapRouterCables, type RouterSheet } from '@mdg-filius/doc';
|
||||
import { RJ45_URL } from '../canvas/icons.js';
|
||||
import { useTranslate } from '../i18n/i18n.js';
|
||||
import type { Change } from './NameField.js';
|
||||
import { SheetNote } from './SheetField.js';
|
||||
|
||||
export function ConnectionsPanel({ sheet, change }: { readonly sheet: RouterSheet; readonly change: Change }) {
|
||||
const t = useTranslate();
|
||||
const [chosen, setChosen] = useState<number | null>(null);
|
||||
const cards = sheet.interfaces;
|
||||
// A card another person removed is no longer marked.
|
||||
const marked = chosen !== null && chosen < cards.length ? chosen : null;
|
||||
const click = (index: number) => {
|
||||
const next = connectionsClick(marked, index);
|
||||
if (next.swap !== undefined) {
|
||||
const [first, second] = next.swap;
|
||||
change.run(() => swapRouterCables(change.doc, change.id, cards[first]!.portId, cards[second]!.portId, change.origin));
|
||||
}
|
||||
setChosen(next.marked);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<h3 className="sheet-subtitle">{t('connections.local')}</h3>
|
||||
<ol className="connections">
|
||||
{cards.map((card, index) => {
|
||||
const name = t('connections.nic', { number: index + 1, address: card.ipAddress });
|
||||
const joined = card.connected;
|
||||
return (
|
||||
<li key={card.portId} data-state={index === marked ? 'marked' : joined === null ? 'free' : 'plugged'}>
|
||||
<button
|
||||
type="button"
|
||||
className="connection-port"
|
||||
aria-pressed={index === marked}
|
||||
aria-label={joined === null ? `${name}, ${t('sheet.notConnected')}` : name}
|
||||
onClick={() => click(index)}
|
||||
>
|
||||
<img src={RJ45_URL} alt="" />
|
||||
</button>
|
||||
<span className="connection-local">{name}</span>
|
||||
{joined === null ? null : <span className="connection-remote">{joined.address === null ? joined.device : `${joined.device} (${joined.address})`}</span>}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
<div className="sheet-buttons">
|
||||
<button
|
||||
type="button"
|
||||
name="addInterface"
|
||||
aria-label={t('connections.add')}
|
||||
title={t('connections.add')}
|
||||
disabled={cards.length >= ROUTER_INTERFACES.maximum}
|
||||
onClick={() => {
|
||||
change.run(() => addRouterInterface(change.doc, change.id, crypto.randomUUID(), change.origin));
|
||||
setChosen(null);
|
||||
}}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
name="removeInterface"
|
||||
aria-label={t('connections.remove')}
|
||||
title={t('connections.remove')}
|
||||
disabled={cards.length <= ROUTER_INTERFACES.fewest}
|
||||
onClick={() => {
|
||||
change.run(() => removeRouterInterface(change.doc, change.id, change.origin));
|
||||
setChosen(null);
|
||||
}}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
</div>
|
||||
{cards.length >= ROUTER_INTERFACES.maximum ? (
|
||||
<p className="sheet-note">{t('connections.most')}</p>
|
||||
) : cards.length <= ROUTER_INTERFACES.fewest ? (
|
||||
<p className="sheet-note">{t('connections.fewest')}</p>
|
||||
) : null}
|
||||
{/* Below the cards, as in Java's dialog. */}
|
||||
<SheetNote text={t('connections.hint')} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import { ICON_URLS } from '../canvas/icons.js';
|
||||
import { useTranslate } from '../i18n/i18n.js';
|
||||
import { NameField, type Change } from './NameField.js';
|
||||
import { HomeRouterFields, RouterFields } from './RouterSheets.js';
|
||||
import { ConnectionsPanel } from './ConnectionsPanel.js';
|
||||
import { FirewallPanel } from './FirewallPanel.js';
|
||||
import { DhcpServerPanel, PortForwardingPanel } from './ServicePanels.js';
|
||||
import { SheetCheck, SheetField } from './SheetField.js';
|
||||
@@ -62,7 +63,9 @@ export interface DeviceSheetProps {
|
||||
}
|
||||
|
||||
/** A page of the sheet that Java opens as a dialog. */
|
||||
export type SheetPage = 'dhcp' | 'forwarding' | 'firewall';
|
||||
export type SheetPage = 'dhcp' | 'forwarding' | 'firewall' | 'connections';
|
||||
|
||||
const PAGE_TITLES: Readonly<Record<SheetPage, string>> = { dhcp: 'sheet.dhcpServer', forwarding: 'sheet.portForwarding', firewall: 'sheet.firewall', connections: 'sheet.connections' };
|
||||
|
||||
export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onClose, onHighlight }: DeviceSheetProps) {
|
||||
const t = useTranslate();
|
||||
@@ -92,7 +95,7 @@ export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onCl
|
||||
‹
|
||||
</button>
|
||||
)}
|
||||
<h2>{open === 'dhcp' ? t('sheet.dhcpServer') : open === 'forwarding' ? t('sheet.portForwarding') : open === 'firewall' ? t('sheet.firewall') : title}</h2>
|
||||
<h2>{open === null ? title : t(PAGE_TITLES[open])}</h2>
|
||||
<button type="button" className="sheet-close" onClick={onClose} aria-label={t('sheet.close')} title={t('sheet.close')}>
|
||||
×
|
||||
</button>
|
||||
@@ -121,6 +124,10 @@ export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onCl
|
||||
<div key={`${id}-firewall`}>
|
||||
<FirewallPanel firewall={sheet.firewall} change={change} />
|
||||
</div>
|
||||
) : open === 'connections' && sheet.kind === 'router' ? (
|
||||
<div key={`${id}-connections`}>
|
||||
<ConnectionsPanel sheet={sheet} change={change} />
|
||||
</div>
|
||||
) : open === 'forwarding' && sheet.kind === 'gateway' ? (
|
||||
<div key={`${id}-forwarding`}>
|
||||
<PortForwardingPanel sheet={sheet} change={change} />
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import type { FirewallAction, FirewallProtocol, FirewallRule } from '@mdg-filius/core';
|
||||
import { useTranslate } from '../i18n/i18n.js';
|
||||
import type { Change } from './NameField.js';
|
||||
import { SheetCheck, SheetField } from './SheetField.js';
|
||||
import { SheetCheck, SheetField, SheetNote } from './SheetField.js';
|
||||
import { SheetTabs } from './SheetTabs.js';
|
||||
|
||||
/** Java's protocol names in the rule table (ruleAsVector): "*" for all. */
|
||||
@@ -32,19 +32,6 @@ const OFFERED: readonly FirewallProtocol[] = ['all', 'tcp', 'udp'];
|
||||
|
||||
const cellText = (rule: FirewallRule, column: (typeof FIREWALL_RULE_COLUMNS)[number]) => (column === 'port' ? (rule.port < 0 ? '' : String(rule.port)) : rule[column]);
|
||||
|
||||
/** A text with Java's line breaks as paragraphs. */
|
||||
function Note({ text }: { readonly text: string }) {
|
||||
return (
|
||||
<>
|
||||
{text.split('\n').map((line, index) => (
|
||||
<p key={index} className="sheet-note">
|
||||
{line}
|
||||
</p>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function FirewallPanel({ firewall, change }: { readonly firewall: FirewallSheet; readonly change: Change }) {
|
||||
const t = useTranslate();
|
||||
const [tab, setTab] = useState('general');
|
||||
@@ -68,15 +55,15 @@ export function FirewallPanel({ firewall, change }: { readonly firewall: Firewal
|
||||
{tab === 'general' ? (
|
||||
<>
|
||||
<SheetCheck name="firewallActive" label={t('firewall.active')} checked={firewall.isActive} onChange={flag('isActive')} />
|
||||
<Note text={t('firewall.activeHint')} />
|
||||
<SheetNote text={t('firewall.activeHint')} />
|
||||
<SheetCheck name="dropIcmp" label={t('firewall.dropIcmp')} checked={firewall.dropIcmp} onChange={flag('dropIcmp')} />
|
||||
<Note text={t('firewall.dropIcmpHint')} />
|
||||
<SheetNote text={t('firewall.dropIcmpHint')} />
|
||||
<SheetCheck name="filterSyn" label={t('firewall.filterSyn')} checked={firewall.filterSynSegmentsOnly} onChange={flag('filterSynSegmentsOnly')} />
|
||||
<Note text={t('firewall.filterSynHint')} />
|
||||
<SheetNote text={t('firewall.filterSynHint')} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Note text={t('firewall.intro')} />
|
||||
<SheetNote text={t('firewall.intro')} />
|
||||
<label className="sheet-field">
|
||||
<span>{t('firewall.defaultPolicy')}</span>
|
||||
<select
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
// general settings, each card and — on a router — the forwarding table
|
||||
// (JWeiterleitungsTabelle), greyed out while automatic routing (RIP) runs.
|
||||
// The card whose tab is open has its cable drawn active, as Java draws it.
|
||||
// The firewall, the port forwarding, the DHCP server and the change of the
|
||||
// cards follow with their dialogs (F4.5c).
|
||||
// The firewall, the port forwarding, the DHCP server and the router's
|
||||
// connections open as pages of the sheet (ServicePanels, FirewallPanel,
|
||||
// ConnectionsPanel).
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
@@ -74,12 +75,13 @@ export function RouterFields({
|
||||
readonly sheet: RouterSheet;
|
||||
readonly change: Change;
|
||||
readonly onHighlight: (link: string | null) => void;
|
||||
readonly onOpen: (page: 'firewall') => void;
|
||||
readonly onOpen: (page: 'firewall' | 'connections') => void;
|
||||
}) {
|
||||
const t = useTranslate();
|
||||
const [tab, setTab] = useState('general');
|
||||
// Java greys the table out while RIP runs; an open table goes back to the general tab.
|
||||
const shown = tab === 'table' && sheet.rip ? 'general' : tab;
|
||||
// Java greys the table out while RIP runs; an open table goes back to the general tab, as does the tab of a card removed.
|
||||
const gone = tab.startsWith('card-') && Number(tab.slice(5)) >= sheet.interfaces.length;
|
||||
const shown = (tab === 'table' && sheet.rip) || gone ? 'general' : tab;
|
||||
const card = shown.startsWith('card-') ? sheet.interfaces[Number(shown.slice(5))] : undefined;
|
||||
useHighlight(card?.connected?.link ?? null, onHighlight);
|
||||
const tabs: Tab[] = [
|
||||
@@ -103,6 +105,9 @@ export function RouterFields({
|
||||
<button type="button" onClick={() => onOpen('firewall')}>
|
||||
{t('sheet.firewall')}
|
||||
</button>
|
||||
<button type="button" onClick={() => onOpen('connections')}>
|
||||
{t('sheet.connections')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : card !== undefined ? (
|
||||
|
||||
@@ -110,3 +110,16 @@ export function SheetCheck({
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
/** A text with Java's line breaks as paragraphs. */
|
||||
export function SheetNote({ text }: { readonly text: string }) {
|
||||
return (
|
||||
<>
|
||||
{text.split('\n').map((line, index) => (
|
||||
<p key={index} className="sheet-note">
|
||||
{line}
|
||||
</p>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -252,3 +252,67 @@
|
||||
font-size: 13px;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.sheet-subtitle {
|
||||
margin: 12px 0 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* A router's cards: Java's RJ45 button left of the card's name, what its
|
||||
* cable joins below, joined by a line like Java's cable. */
|
||||
.connections {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.connections li {
|
||||
display: grid;
|
||||
grid-template-columns: 52px 1fr;
|
||||
column-gap: 10px;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.connection-port {
|
||||
grid-row: span 2;
|
||||
width: 52px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.connections li[data-state='plugged'] .connection-port {
|
||||
background: var(--connection-plugged);
|
||||
}
|
||||
|
||||
.connections li[data-state='free'] .connection-port {
|
||||
background: var(--connection-free);
|
||||
}
|
||||
|
||||
.connections li[data-state='marked'] .connection-port {
|
||||
background: var(--connection-marked);
|
||||
outline: 2px solid var(--border-focus);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.connection-port img {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.connection-local {
|
||||
font-size: 14px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.connection-remote {
|
||||
padding-left: 10px;
|
||||
border-left: 2px solid var(--canvas-cable);
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
/* Java's active cable (JCablePanel.farbeBlinken): a frame on its way, or the card whose tab is open. */
|
||||
--canvas-cable-active: rgb(0 255 64);
|
||||
--canvas-label: #000000;
|
||||
/* Java's buttons of a router's connections (JConnectionsDialog): a card plugged, free, marked. */
|
||||
--connection-plugged: rgb(0 255 0);
|
||||
--connection-free: rgb(255 0 0);
|
||||
--connection-marked: rgb(255 255 0);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
@@ -64,6 +68,9 @@
|
||||
--canvas-cable-wireless: rgb(150 150 150);
|
||||
--canvas-cable-active: rgb(0 200 50);
|
||||
--canvas-label: #000000;
|
||||
--connection-plugged: rgb(0 190 0);
|
||||
--connection-free: rgb(210 40 40);
|
||||
--connection-marked: rgb(235 215 0);
|
||||
}
|
||||
|
||||
html,
|
||||
|
||||
@@ -9,9 +9,9 @@ import { CATALOGS, translate } from '../src/i18n/i18n.js';
|
||||
|
||||
const bundles = join(dirname(fileURLToPath(import.meta.url)), '../../../tools/java-oracle/.upstream/src/main/resources/filius/messages');
|
||||
|
||||
/** A Java .properties file: ISO-8859-1, \uXXXX escapes, `key = value`. */
|
||||
/** A Java .properties file: ISO-8859-1, \uXXXX escapes, `key = value`, a backslash at a line's end going on in the next without its indent. */
|
||||
function properties(file: string): Map<string, string> {
|
||||
const text = new TextDecoder('latin1').decode(readFileSync(join(bundles, file)));
|
||||
const text = new TextDecoder('latin1').decode(readFileSync(join(bundles, file))).replace(/\\\r?\n[ \t]*/g, '');
|
||||
const entries = new Map<string, string>();
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const match = /^([\w.]+)\s*=\s*(.*)$/.exec(line);
|
||||
@@ -105,6 +105,10 @@ const FROM_JAVA: Readonly<Record<string, string>> = {
|
||||
'firewall.dropIcmp': 'jfirewalldialog_msg40',
|
||||
'firewall.dropIcmpHint': 'jfirewalldialog_msg41',
|
||||
'firewall.filterSyn': 'jfirewalldialog_msg42',
|
||||
'sheet.connections': 'jvermittlungsrechnerkonfiguration_msg23',
|
||||
'connections.local': 'jvermittlungsrechnerkonfiguration_msg22',
|
||||
'connections.add': 'jvermittlungsrechnerkonfiguration_msg24',
|
||||
'connections.most': 'jvermittlungsrechnerkonfiguration_msg25',
|
||||
};
|
||||
|
||||
/** Java's words, mended: the prompt says "Vermittlungsrechnerns" (guicontainer_msg1). */
|
||||
@@ -124,6 +128,16 @@ const FROM_JAVA_MENDED: Readonly<Record<string, readonly [string, (java: string)
|
||||
// Java's "\n" as line breaks; the last sentence of the German starts with a small letter there.
|
||||
'firewall.intro': ['jfirewalldialog_msg37', (java) => java.replaceAll('\\n', '\n').replace('\nüber', '\nÜber')],
|
||||
'firewall.filterSynHint': ['jfirewalldialog_msg43', (java) => java.replaceAll('\\n', '\n')],
|
||||
// Java's lines without their leading space and the empty one; a step starts with a capital letter.
|
||||
'connections.hint': [
|
||||
'jvermittlungsrechnerkonfiguration_msg19',
|
||||
(java) =>
|
||||
java
|
||||
.split('\\n')
|
||||
.map((line) => line.trim().replace(/^(\d\. )(\p{Ll})/u, (_, step: string, letter: string) => step + letter.toUpperCase()))
|
||||
.filter((line) => line !== '')
|
||||
.join('\n'),
|
||||
],
|
||||
};
|
||||
|
||||
describe('the catalogs', () => {
|
||||
|
||||
Reference in New Issue
Block a user