mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
fix(portal): correct the licence source and the spacing bugs behind it
Four of these look like polish and are not. The audit notice used the portal tier, which on self-hosted is derived from the SaaS account link and can never report Enterprise — so the notice showed on every install regardless of licence. It now takes the backend's runningEE flag, passed from the view because Storybook mounts no AppConfigProvider. The skipped tooltip rendered in caps because the bubble inherits from its trigger, and the trigger sits inside an uppercased stat label. Reset on the bubble so no consumer can hit it. The skipped stat sat below its neighbours because ActionIcon is sized to a control, not to the text it annotates. Section sub-lines become the same icon hint, so both now go through one component. Consecutive banners sat 1.75rem apart: a margin meant for banners inside cards was also applying to banners that are rows of the flex stack.
This commit is contained in:
@@ -7763,6 +7763,7 @@ titleCreated = "Key created"
|
||||
|
||||
[portal.infrastructure.encryption]
|
||||
heading = "Encryption at rest"
|
||||
hintLabel = "More about this section"
|
||||
subheading = "Stored files are encrypted before they reach disk, the database or object storage."
|
||||
unknown = "Unknown"
|
||||
|
||||
|
||||
@@ -22,6 +22,12 @@
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
pointer-events: none;
|
||||
/* The trigger often sits inside styled text (an uppercased stat label, a
|
||||
heading), and the bubble is a child of it. Reset what would otherwise be
|
||||
inherited so the explanation always reads as prose. */
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.sui-tip__bubble--top {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/* ActionIcon is sized to match a control's height, which is far taller than the
|
||||
label or heading this sits beside — enough to push a stat's value off the row
|
||||
its siblings sit on. Shrink it to the text it annotates. */
|
||||
.portal-hint .mantine-ActionIcon-root {
|
||||
--ai-size: 1.1rem;
|
||||
min-width: 1.1rem;
|
||||
min-height: 1.1rem;
|
||||
color: var(--c-text-subtle);
|
||||
}
|
||||
|
||||
.portal-hint .mantine-ActionIcon-root:hover {
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
/* Keep the trigger on the text baseline rather than the line box's centre. */
|
||||
.portal-hint {
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ActionIcon, Tooltip, type TooltipPlacement } from "@app/ui";
|
||||
import LocalIcon from "@app/components/shared/LocalIcon";
|
||||
import "@portal/components/InfoHint.css";
|
||||
|
||||
export interface InfoHintProps {
|
||||
/** The explanation. Shown on hover, keyboard focus and tap. */
|
||||
content: string;
|
||||
/** Accessible name for the trigger, e.g. "What does skipped mean?". */
|
||||
label: string;
|
||||
placement?: TooltipPlacement;
|
||||
}
|
||||
|
||||
/**
|
||||
* The small circled "i" that carries a line of explanation next to a heading or
|
||||
* a label, so the copy is there when someone wants it and out of the way when
|
||||
* they do not.
|
||||
*
|
||||
* Sized to the text rather than to a control, because the usual home for one of
|
||||
* these is inside a label whose line box it must not inflate.
|
||||
*/
|
||||
export function InfoHint({ content, label, placement = "top" }: InfoHintProps) {
|
||||
return (
|
||||
<Tooltip content={content} placement={placement} className="portal-hint">
|
||||
<ActionIcon variant="quiet" size="sm" aria-label={label}>
|
||||
<LocalIcon icon="info-rounded" width="0.9rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -130,7 +130,8 @@ export function EncryptionKeyTable({
|
||||
<Card padding="loose">
|
||||
<SectionHeader
|
||||
title={t("portal.infrastructure.encryption.keys.heading")}
|
||||
sub={t("portal.infrastructure.encryption.keys.subheading")}
|
||||
hint={t("portal.infrastructure.encryption.keys.subheading")}
|
||||
hintLabel={t("portal.infrastructure.encryption.hintLabel")}
|
||||
/>
|
||||
{/* Column headers over an empty body are chrome around nothing. */}
|
||||
{keys.length === 0 ? (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ActionIcon,
|
||||
Banner,
|
||||
Button,
|
||||
Card,
|
||||
@@ -9,9 +8,8 @@ import {
|
||||
StatTile,
|
||||
Modal,
|
||||
StatusBadge,
|
||||
Tooltip,
|
||||
} from "@app/ui";
|
||||
import LocalIcon from "@app/components/shared/LocalIcon";
|
||||
import { InfoHint } from "@portal/components/InfoHint";
|
||||
import type { StatusTone } from "@app/ui";
|
||||
import { SectionHeader } from "@portal/components/infrastructure/SectionHeader";
|
||||
import type {
|
||||
@@ -67,7 +65,8 @@ export function EncryptionMigrationCard({
|
||||
<div className="portal-enc__head">
|
||||
<SectionHeader
|
||||
title={t("portal.infrastructure.encryption.migration.heading")}
|
||||
sub={t("portal.infrastructure.encryption.migration.subheading")}
|
||||
hint={t("portal.infrastructure.encryption.migration.subheading")}
|
||||
hintLabel={t("portal.infrastructure.encryption.hintLabel")}
|
||||
/>
|
||||
<StatusBadge tone={STATE_TONE[state]} size="sm">
|
||||
{t(`portal.infrastructure.encryption.migration.state.${state}`)}
|
||||
@@ -122,24 +121,17 @@ export function EncryptionMigrationCard({
|
||||
/>
|
||||
<StatTile
|
||||
label={
|
||||
<span className="portal-enc__stat-label">
|
||||
<>
|
||||
{t("portal.infrastructure.encryption.migration.skipped")}
|
||||
<Tooltip
|
||||
<InfoHint
|
||||
content={t(
|
||||
"portal.infrastructure.encryption.migration.skippedNote",
|
||||
)}
|
||||
>
|
||||
<ActionIcon
|
||||
variant="quiet"
|
||||
size="sm"
|
||||
aria-label={t(
|
||||
"portal.infrastructure.encryption.migration.skippedHelp",
|
||||
)}
|
||||
>
|
||||
<LocalIcon icon="info-rounded" width="0.875rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</span>
|
||||
label={t(
|
||||
"portal.infrastructure.encryption.migration.skippedHelp",
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
value={skipped.toLocaleString()}
|
||||
/>
|
||||
|
||||
@@ -110,6 +110,13 @@
|
||||
margin: 0.75rem 0 0;
|
||||
}
|
||||
|
||||
/* ...but a banner that is itself a row of the stack is spaced by the stack's
|
||||
gap. Leaving the margin on stacks the two, so consecutive banners sat 1.75rem
|
||||
apart while every other row sat at 1rem. */
|
||||
.portal-enc__stack > .sui-banner {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.portal-enc__fingerprint code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8125rem;
|
||||
@@ -154,13 +161,17 @@
|
||||
|
||||
.portal-enc__migration-stats {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.portal-enc__stat-label {
|
||||
/* One label carries a hint icon and the others do not. Without a shared line
|
||||
box its value drops below its neighbours'. */
|
||||
.portal-enc__migration-stats .sui-stat__label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
min-height: 1.25rem;
|
||||
}
|
||||
|
||||
@@ -117,9 +117,10 @@ export const Active: Story = {
|
||||
globals: { tier: "enterprise" },
|
||||
};
|
||||
|
||||
/** 03. Pro licence: encryption runs, audit does not. */
|
||||
/** 03. Non-Enterprise licence: encryption runs, audit does not. */
|
||||
export const ProLicenceAuditNotice: Story = {
|
||||
globals: { tier: "pro" },
|
||||
globals: { tier: "enterprise" },
|
||||
args: { auditAvailable: false },
|
||||
};
|
||||
|
||||
/** 04. The revoke dialog, which is where the surprising behaviour is explained. */
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
StatTile,
|
||||
StatusBadge,
|
||||
} from "@app/ui";
|
||||
import { useTier } from "@portal/contexts/TierContext";
|
||||
import { errorMessage } from "@portal/api/http";
|
||||
import {
|
||||
disableEncryptionKey,
|
||||
@@ -40,6 +39,13 @@ const MIGRATION_POLL_MS = 2000;
|
||||
export interface EncryptionPanelProps {
|
||||
/** Shows the cross-node propagation note when revoking. */
|
||||
clusterEnabled?: boolean;
|
||||
/**
|
||||
* Whether the licence records audit events (Enterprise). Supplied by the view
|
||||
* from the backend's `runningEE` flag rather than read here, because the
|
||||
* portal tier is derived from the SaaS account link and never reports
|
||||
* Enterprise on a self-hosted install.
|
||||
*/
|
||||
auditAvailable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,9 +58,9 @@ export interface EncryptionPanelProps {
|
||||
*/
|
||||
export function EncryptionPanel({
|
||||
clusterEnabled = false,
|
||||
auditAvailable = true,
|
||||
}: EncryptionPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const { tier } = useTier();
|
||||
|
||||
const [status, setStatus] = useState<StorageEncryptionStatus | null>(null);
|
||||
const [loadError, setLoadError] = useState<unknown>(null);
|
||||
@@ -216,7 +222,8 @@ export function EncryptionPanel({
|
||||
<section className="portal-enc__stack">
|
||||
<SectionHeader
|
||||
title={t("portal.infrastructure.encryption.heading")}
|
||||
sub={t("portal.infrastructure.encryption.subheading")}
|
||||
hint={t("portal.infrastructure.encryption.subheading")}
|
||||
hintLabel={t("portal.infrastructure.encryption.hintLabel")}
|
||||
/>
|
||||
<Card padding="loose">
|
||||
<EmptyState
|
||||
@@ -262,14 +269,15 @@ export function EncryptionPanel({
|
||||
<section className="portal-enc__head">
|
||||
<SectionHeader
|
||||
title={t("portal.infrastructure.encryption.heading")}
|
||||
sub={t("portal.infrastructure.encryption.subheading")}
|
||||
hint={t("portal.infrastructure.encryption.subheading")}
|
||||
hintLabel={t("portal.infrastructure.encryption.hintLabel")}
|
||||
/>
|
||||
<StatusBadge tone={writeStateTone} size="sm">
|
||||
{t(`portal.infrastructure.encryption.writeState.${writeStateKey}`)}
|
||||
</StatusBadge>
|
||||
</section>
|
||||
|
||||
{tier !== "enterprise" ? (
|
||||
{!auditAvailable ? (
|
||||
<Banner
|
||||
tone="info"
|
||||
title={t("portal.infrastructure.encryption.auditNotice.title")}
|
||||
@@ -305,7 +313,8 @@ export function EncryptionPanel({
|
||||
<Card padding="loose">
|
||||
<SectionHeader
|
||||
title={t("portal.infrastructure.encryption.coverage.heading")}
|
||||
sub={t("portal.infrastructure.encryption.coverage.subheading")}
|
||||
hint={t("portal.infrastructure.encryption.coverage.subheading")}
|
||||
hintLabel={t("portal.infrastructure.encryption.hintLabel")}
|
||||
/>
|
||||
<div className="portal-enc__coverage">
|
||||
<div className="portal-enc__coverage-tiles">
|
||||
@@ -356,7 +365,10 @@ export function EncryptionPanel({
|
||||
<Card padding="loose">
|
||||
<SectionHeader
|
||||
title={t("portal.infrastructure.encryption.masterKey.heading")}
|
||||
sub={t("portal.infrastructure.encryption.masterKey.subheading")}
|
||||
hint={t(
|
||||
"portal.infrastructure.encryption.masterKey.subheading",
|
||||
)}
|
||||
hintLabel={t("portal.infrastructure.encryption.hintLabel")}
|
||||
/>
|
||||
{status.masterKeyFingerprint ? (
|
||||
<>
|
||||
|
||||
@@ -36,7 +36,8 @@ export function EncryptionRotationCard({
|
||||
<Card padding="loose">
|
||||
<SectionHeader
|
||||
title={t("portal.infrastructure.encryption.rotation.heading")}
|
||||
sub={t("portal.infrastructure.encryption.rotation.subheading")}
|
||||
hint={t("portal.infrastructure.encryption.rotation.subheading")}
|
||||
hintLabel={t("portal.infrastructure.encryption.hintLabel")}
|
||||
/>
|
||||
|
||||
<div className="portal-enc__kv">
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
/** Title + sub-line heading shared by every Infrastructure section. */
|
||||
export function SectionHeader({ title, sub }: { title: string; sub: string }) {
|
||||
import { InfoHint } from "@portal/components/InfoHint";
|
||||
|
||||
export interface SectionHeaderProps {
|
||||
title: string;
|
||||
/** Sub-line under the title. Prefer `hint` for anything explanatory. */
|
||||
sub?: string;
|
||||
/** Explanation, carried by an info icon beside the title instead of a sub-line. */
|
||||
hint?: string;
|
||||
/** Accessible name for the hint trigger. Required whenever `hint` is set. */
|
||||
hintLabel?: string;
|
||||
}
|
||||
|
||||
/** Title + explanation heading shared by every Infrastructure section. */
|
||||
export function SectionHeader({
|
||||
title,
|
||||
sub,
|
||||
hint,
|
||||
hintLabel,
|
||||
}: SectionHeaderProps) {
|
||||
return (
|
||||
<header className="portal-infra__section-head">
|
||||
<h2 className="portal-infra__section-title">{title}</h2>
|
||||
<p className="portal-infra__section-sub">{sub}</p>
|
||||
<div className="portal-infra__section-title-row">
|
||||
<h2 className="portal-infra__section-title">{title}</h2>
|
||||
{hint ? <InfoHint content={hint} label={hintLabel ?? title} /> : null}
|
||||
</div>
|
||||
{sub ? <p className="portal-infra__section-sub">{sub}</p> : null}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,6 +58,12 @@
|
||||
margin-bottom: 0.625rem;
|
||||
}
|
||||
|
||||
.portal-infra__section-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.portal-infra__section-title {
|
||||
margin: 0;
|
||||
font-size: 0.9375rem;
|
||||
|
||||
@@ -20,7 +20,8 @@ export function Infrastructure() {
|
||||
const { setActiveView } = useView();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
// Audit is Enterprise-only; disabled (greyed, inert) on non-enterprise instances.
|
||||
const auditEnabled = useEnterpriseEnabled().enabled;
|
||||
const enterprise = useEnterpriseEnabled();
|
||||
const auditEnabled = enterprise.enabled;
|
||||
|
||||
const canOpenTab = useCallback(
|
||||
(key: string) =>
|
||||
@@ -99,7 +100,14 @@ export function Infrastructure() {
|
||||
<div className="portal-infra__panel">
|
||||
{tab === "api-keys" && <ApiKeysTab />}
|
||||
{tab === "audit" && <AuditTab />}
|
||||
{tab === "storage" && <EncryptionPanel />}
|
||||
{tab === "storage" && (
|
||||
// Treat "still resolving" as available: flashing "your licence records
|
||||
// no audit trail" at an Enterprise operator is worse than the notice
|
||||
// arriving a beat late.
|
||||
<EncryptionPanel
|
||||
auditAvailable={enterprise.loading || enterprise.enabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user