Skip to content

Commit 088f3f9

Browse files
committed
Fix app setup dialog
1 parent 85f256c commit 088f3f9

9 files changed

Lines changed: 119 additions & 57 deletions

File tree

branding/styles/mos.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,14 @@ label.mos-row {
584584
background var(--mos-transition);
585585
}
586586

587+
/* `font: inherit` above resets family and size together, so the code variant
588+
has to restate both for a field — otherwise a monospace value silently comes
589+
out in the body face. */
590+
.mos-row-input.mos-row-code {
591+
font-family: var(--mos-font-mono);
592+
font-size: var(--mos-text-small);
593+
}
594+
587595
.mos-row-input::placeholder {
588596
color: var(--mos-color-text-muted);
589597
opacity: 1;

site/generated/branding/mos.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,14 @@ label.mos-row {
589589
background var(--mos-transition);
590590
}
591591

592+
/* `font: inherit` above resets family and size together, so the code variant
593+
has to restate both for a field — otherwise a monospace value silently comes
594+
out in the body face. */
595+
.mos-row-input.mos-row-code {
596+
font-family: var(--mos-font-mono);
597+
font-size: var(--mos-text-small);
598+
}
599+
592600
.mos-row-input::placeholder {
593601
color: var(--mos-color-text-muted);
594602
opacity: 1;

suite-manager/frontend/src/components/disclaimers/DisclaimerNotice.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ export function DisclaimerNotice({ copy, details, variant = 'warning' }: {
3636
</div>
3737
</Notice>
3838
{detailsOpen ? <Dialog
39-
closeOnBackdrop
4039
footer={<button className="mos-btn mos-btn-primary" onClick={() => setDetailsOpen(false)} type="button">{copy.dismissLabel}</button>}
4140
onClose={() => setDetailsOpen(false)}
4241
title={copy.dialogTitle}

suite-manager/frontend/src/components/ui.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ export function AdvancedPanel({
316316
</details>;
317317
}
318318

319-
export function Dialog({ children, className, closeOnBackdrop = false, footer, header, onClose, title }: { children: ReactNode; className?: string; closeOnBackdrop?: boolean; footer?: ReactNode; header?: ReactNode; onClose: () => void; title: string }) {
319+
export function Dialog({ children, className, footer, header, onClose, title }: { children: ReactNode; className?: string; footer?: ReactNode; header?: ReactNode; onClose: () => void; title: string }) {
320320
const closeRef = useRef<HTMLButtonElement>(null);
321321
const dialogRef = useRef<HTMLElement>(null);
322322
const onCloseRef = useRef(onClose);
@@ -329,13 +329,18 @@ export function Dialog({ children, className, closeOnBackdrop = false, footer, h
329329
document.addEventListener('keydown', onKeyDown);
330330
return () => document.removeEventListener('keydown', onKeyDown);
331331
}, []);
332+
// Escape and a click on the backdrop both close every dialog, with no opt-out:
333+
// a dismissal that works on some dialogs and not others is a worse trap than
334+
// either rule on its own. A dialog that must not be dismissed mid-flight
335+
// guards its own onClose, the way AppConfigDialog does while it is saving.
336+
//
332337
// Portalled to <body> for the same reason as Drawer and ActionMenu: a dialog
333338
// opened from inside a frosted surface (the app detail slide-over, any
334339
// .mos-panel) would otherwise take that ancestor as its containing block, so
335340
// `position: fixed` centres it on the panel instead of the screen and its
336341
// frost blurs nothing. Screen-centred and frosted is the only correct result.
337342
return createPortal(
338-
<div className="suite-modal-backdrop" onClick={closeOnBackdrop ? (event) => { if (event.target === event.currentTarget) onCloseRef.current(); } : undefined} role="presentation"><section ref={dialogRef} aria-label={title} aria-modal="true" className={`suite-dialog mos-panel${className ? ` ${className}` : ''}`} role="dialog"><div className="suite-dialog-header">{header ?? <h2>{title}</h2>}<button ref={closeRef} aria-label={`Close ${title}`} className="suite-icon-button" onClick={onClose} type="button"><Icon name="x" /></button></div>{children}{footer ? <div className="suite-dialog-footer">{footer}</div> : null}</section></div>,
343+
<div className="suite-modal-backdrop" onClick={(event) => { if (event.target === event.currentTarget) onCloseRef.current(); }} role="presentation"><section ref={dialogRef} aria-label={title} aria-modal="true" className={`suite-dialog mos-panel${className ? ` ${className}` : ''}`} role="dialog"><div className="suite-dialog-header">{header ?? <h2>{title}</h2>}<button ref={closeRef} aria-label={`Close ${title}`} className="suite-icon-button" onClick={onClose} type="button"><Icon name="x" /></button></div>{children}{footer ? <div className="suite-dialog-footer">{footer}</div> : null}</section></div>,
339344
document.body,
340345
);
341346
}

suite-manager/frontend/src/features/apps/AppConfigDialog.tsx

Lines changed: 56 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -454,55 +454,67 @@ export function AppConfigDialog({
454454
{generatedFields.map((field) => <Row key={field.id} label={splitFieldLabel(field.label).label}>
455455
{factValue(field, storedFor(field.id))}
456456
</Row>)}
457-
{rows.map((row, index) => <div className="mos-row" key={row.key}>
458-
<div className="mos-row-main">
459-
<input
460-
aria-label="Variable name"
461-
autoComplete="off"
462-
className="mos-row-input suite-env-name"
463-
disabled={busy}
464-
// A name that reads like a credential starts masked, until the
465-
// owner says otherwise with the eye control beside it.
466-
onChange={(event) => update(index, {
467-
name: event.currentTarget.value,
468-
...(row.secretTouched ? {} : { secret: SECRET_LOOKING_NAME.test(event.currentTarget.value) }),
469-
})}
470-
placeholder="EXAMPLE_API_KEY"
471-
spellCheck={false}
472-
value={row.name}
473-
/>
474-
<RowTrailing>
475-
{row.secret && row.stored && !row.value ? <>
476-
<RowValue mask />
477-
<RowValue code>{fingerprintTail(row.stored)}</RowValue>
478-
<RowAction disabled={busy} icon="refresh" label={`Replace ${row.name || 'this value'}`} onClick={() => update(index, { stored: null })} />
479-
</> : <RowInput
480-
aria-label="Variable value"
457+
{rows.map((row, index) => {
458+
// MOS never gets a stored secret back from the server, so this row
459+
// has nothing to reveal and no box to put it in: it states what it
460+
// holds, and Replace is the only way to change it.
461+
const held = row.secret && row.stored && !row.value;
462+
return <div className="mos-row suite-env-row" key={row.key}>
463+
<div className="mos-row-main">
464+
<RowInput
465+
aria-label="Variable name"
481466
autoComplete="off"
482467
code
483468
disabled={busy}
484-
onChange={(event) => update(index, { value: event.currentTarget.value })}
469+
// A name that reads like a credential starts masked, until the
470+
// owner says otherwise with the eye control beside it.
471+
onChange={(event) => update(index, {
472+
name: event.currentTarget.value,
473+
...(row.secretTouched ? {} : { secret: SECRET_LOOKING_NAME.test(event.currentTarget.value) }),
474+
})}
475+
placeholder="EXAMPLE_API_KEY"
485476
spellCheck={false}
486-
type={row.secret ? 'password' : 'text'}
487-
value={row.value}
488-
/>}
489-
<RowAction
490-
disabled={busy}
491-
icon={row.secret ? 'eye-off' : 'eye'}
492-
label={row.secret ? `Stop hiding ${row.name || 'this value'}` : `Hide ${row.name || 'this value'}`}
493-
onClick={() => update(index, { secret: !row.secret, secretTouched: true, stored: null, value: row.stored ? '' : row.value })}
494-
/>
495-
<RowAction
496-
danger
497-
disabled={busy}
498-
icon="x"
499-
label={`Remove ${row.name || 'this variable'}`}
500-
onClick={() => setRows((current) => current.filter((_item, position) => position !== index))}
477+
value={row.name}
501478
/>
502-
</RowTrailing>
503-
</div>
504-
{rowErrors[index] ? <p className="mos-row-help mos-row-invalid" role="alert">{rowErrors[index]}</p> : null}
505-
</div>)}
479+
<RowTrailing>
480+
{held ? <>
481+
{/* Said the way the generated rows above say it, and for the
482+
same reason: bullets followed by characters read as the
483+
tail of the value itself, and this is a fingerprint —
484+
enough to tell two stored secrets apart, and no part of
485+
either one. */}
486+
<RowValue>{`Hidden value · ${fingerprintTail(row.stored)}`}</RowValue>
487+
<RowAction disabled={busy} icon="refresh" label={`Replace ${row.name || 'this value'}`} onClick={() => update(index, { stored: null })} />
488+
</> : <>
489+
<RowInput
490+
aria-label="Variable value"
491+
autoComplete="off"
492+
code
493+
disabled={busy}
494+
onChange={(event) => update(index, { value: event.currentTarget.value })}
495+
spellCheck={false}
496+
type={row.secret ? 'password' : 'text'}
497+
value={row.value}
498+
/>
499+
<RowAction
500+
disabled={busy}
501+
icon={row.secret ? 'eye-off' : 'eye'}
502+
label={row.secret ? `Stop hiding ${row.name || 'this value'}` : `Hide ${row.name || 'this value'}`}
503+
onClick={() => update(index, { secret: !row.secret, secretTouched: true })}
504+
/>
505+
</>}
506+
<RowAction
507+
danger
508+
disabled={busy}
509+
icon="x"
510+
label={`Remove ${row.name || 'this variable'}`}
511+
onClick={() => setRows((current) => current.filter((_item, position) => position !== index))}
512+
/>
513+
</RowTrailing>
514+
</div>
515+
{rowErrors[index] ? <p className="mos-row-help mos-row-invalid" role="alert">{rowErrors[index]}</p> : null}
516+
</div>;
517+
})}
506518
</Rows> : null}
507519

508520
<p className="mos-row-help">

suite-manager/frontend/src/features/apps/AppsScreen.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -926,7 +926,7 @@ function AppDetail({
926926
service={app.routes[0]?.service || app.services[0]?.id || app.id}
927927
webAddress={appAddress(app)}
928928
/> : null}
929-
{galleryOpen && app.catalog.screenshots.length ? <Dialog className="suite-app-gallery-dialog" closeOnBackdrop onClose={() => setGalleryOpen(false)} title={`${app.name} screens`}>
929+
{galleryOpen && app.catalog.screenshots.length ? <Dialog className="suite-app-gallery-dialog" onClose={() => setGalleryOpen(false)} title={`${app.name} screens`}>
930930
<figure className="suite-app-gallery">
931931
<div className="suite-app-gallery-frame">
932932
<img alt={app.catalog.screenshots[slideIdx]?.alt || `${app.name} screenshot ${slideIdx + 1}`} src={app.catalog.screenshots[slideIdx]?.src || app.catalog.screenshots[0]!.src} />
@@ -945,7 +945,6 @@ function AppDetail({
945945
</Dialog> : null}
946946
{resourcesOpen ? <Dialog
947947
className="suite-app-resources-dialog"
948-
closeOnBackdrop
949948
footer={<button className="mos-btn mos-btn-secondary" onClick={() => setResourcesOpen(false)} type="button">Close</button>}
950949
onClose={() => setResourcesOpen(false)}
951950
title="What runs on your server"

suite-manager/frontend/src/features/apps/PrivacyPosture.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@ export function PrivacyPostureDialog({ advisories, appName, assessmentUrl = ASSE
111111
const gradeScale = gradeScaleLabel(privacy);
112112
return <Dialog
113113
className="suite-privacy-dialog"
114-
closeOnBackdrop
115114
header={<div className="suite-privacy-dialog-heading">
116115
<PrivacyShieldBadge privacy={privacy} size="dialog" />
117116
<div>

suite-manager/frontend/src/styles/index.css

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3198,10 +3198,12 @@
31983198
editor needs that a plain label/value row does not.
31993199
32003200
Narrower than the default dialog on purpose: a list of rows reads down, and
3201-
760px of row leaves the value stranded a long way from its label. */
3201+
760px of row leaves the value stranded a long way from its label. Not as
3202+
narrow as a pure label/value list would want, because the environment editor
3203+
puts two fields on one row and they have to fit side by side. */
32023204
.suite-dialog.suite-app-config-dialog {
32033205
gap: var(--mos-space-5);
3204-
width: min(100%, 560px);
3206+
width: min(100%, 640px);
32053207
padding: var(--mos-space-6);
32063208
}
32073209

@@ -3230,13 +3232,35 @@
32303232

32313233
/* A variable is a name and a value, both editable, which is the one row that
32323234
needs its left side to be a field rather than a label — and the only one
3233-
whose right side has to grow, since a value is typed rather than read. */
3234-
.suite-env-name {
3235-
flex: 1 1 40%;
3235+
whose right side has to grow, since a value is typed rather than read.
3236+
3237+
Two columns rather than the row's usual flex, because flex lets the pair wrap
3238+
and the wrapped result is unreadable: four identical boxes down the dialog,
3239+
with nothing to say which value belongs to which name. A grid keeps each pair
3240+
on its own line at every width the dialog has. The name column is the wider
3241+
of the two because it is the identifier: two variables from the same upstream
3242+
service differ at the end of a long shared prefix. */
3243+
.suite-env-row > .mos-row-main {
3244+
display: grid;
3245+
grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
3246+
gap: var(--mos-space-3);
3247+
align-items: center;
3248+
}
3249+
3250+
/* The row's own columns already size these; the primitive's 14rem basis is for
3251+
a field that has a whole row to itself and would force this pair to wrap. */
3252+
.suite-env-row .mos-row-input {
3253+
flex: 1 1 4rem;
32363254
}
32373255

3238-
.suite-env-name + .mos-row-trailing {
3239-
flex: 1 1 60%;
3256+
/* Too narrow for two fields to say anything side by side. Stacked, the row's
3257+
own hairline is what separates one pair from the next, which is the same job
3258+
it does everywhere else in the dialog. */
3259+
@media (max-width: 34rem) {
3260+
.suite-env-row > .mos-row-main {
3261+
grid-template-columns: minmax(0, 1fr);
3262+
gap: var(--mos-space-2);
3263+
}
32403264
}
32413265

32423266
.suite-env-paste { display: grid; gap: var(--mos-space-3); }

suite-manager/frontend/src/styles/mos.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,14 @@ label.mos-row {
589589
background var(--mos-transition);
590590
}
591591

592+
/* `font: inherit` above resets family and size together, so the code variant
593+
has to restate both for a field — otherwise a monospace value silently comes
594+
out in the body face. */
595+
.mos-row-input.mos-row-code {
596+
font-family: var(--mos-font-mono);
597+
font-size: var(--mos-text-small);
598+
}
599+
592600
.mos-row-input::placeholder {
593601
color: var(--mos-color-text-muted);
594602
opacity: 1;

0 commit comments

Comments
 (0)