mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
Portal Policies: table layout with colour-matched icons (#6994)
# Description of Changes Reworks the Policies catalogue from blocky cards into a clean table with tinted category icons and tone-matched "enforces" chips, so it matches the styling used elsewhere in the portal. Part of a portal (processor) UI-consistency pass, split into small focused PRs. ## Before / after <img width="2816" height="1318" alt="after-policies-light" src="https://github.com/user-attachments/assets/af7586f7-8d23-47eb-ae2e-90630d09929b" /> <img width="2816" height="1438" alt="before-policies-dark" src="https://github.com/user-attachments/assets/efcc63d9-5f2d-4dca-944d-d470aa689304" /> <img width="2816" height="1438" alt="before-policies-light" src="https://github.com/user-attachments/assets/2398df15-0632-439f-9f23-0a6ddc496083" /> <img width="2816" height="1318" alt="after-policies-dark" src="https://github.com/user-attachments/assets/9a3726ac-1637-4c7c-ad7a-dcdaf141d89d" /> --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [x] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [x] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details.
This commit is contained in:
@@ -7515,6 +7515,13 @@ label = "Docs enforced"
|
||||
description = "Configured but not firing"
|
||||
label = "Paused"
|
||||
|
||||
[portal.policies.table]
|
||||
appliesTo = "Applies to"
|
||||
docs = "Docs enforced"
|
||||
enforces = "Enforces"
|
||||
policy = "Policy"
|
||||
status = "Status"
|
||||
|
||||
[portal.policies.wizard.actions]
|
||||
back = "Back"
|
||||
cancel = "Cancel"
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
type CatalogueEntry,
|
||||
type PoliciesResponse,
|
||||
} from "@portal/api/policies";
|
||||
import { policyCategoryIcon } from "@app/components/policies/policyCategoryIcon";
|
||||
import { PolicyCategoryBadge } from "@portal/components/policies/PolicyCategoryIcon";
|
||||
import "@portal/components/PolicySummary.css";
|
||||
|
||||
/**
|
||||
@@ -63,9 +63,7 @@ export function PolicySummary() {
|
||||
header: t("portal.policySummary.column.policy"),
|
||||
render: ({ entry }) => (
|
||||
<div className="portal-policysum__cat">
|
||||
<span className="portal-policysum__icon" aria-hidden>
|
||||
{policyCategoryIcon(entry.category.id)}
|
||||
</span>
|
||||
<PolicyCategoryBadge category={entry.category} />
|
||||
<div className="portal-policysum__cat-text">
|
||||
<strong>{t(entry.category.label)}</strong>
|
||||
<span>{t(entry.category.desc)}</span>
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Chip, StatusBadge, Table, type TableColumn } from "@app/ui";
|
||||
import type { CatalogueEntry } from "@portal/api/policies";
|
||||
import { PolicyCategoryBadge } from "@portal/components/policies/PolicyCategoryIcon";
|
||||
import "@portal/views/Policies.css";
|
||||
|
||||
interface PolicyCatalogueTableProps {
|
||||
entries: CatalogueEntry[];
|
||||
onOpen: (entry: CatalogueEntry) => void;
|
||||
/** Setup is unavailable (e.g. the AI engine is off): shown, but not openable. */
|
||||
isLocked?: (entry: CatalogueEntry) => boolean;
|
||||
/** Chip text explaining why setup is locked (e.g. "Requires AI engine"). */
|
||||
lockedLabel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The policy catalogue as a proper data table (Policy / Enforces / Applies to /
|
||||
* Docs / Status), replacing the stacked full-width cards that read as "blocky".
|
||||
* Same shared Table + StatusBadge + Chip primitives the Sources, Documents and
|
||||
* Home policy tables use, so every list page in the portal now reads alike.
|
||||
*/
|
||||
export function PolicyCatalogueTable({
|
||||
entries,
|
||||
onOpen,
|
||||
isLocked,
|
||||
lockedLabel,
|
||||
}: PolicyCatalogueTableProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = useMemo<TableColumn<CatalogueEntry>[]>(
|
||||
() => [
|
||||
{
|
||||
key: "policy",
|
||||
header: t("portal.policies.table.policy", "Policy"),
|
||||
render: (entry) => (
|
||||
<div className="portal-policies__cell">
|
||||
<PolicyCategoryBadge category={entry.category} />
|
||||
<strong className="portal-policies__cell-name">
|
||||
{t(entry.category.label)}
|
||||
</strong>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "enforces",
|
||||
header: t("portal.policies.table.enforces", "Enforces"),
|
||||
render: (entry) => (
|
||||
<div className="portal-policies__rulechips">
|
||||
{entry.config.rules.map((r) => (
|
||||
<Chip key={r} accent="neutral" size="sm">
|
||||
{t(r)}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "scope",
|
||||
header: t("portal.policies.table.appliesTo", "Applies to"),
|
||||
render: (entry) => (
|
||||
<span className="portal-policies__muted">
|
||||
{t(entry.config.scopeLabel)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "docs",
|
||||
header: t("portal.policies.table.docs", "Docs enforced"),
|
||||
align: "right",
|
||||
width: "8rem",
|
||||
render: (entry) => (
|
||||
<span className="portal-policies__docs">
|
||||
{entry.policy ? entry.policy.stats.enforced.toLocaleString() : "—"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
header: t("portal.policies.table.status", "Status"),
|
||||
align: "right",
|
||||
width: "8.5rem",
|
||||
render: (entry) => {
|
||||
if (entry.category.comingSoon) {
|
||||
// One consistent neutral chip for every "Upgrade to Enterprise" —
|
||||
// the same action should read the same on every row.
|
||||
return (
|
||||
<Chip accent="neutral" size="sm">
|
||||
{t("portal.policies.card.comingSoon")}
|
||||
</Chip>
|
||||
);
|
||||
}
|
||||
if (isLocked?.(entry)) {
|
||||
return (
|
||||
<Chip accent="neutral" size="sm">
|
||||
{lockedLabel ?? t("portal.policies.card.requiresAiEngine")}
|
||||
</Chip>
|
||||
);
|
||||
}
|
||||
if (entry.policy) {
|
||||
const paused = entry.policy.state.status === "paused";
|
||||
return (
|
||||
<StatusBadge
|
||||
tone={paused ? "warning" : "success"}
|
||||
size="sm"
|
||||
pulse={!paused}
|
||||
>
|
||||
{paused
|
||||
? t("portal.policies.status.paused")
|
||||
: t("portal.policies.status.active")}
|
||||
</StatusBadge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button size="sm" variant="secondary" onClick={() => onOpen(entry)}>
|
||||
{t("portal.policySummary.action.setUp")}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[t, onOpen, isLocked, lockedLabel],
|
||||
);
|
||||
|
||||
return (
|
||||
<Table<CatalogueEntry>
|
||||
className="portal-policies__table"
|
||||
columns={columns}
|
||||
rows={entries}
|
||||
rowKey={(e) => e.category.id}
|
||||
onRowClick={(entry) =>
|
||||
entry.category.comingSoon || isLocked?.(entry)
|
||||
? undefined
|
||||
: onOpen(entry)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/* Neutral gray rounded icon badge for a policy category. Lives with the
|
||||
component so every consumer gets the styling without importing a view CSS. */
|
||||
.pcat-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-bg-muted);
|
||||
color: var(--color-text-3);
|
||||
/* The shared MUI outline glyph inherits its size from here. */
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { PolicyCategory } from "@portal/api/policies";
|
||||
import { policyCategoryIcon } from "@app/components/policies/policyCategoryIcon";
|
||||
import "@portal/components/policies/PolicyCategoryIcon.css";
|
||||
|
||||
/** A neutral gray rounded badge holding the category's shared outline icon. */
|
||||
export function PolicyCategoryBadge({
|
||||
category,
|
||||
}: {
|
||||
category: PolicyCategory;
|
||||
}) {
|
||||
return (
|
||||
<span className="pcat-badge" aria-hidden>
|
||||
{policyCategoryIcon(category.id)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
import { fetchSources } from "@portal/api/sources";
|
||||
import { useAsync } from "@portal/hooks/useAsync";
|
||||
import { PolicyFieldRow } from "@portal/components/policies/PolicyFieldRow";
|
||||
import { policyCategoryIcon } from "@app/components/policies/policyCategoryIcon";
|
||||
import { PolicyCategoryBadge } from "@portal/components/policies/PolicyCategoryIcon";
|
||||
import { PolicyRedactConfig } from "@app/components/policies/PolicyRedactConfig";
|
||||
import { PolicyWatermarkConfig } from "@app/components/policies/PolicyWatermarkConfig";
|
||||
import { ClassificationLabelsSection } from "@portal/components/policies/ClassificationLabelsSection";
|
||||
@@ -334,9 +334,7 @@ function PolicySetupWizardBody({
|
||||
width="lg"
|
||||
title={
|
||||
<span className="portal-policies__wizard-title">
|
||||
<span className="portal-policies__cat-icon" aria-hidden>
|
||||
{policyCategoryIcon(category.id)}
|
||||
</span>
|
||||
<PolicyCategoryBadge category={category} />
|
||||
{isEdit
|
||||
? t("portal.policies.wizard.title.edit", {
|
||||
category: t(category.label),
|
||||
|
||||
@@ -453,3 +453,38 @@
|
||||
.portal-policies__detail-stats .sui-stat + .sui-stat {
|
||||
border-left: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
/* ── Catalogue table (replaces the stacked category cards) ──────────────── */
|
||||
.portal-policies__cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.portal-policies__cell-name {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.portal-policies__rulechips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.3125rem;
|
||||
}
|
||||
|
||||
.portal-policies__muted {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.portal-policies__docs {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
/* The .pcat-badge icon styles live with the PolicyCategoryIcon component
|
||||
(PolicyCategoryIcon.css) so the Home policy summary picks them up too. */
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
type PolicySetupResult,
|
||||
} from "@portal/api/policies";
|
||||
import { CatalogueSummary } from "@portal/components/policies/CatalogueSummary";
|
||||
import { PolicyCategoryCard } from "@portal/components/policies/PolicyCategoryCard";
|
||||
import { PolicyCatalogueTable } from "@portal/components/policies/PolicyCatalogueTable";
|
||||
import { PolicyDetailPanel } from "@portal/components/policies/PolicyDetailPanel";
|
||||
import { PolicySetupWizard } from "@portal/components/policies/PolicySetupWizard";
|
||||
import { useAiEngineEnabled } from "@portal/hooks/useAiEngineEnabled";
|
||||
@@ -53,14 +53,14 @@ export function Policies() {
|
||||
const { enabled: aiEngineEnabled, loading: aiEngineLoading } =
|
||||
useAiEngineEnabled();
|
||||
|
||||
function isLocked(entry: CatalogueEntry): boolean {
|
||||
return (
|
||||
const isLocked = useCallback(
|
||||
(entry: CatalogueEntry): boolean =>
|
||||
entry.category.requiresAiEngine === true &&
|
||||
!aiEngineEnabled &&
|
||||
!aiEngineLoading &&
|
||||
!entry.policy
|
||||
);
|
||||
}
|
||||
!entry.policy,
|
||||
[aiEngineEnabled, aiEngineLoading],
|
||||
);
|
||||
|
||||
const catalogue = data?.catalogue ?? [];
|
||||
const refetch = useCallback(() => setVersion((v) => v + 1), []);
|
||||
@@ -184,17 +184,12 @@ export function Policies() {
|
||||
)}
|
||||
|
||||
{!isLoading && !fetchError && (
|
||||
<div className="portal-policies__grid">
|
||||
{displayCatalogue.map((entry) => (
|
||||
<PolicyCategoryCard
|
||||
key={entry.category.id}
|
||||
entry={entry}
|
||||
onOpen={openEntry}
|
||||
locked={isLocked(entry)}
|
||||
lockedLabel={t("portal.policies.card.requiresAiEngine")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<PolicyCatalogueTable
|
||||
entries={displayCatalogue}
|
||||
onOpen={openEntry}
|
||||
isLocked={isLocked}
|
||||
lockedLabel={t("portal.policies.card.requiresAiEngine")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PolicyDetailPanel
|
||||
|
||||
Reference in New Issue
Block a user