feat: F4.6g2 the DNS server window, its records being /dns/hosts
Oracle DnsWindowRun uses Java's DNS server window as a person does and asks a computer's resolver after each step. The core's DNS server is a program its window changes: on and off (its socket on port 53), recursive resolution, and records that are /dns/hosts, read on every query as Java's server reads them (the reader moves from fls to the core). The snapshot carries every installed DNS server with whether it runs. The room takes the window's actions, sends its view and writes the server back after the run; the editor shows Java's window with its three tabs. Not taken from Java: a cell edit spilling into records of another type, an add losing what another program wrote into the file, an empty mail domain, a removal hitting the first record of the name. A table cell no longer takes the room's value over what is being typed (the web server's too). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,8 @@
|
||||
// leaves it; so far the command line (F4.6b), the web browser and the web
|
||||
// server (F4.6d), the e-mail program and server (F4.6e), the text editor
|
||||
// (F4.6f2), the file explorer and the image viewer (F4.6f3), the echo
|
||||
// server and the generic client (F4.6g1) have one, the others follow. The browser's window takes its title from its page, the
|
||||
// server and the generic client (F4.6g1), the DNS server (F4.6g2) have one,
|
||||
// the others follow. The browser's window takes its title from its page, the
|
||||
// text editor's from its open file, the image viewer's from its picture, as
|
||||
// Java's do.
|
||||
|
||||
@@ -39,6 +40,7 @@ import { TerminalPanel } from './TerminalPanel.js';
|
||||
import { TextEditorPanel, useTextEditorWindow } from './TextEditorPanel.js';
|
||||
import { FileExplorerPanel } from './FileExplorerPanel.js';
|
||||
import { EchoServerPanel, SimpleClientPanel } from './EchoPanels.js';
|
||||
import { DnsServerPanel } from './DnsServerPanel.js';
|
||||
import { ImageViewerPanel, useImageViewer } from './ImageViewerPanel.js';
|
||||
import { WebServerPanel } from './WebServerPanel.js';
|
||||
import './desktop.css';
|
||||
@@ -69,7 +71,7 @@ export interface DesktopWindowProps {
|
||||
export type DesktopWindowName = DesktopProgram | 'installation';
|
||||
|
||||
/** The programs whose windows are built so far. */
|
||||
const WITH_WINDOW: ReadonlySet<DesktopProgram> = new Set(['commandPrompt', 'webBrowser', 'webServer', 'emailClient', 'emailServer', 'textEditor', 'fileExplorer', 'imageViewer', 'echoServer', 'simpleClient']);
|
||||
const WITH_WINDOW: ReadonlySet<DesktopProgram> = new Set(['commandPrompt', 'webBrowser', 'webServer', 'emailClient', 'emailServer', 'textEditor', 'fileExplorer', 'imageViewer', 'echoServer', 'simpleClient', 'dnsServer']);
|
||||
|
||||
/** The document's desktop of a computer, read anew after every change (a program installed, the label changed). */
|
||||
function useDesktopView(doc: Y.Doc, id: string) {
|
||||
@@ -186,6 +188,7 @@ export function DesktopWindow({ doc, id, simulation, active, x, y, z, program: o
|
||||
{shown === 'imageViewer' ? <ImageViewerPanel doc={doc} node={id} /> : null}
|
||||
{shown === 'echoServer' ? <EchoServerPanel node={id} simulation={simulation} /> : null}
|
||||
{shown === 'simpleClient' ? <SimpleClientPanel node={id} simulation={simulation} /> : null}
|
||||
{shown === 'dnsServer' ? <DnsServerPanel node={id} simulation={simulation} /> : null}
|
||||
</ProgramFrame>
|
||||
)}
|
||||
{info ? (
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
// A computer's DNS server, after Java's GUIApplicationDNSServerWindow
|
||||
// (measured with tools/java-oracle's DnsWindowRun): the start button
|
||||
// ("Starten"/"Beenden") and the box for recursive resolution above, three
|
||||
// tabs — addresses (A), mail exchange (MX), name servers (NS) — each with
|
||||
// its two fields (the address starts as the computer's own, as Java's), red
|
||||
// while Java's patterns do not take them, "Hinzufügen" (Java's message when
|
||||
// they do not: the valid example), "Auswahl entfernen" and the table, whose
|
||||
// cells are written when they are left. The server is the room's: the panel
|
||||
// shows its view and hands over what is done; what the server does better
|
||||
// than Java is in its core (apps/dnsServer.ts).
|
||||
//
|
||||
// Better than Java: a cell that its check does not take is marked and goes
|
||||
// back when it is left (Java's goes back without a word).
|
||||
|
||||
import { useEffect, useState, type KeyboardEvent } from 'react';
|
||||
import { validDnsRecord, type DnsRecordType } from '@mdg-filius/core';
|
||||
import type { DnsServerView } from '@mdg-filius/protocol';
|
||||
import { useTranslate } from '../i18n/i18n.js';
|
||||
import type { Simulation } from '../simulation/useSimulation.js';
|
||||
|
||||
/** Each tab's words: its title, its fields' labels, its columns, Java's message for what it does not take. */
|
||||
const TABS: Readonly<Record<DnsRecordType, { readonly tab: string; readonly fields: readonly [string, string]; readonly columns: readonly [string, string]; readonly invalid: string }>> = {
|
||||
A: { tab: 'dnsServer.tabA', fields: ['dnsServer.aName', 'dnsServer.aAddress'], columns: ['dnsServer.aNameColumn', 'dnsServer.aAddressColumn'], invalid: 'dnsServer.invalidA' },
|
||||
MX: { tab: 'dnsServer.tabMX', fields: ['dnsServer.mxDomain', 'dnsServer.mxServer'], columns: ['dnsServer.mxDomainColumn', 'dnsServer.mxServerColumn'], invalid: 'dnsServer.invalidMX' },
|
||||
NS: { tab: 'dnsServer.tabNS', fields: ['dnsServer.nsDomain', 'dnsServer.nsServer'], columns: ['dnsServer.nsDomainColumn', 'dnsServer.nsServerColumn'], invalid: 'dnsServer.invalidNS' },
|
||||
};
|
||||
|
||||
/** Whether a field's text is what the tab takes there (its other part taken as valid). */
|
||||
function fieldValid(type: DnsRecordType, column: 0 | 1, text: string): boolean {
|
||||
if (text === '') return true;
|
||||
return column === 0 ? validDnsRecord(type, text, type === 'A' ? '1.1.1.1' : 'a.de') : validDnsRecord(type, 'a.de', text);
|
||||
}
|
||||
|
||||
/** A cell of the table, written when it is left or Enter is pressed and it changed; one its check does not take goes back. */
|
||||
function RecordCell({
|
||||
type,
|
||||
column,
|
||||
value,
|
||||
label,
|
||||
onCommit,
|
||||
}: {
|
||||
readonly type: DnsRecordType;
|
||||
readonly column: 0 | 1;
|
||||
readonly value: string;
|
||||
readonly label: string;
|
||||
readonly onCommit: (value: string) => void;
|
||||
}) {
|
||||
const [draft, setDraft] = useState(value);
|
||||
const [editing, setEditing] = useState(false);
|
||||
// The server's value, but not over what is being typed since the last Enter, Escape or leaving (it may come just after a cell written).
|
||||
useEffect(() => {
|
||||
if (!editing) setDraft(value);
|
||||
}, [value, editing]);
|
||||
const valid = draft !== '' && fieldValid(type, column, draft.trim());
|
||||
const commit = () => {
|
||||
setEditing(false);
|
||||
if (draft !== value && valid) onCommit(draft);
|
||||
else setDraft(value);
|
||||
};
|
||||
const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') commit();
|
||||
if (event.key === 'Escape') {
|
||||
setEditing(false);
|
||||
setDraft(value);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<input
|
||||
value={draft}
|
||||
aria-label={label}
|
||||
aria-invalid={valid ? undefined : 'true'}
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
onChange={(event) => {
|
||||
setDraft(event.target.value);
|
||||
setEditing(true);
|
||||
}}
|
||||
onBlur={commit}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function DnsServerPanel({ node, simulation }: { readonly node: string; readonly simulation: Simulation }) {
|
||||
const t = useTranslate();
|
||||
const topic = `app:${node}/dnsServer` as const;
|
||||
const { watch, act } = simulation;
|
||||
useEffect(() => watch([topic, `node:${node}`]), [watch, topic, node]);
|
||||
const view = simulation.views.get(topic) as DnsServerView | null | undefined;
|
||||
const ownAddress = simulation.nodes.get(node)?.ipAddress ?? '';
|
||||
const [tab, setTab] = useState<DnsRecordType>('A');
|
||||
// The address field starts as the computer's own (Java's window takes it when it opens): null until it is typed into or emptied.
|
||||
const [fields, setFields] = useState<Record<DnsRecordType, readonly [string, string | null]>>({ A: ['', null], MX: ['', ''], NS: ['', ''] });
|
||||
const [chosen, setChosen] = useState<number | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
if (view === null || view === undefined) return <div className="web-server" />;
|
||||
const words = TABS[tab];
|
||||
const shownFields = (type: DnsRecordType): readonly [string, string] => [fields[type][0], fields[type][1] ?? ownAddress];
|
||||
const [first, second] = shownFields(tab);
|
||||
const rows = view.rows[tab];
|
||||
|
||||
const add = () => {
|
||||
if (!validDnsRecord(tab, first.trim(), second.trim())) {
|
||||
setNotice(t(words.invalid));
|
||||
return;
|
||||
}
|
||||
act(node, 'dnsServer', 'addRecord', { type: tab, name: first.trim(), data: second.trim() });
|
||||
setFields({ ...fields, [tab]: ['', ''] });
|
||||
setNotice(null);
|
||||
};
|
||||
const remove = () => {
|
||||
if (chosen === null || chosen >= rows.length) return;
|
||||
act(node, 'dnsServer', 'removeRecord', { type: tab, row: chosen });
|
||||
setChosen(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="web-server dns-server">
|
||||
<div className="web-server-bar">
|
||||
<button type="button" onClick={() => act(node, 'dnsServer', 'setActive', { active: !view.active })}>
|
||||
{t(view.active ? 'dnsServer.stop' : 'dnsServer.start')}
|
||||
</button>
|
||||
<label>
|
||||
<input type="checkbox" checked={view.recursive} onChange={(event) => act(node, 'dnsServer', 'setRecursive', { enabled: event.target.checked })} />
|
||||
{t('dnsServer.recursive')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="mail-tabs" role="tablist">
|
||||
{(['A', 'MX', 'NS'] as const).map((each) => (
|
||||
<button
|
||||
key={each}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={tab === each}
|
||||
onClick={() => {
|
||||
setTab(each);
|
||||
setChosen(null);
|
||||
setNotice(null);
|
||||
}}
|
||||
>
|
||||
{t(TABS[each].tab)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="dns-server-page" role="tabpanel" aria-label={t(words.tab)}>
|
||||
{([0, 1] as const).map((column) => (
|
||||
<label key={column} className="simple-client-field">
|
||||
<span>{t(words.fields[column])}</span>
|
||||
<input
|
||||
value={shownFields(tab)[column]}
|
||||
aria-invalid={fieldValid(tab, column, shownFields(tab)[column].trim()) ? undefined : 'true'}
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
onChange={(event) => {
|
||||
const next: [string, string] = [...shownFields(tab)];
|
||||
next[column] = event.target.value;
|
||||
setFields({ ...fields, [tab]: next });
|
||||
setNotice(null);
|
||||
}}
|
||||
onKeyDown={(event) => event.key === 'Enter' && add()}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
<div className="web-server-bar">
|
||||
<button type="button" onClick={add}>
|
||||
{t('dnsServer.add')}
|
||||
</button>
|
||||
<button type="button" disabled={chosen === null || chosen >= rows.length} onClick={remove}>
|
||||
{t('dnsServer.remove')}
|
||||
</button>
|
||||
</div>
|
||||
{notice === null ? null : (
|
||||
<p className="mail-notice dns-server-notice" role="alert">
|
||||
{notice}
|
||||
</p>
|
||||
)}
|
||||
<table className="dns-server-table" aria-label={t('dnsServer.tables')}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t(words.columns[0])}</th>
|
||||
<th>{t(words.columns[1])}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(([name, data], row) => (
|
||||
<tr key={row} aria-selected={row === chosen} onPointerDown={() => setChosen(row)}>
|
||||
{([name, data] as const).map((value, column) => (
|
||||
<td key={column}>
|
||||
<RecordCell
|
||||
type={tab}
|
||||
column={column as 0 | 1}
|
||||
value={value}
|
||||
label={`${t(words.columns[column]!)} ${t('dnsServer.row', { row: row + 1 })}`}
|
||||
onCommit={(written) => act(node, 'dnsServer', 'changeRecord', { type: tab, row, column, value: written })}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -17,15 +17,37 @@ const NO_LINES: readonly LogLine<ServerLogEntry>[] = [];
|
||||
/** A cell of the table, written to the server when it is left or Enter is pressed and it changed. */
|
||||
function HostCell({ value, label, onCommit }: { readonly value: string; readonly label: string; readonly onCommit: (value: string) => void }) {
|
||||
const [draft, setDraft] = useState(value);
|
||||
useEffect(() => setDraft(value), [value]);
|
||||
const [editing, setEditing] = useState(false);
|
||||
// The server's value, but not over what is being typed since the last Enter, Escape or leaving (it may come just after a cell written).
|
||||
useEffect(() => {
|
||||
if (!editing) setDraft(value);
|
||||
}, [value, editing]);
|
||||
const commit = () => {
|
||||
setEditing(false);
|
||||
if (draft !== value) onCommit(draft);
|
||||
};
|
||||
const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') commit();
|
||||
if (event.key === 'Escape') setDraft(value);
|
||||
if (event.key === 'Escape') {
|
||||
setEditing(false);
|
||||
setDraft(value);
|
||||
}
|
||||
};
|
||||
return <input value={draft} aria-label={label} autoCapitalize="off" autoCorrect="off" spellCheck={false} onChange={(event) => setDraft(event.target.value)} onBlur={commit} onKeyDown={onKeyDown} />;
|
||||
return (
|
||||
<input
|
||||
value={draft}
|
||||
aria-label={label}
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
onChange={(event) => {
|
||||
setDraft(event.target.value);
|
||||
setEditing(true);
|
||||
}}
|
||||
onBlur={commit}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function WebServerPanel({ node, simulation }: { readonly node: string; readonly simulation: Simulation }) {
|
||||
|
||||
@@ -1083,3 +1083,60 @@
|
||||
padding: 4px 6px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* The DNS server (GUIApplicationDNSServerWindow): button and box, three tabs
|
||||
* of two fields, two buttons and a table whose cells are written in place. */
|
||||
.dns-server-page {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dns-server input:not([type='checkbox']) {
|
||||
min-height: 32px;
|
||||
padding: 0 6px;
|
||||
border: 1px solid #a0a0a0;
|
||||
background: #ffffff;
|
||||
color: #000000;
|
||||
font: 16px Dialog, sans-serif;
|
||||
}
|
||||
|
||||
.dns-server [aria-invalid='true'] {
|
||||
border-color: #ff0000;
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
.dns-server-notice {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.dns-server-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.dns-server-table th {
|
||||
padding: 4px 6px;
|
||||
border-bottom: 1px solid #a0a0a0;
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.dns-server-table td {
|
||||
padding: 2px;
|
||||
border-bottom: 1px solid #dddddd;
|
||||
}
|
||||
|
||||
.dns-server-table td input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.dns-server-table tr[aria-selected='true'] td,
|
||||
.dns-server-table tr[aria-selected='true'] input {
|
||||
background: #b8cfe5;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user