Implement governed distribution lists
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@govoplan/dist-lists-webui",
|
||||
"version": "0.1.14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"module": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts"
|
||||
},
|
||||
"./styles/dist-lists.css": "./src/styles/dist-lists.css"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.14",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": ">=19.2.7 <20",
|
||||
"react-dom": ">=19.2.7 <20",
|
||||
"react-router": ">=8.3.0 <9",
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@govoplan/core-webui": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import {
|
||||
apiFetch,
|
||||
apiPath,
|
||||
type ApiSettings
|
||||
} from "@govoplan/core-webui";
|
||||
|
||||
export type DefinitionKind = "static" | "parameterized" | "dynamic" | "template";
|
||||
export type EntryMode = "include" | "exclude" | "override";
|
||||
export type EntryKind =
|
||||
| "address_contact"
|
||||
| "address_list"
|
||||
| "address_email"
|
||||
| "raw_email"
|
||||
| "raw_postal_address"
|
||||
| "internal_mail"
|
||||
| "portal"
|
||||
| "idm_identity"
|
||||
| "idm_group"
|
||||
| "organization_unit"
|
||||
| "function"
|
||||
| "effective_function_incumbent"
|
||||
| "dataflow_result"
|
||||
| "distribution_list";
|
||||
export type DistributionChannel = "email" | "postal" | "internal_mail" | "portal";
|
||||
export type DistributionOutcome =
|
||||
| "usable"
|
||||
| "unresolved"
|
||||
| "invalid"
|
||||
| "suppressed"
|
||||
| "ambiguous"
|
||||
| "duplicate"
|
||||
| "policy_blocked"
|
||||
| "provider_unavailable"
|
||||
| "stale";
|
||||
|
||||
export type SourceReference = {
|
||||
provider: string;
|
||||
resource_type: string;
|
||||
resource_id: string;
|
||||
revision?: string | null;
|
||||
fingerprint?: string | null;
|
||||
label?: string | null;
|
||||
metadata: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type DistributionParameter = {
|
||||
key: string;
|
||||
value_type: "string" | "integer" | "number" | "boolean" | "date" | "datetime" | "string_list";
|
||||
label?: string | null;
|
||||
required: boolean;
|
||||
default?: unknown;
|
||||
allowed_values: unknown[];
|
||||
minimum?: number | null;
|
||||
maximum?: number | null;
|
||||
pattern?: string | null;
|
||||
description?: string | null;
|
||||
};
|
||||
|
||||
export type DistributionListEntryInput = {
|
||||
entry_key?: string | null;
|
||||
kind: EntryKind;
|
||||
mode: EntryMode;
|
||||
source: SourceReference;
|
||||
label?: string | null;
|
||||
purpose?: string | null;
|
||||
requested_channels: DistributionChannel[];
|
||||
effective_from?: string | null;
|
||||
effective_until?: string | null;
|
||||
configuration: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type DistributionListEntry = DistributionListEntryInput & {
|
||||
id: string;
|
||||
entry_key: string;
|
||||
order: number;
|
||||
};
|
||||
|
||||
export type DistributionListRevision = {
|
||||
id: string;
|
||||
revision: number;
|
||||
definition_kind: DefinitionKind;
|
||||
definition_hash: string;
|
||||
parameters: DistributionParameter[];
|
||||
constraints: Record<string, unknown>;
|
||||
source_fingerprints: Array<Record<string, unknown>>;
|
||||
entries: DistributionListEntry[];
|
||||
created_by_account_id?: string | null;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type DistributionList = {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
scope_type: "tenant" | "group" | "user";
|
||||
scope_id?: string | null;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
status: string;
|
||||
current_revision: number;
|
||||
resource_revision: number;
|
||||
etag: string;
|
||||
metadata: Record<string, unknown>;
|
||||
deleted_at?: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
revision: DistributionListRevision;
|
||||
};
|
||||
|
||||
export type DistributionListPayload = {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
scope_type: "tenant" | "group" | "user";
|
||||
scope_id?: string | null;
|
||||
definition_kind: DefinitionKind;
|
||||
parameters: DistributionParameter[];
|
||||
constraints: Record<string, unknown>;
|
||||
entries: DistributionListEntryInput[];
|
||||
metadata: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type DistributionExplanation = {
|
||||
code: string;
|
||||
message: string;
|
||||
severity: "info" | "warning" | "error";
|
||||
provider?: string | null;
|
||||
source?: SourceReference | null;
|
||||
provenance: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type DistributionChannelCandidate = {
|
||||
channel: DistributionChannel;
|
||||
target: string;
|
||||
target_key: string;
|
||||
status: DistributionOutcome;
|
||||
contact_point_id?: string | null;
|
||||
locale?: string | null;
|
||||
preferred: boolean;
|
||||
reason_code?: string | null;
|
||||
explanation?: string | null;
|
||||
source?: SourceReference | null;
|
||||
decision_provenance: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type DistributionRecipient = {
|
||||
recipient_key: string;
|
||||
display_name: string;
|
||||
status: DistributionOutcome;
|
||||
channels: DistributionChannelCandidate[];
|
||||
identity_id?: string | null;
|
||||
account_id?: string | null;
|
||||
contact_id?: string | null;
|
||||
organization_unit_id?: string | null;
|
||||
function_id?: string | null;
|
||||
source_entry_ids: string[];
|
||||
explanations: DistributionExplanation[];
|
||||
attributes: Record<string, unknown>;
|
||||
provenance: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type ProviderEvidence = {
|
||||
provider: string;
|
||||
source: SourceReference;
|
||||
actual_revision?: string | null;
|
||||
actual_fingerprint?: string | null;
|
||||
stale: boolean;
|
||||
generated_at?: string | null;
|
||||
details: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type ExpansionResult = {
|
||||
list_id: string;
|
||||
revision_id: string;
|
||||
revision: number;
|
||||
definition_hash: string;
|
||||
recipients: DistributionRecipient[];
|
||||
excluded: DistributionRecipient[];
|
||||
diagnostics: DistributionExplanation[];
|
||||
provider_evidence: ProviderEvidence[];
|
||||
expansion_hash: string;
|
||||
generated_at: string;
|
||||
snapshot_id?: string | null;
|
||||
stale: boolean;
|
||||
truncated: boolean;
|
||||
};
|
||||
|
||||
export type ExpansionPayload = {
|
||||
revision?: number | null;
|
||||
effective_at?: string | null;
|
||||
purpose?: string | null;
|
||||
requested_channels?: DistributionChannel[];
|
||||
parameters?: Record<string, unknown>;
|
||||
preview?: boolean;
|
||||
freeze?: boolean;
|
||||
idempotency_key?: string | null;
|
||||
max_entries?: number;
|
||||
max_results?: number;
|
||||
max_depth?: number;
|
||||
max_provider_results?: number;
|
||||
};
|
||||
|
||||
export type DistributionSnapshot = {
|
||||
id: string;
|
||||
list_id: string;
|
||||
revision_id: string;
|
||||
revision: number;
|
||||
expansion_hash: string;
|
||||
effective_at: string;
|
||||
recipient_count: number;
|
||||
excluded_count: number;
|
||||
stale: boolean;
|
||||
truncated: boolean;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type ProviderOption = {
|
||||
key: string;
|
||||
kind: EntryKind;
|
||||
label: string;
|
||||
description?: string | null;
|
||||
provider: string;
|
||||
source: SourceReference;
|
||||
available: boolean;
|
||||
reason?: string | null;
|
||||
metadata: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type ProviderCatalogue = {
|
||||
items: ProviderOption[];
|
||||
unavailable_providers: DistributionExplanation[];
|
||||
};
|
||||
|
||||
export async function listDistributionLists(
|
||||
settings: ApiSettings,
|
||||
query = ""
|
||||
): Promise<DistributionList[]> {
|
||||
const result = await apiFetch<{ items: DistributionList[] }>(
|
||||
settings,
|
||||
apiPath("/api/v1/dist-lists/lists", { query, limit: 500 })
|
||||
);
|
||||
return result.items;
|
||||
}
|
||||
|
||||
export function createDistributionList(
|
||||
settings: ApiSettings,
|
||||
payload: DistributionListPayload
|
||||
): Promise<DistributionList> {
|
||||
return apiFetch(settings, "/api/v1/dist-lists/lists", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
export function updateDistributionList(
|
||||
settings: ApiSettings,
|
||||
item: DistributionList,
|
||||
payload: DistributionListPayload
|
||||
): Promise<DistributionList> {
|
||||
return apiFetch(settings, `/api/v1/dist-lists/lists/${encodeURIComponent(item.id)}`, {
|
||||
method: "PUT",
|
||||
headers: { "If-Match": item.etag },
|
||||
body: JSON.stringify({ ...payload, base_revision: item.resource_revision })
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteDistributionList(
|
||||
settings: ApiSettings,
|
||||
item: DistributionList
|
||||
): Promise<void> {
|
||||
return apiFetch(settings, `/api/v1/dist-lists/lists/${encodeURIComponent(item.id)}`, {
|
||||
method: "DELETE",
|
||||
headers: { "If-Match": item.etag },
|
||||
body: JSON.stringify({ base_revision: item.resource_revision })
|
||||
});
|
||||
}
|
||||
|
||||
export function expandDistributionList(
|
||||
settings: ApiSettings,
|
||||
listId: string,
|
||||
payload: ExpansionPayload
|
||||
): Promise<ExpansionResult> {
|
||||
return apiFetch(settings, `/api/v1/dist-lists/lists/${encodeURIComponent(listId)}/expand`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
export async function listDistributionSnapshots(
|
||||
settings: ApiSettings,
|
||||
listId: string
|
||||
): Promise<DistributionSnapshot[]> {
|
||||
const result = await apiFetch<{ items: DistributionSnapshot[] }>(
|
||||
settings,
|
||||
`/api/v1/dist-lists/lists/${encodeURIComponent(listId)}/snapshots`
|
||||
);
|
||||
return result.items;
|
||||
}
|
||||
|
||||
export function listProviderOptions(
|
||||
settings: ApiSettings,
|
||||
query = "",
|
||||
limit = 50,
|
||||
signal?: AbortSignal
|
||||
): Promise<ProviderCatalogue> {
|
||||
return apiFetch(
|
||||
settings,
|
||||
apiPath("/api/v1/dist-lists/providers", { query, limit }),
|
||||
{ signal }
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import {
|
||||
FormField,
|
||||
SearchableSelect,
|
||||
type ApiSettings,
|
||||
type SearchableSelectOption
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
listDistributionLists,
|
||||
type DistributionList
|
||||
} from "../api/distLists";
|
||||
|
||||
export type DistributionListPickerProps = {
|
||||
settings: ApiSettings;
|
||||
value: string;
|
||||
onChange: (listId: string, item: DistributionList | null) => void;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
selected?: DistributionList | null;
|
||||
};
|
||||
|
||||
export default function DistributionListPicker({
|
||||
settings,
|
||||
value,
|
||||
onChange,
|
||||
label = "Distribution list",
|
||||
placeholder = "Search distribution lists",
|
||||
disabled = false,
|
||||
required = false,
|
||||
selected = null
|
||||
}: DistributionListPickerProps) {
|
||||
const cache = useRef(new Map<string, DistributionList>());
|
||||
if (selected) cache.current.set(selected.id, selected);
|
||||
|
||||
const selectedOption = useMemo<SearchableSelectOption | null>(() => {
|
||||
const item = selected ?? cache.current.get(value);
|
||||
if (!item) return value ? { value, label: value } : null;
|
||||
return optionFor(item);
|
||||
}, [selected, value]);
|
||||
|
||||
const loadOptions = useCallback(async (query: string) => {
|
||||
const items = await listDistributionLists(settings, query);
|
||||
for (const item of items) cache.current.set(item.id, item);
|
||||
return items.map(optionFor);
|
||||
}, [settings]);
|
||||
|
||||
return (
|
||||
<FormField label={label}>
|
||||
<SearchableSelect
|
||||
value={value}
|
||||
selectedOption={selectedOption}
|
||||
loadOptions={loadOptions}
|
||||
onChange={(nextValue) =>
|
||||
onChange(nextValue, cache.current.get(nextValue) ?? null)}
|
||||
placeholder={placeholder}
|
||||
searchPlaceholder="Search by list name"
|
||||
emptyText="No matching distribution lists."
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
/>
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
|
||||
function optionFor(item: DistributionList): SearchableSelectOption {
|
||||
return {
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
description: `${item.revision.entries.length} entries · revision ${item.current_revision}`,
|
||||
searchText: `${item.description ?? ""} ${item.scope_type}`
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
export { default } from "./module";
|
||||
export * from "./module";
|
||||
export * from "./api/distLists";
|
||||
export { default as DistributionListPicker } from "./components/DistributionListPicker";
|
||||
export type { DistributionListPickerProps } from "./components/DistributionListPicker";
|
||||
export { default as DistributionListsPage } from "./features/distributionLists/DistributionListsPage";
|
||||
@@ -0,0 +1,47 @@
|
||||
import { createElement, lazy } from "react";
|
||||
import type { PlatformWebModule } from "@govoplan/core-webui";
|
||||
import "./styles/dist-lists.css";
|
||||
|
||||
const DistributionListsPage = lazy(
|
||||
() => import("./features/distributionLists/DistributionListsPage")
|
||||
);
|
||||
|
||||
const readScopes = [
|
||||
"dist_lists:list:read",
|
||||
"dist_lists:list:write",
|
||||
"dist_lists:list:admin"
|
||||
];
|
||||
|
||||
export const distributionListsModule: PlatformWebModule = {
|
||||
id: "dist_lists",
|
||||
label: "Distribution Lists",
|
||||
version: "0.1.14",
|
||||
optionalDependencies: [
|
||||
"addresses",
|
||||
"identity",
|
||||
"idm",
|
||||
"organizations",
|
||||
"dataflow",
|
||||
"policy"
|
||||
],
|
||||
navItems: [
|
||||
{
|
||||
to: "/distribution-lists",
|
||||
label: "Distribution Lists",
|
||||
iconName: "list-tree",
|
||||
anyOf: readScopes,
|
||||
order: 74
|
||||
}
|
||||
],
|
||||
routes: [
|
||||
{
|
||||
path: "/distribution-lists",
|
||||
anyOf: readScopes,
|
||||
order: 74,
|
||||
render: ({ settings, auth }) =>
|
||||
createElement(DistributionListsPage, { settings, auth })
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default distributionListsModule;
|
||||
@@ -0,0 +1,552 @@
|
||||
.dist-lists-page {
|
||||
position: relative;
|
||||
height: calc(100vh - 115px);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.dist-lists-page *,
|
||||
.dist-lists-page *::before,
|
||||
.dist-lists-page *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dist-lists-shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(250px, 300px) minmax(0, 1fr);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border: var(--border-line);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.dist-lists-sidebar,
|
||||
.dist-lists-workspace,
|
||||
.dist-lists-list-frame,
|
||||
.dist-lists-content-frame,
|
||||
.dist-lists-content {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.dist-lists-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-right: var(--border-line);
|
||||
background: var(--panel-soft);
|
||||
}
|
||||
|
||||
.dist-lists-sidebar-toolbar,
|
||||
.dist-lists-workspace-toolbar,
|
||||
.dist-lists-section-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
flex: 0 0 auto;
|
||||
border-bottom: var(--border-line);
|
||||
background: var(--panel-header);
|
||||
}
|
||||
|
||||
.dist-lists-sidebar-toolbar {
|
||||
min-height: 52px;
|
||||
padding: 8px 10px 8px 14px;
|
||||
}
|
||||
|
||||
.dist-lists-sidebar-toolbar > span,
|
||||
.dist-lists-toolbar-actions,
|
||||
.dist-lists-row-actions,
|
||||
.dist-lists-preview-actions,
|
||||
.dist-lists-explanation-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.dist-lists-search {
|
||||
padding: 9px;
|
||||
border-bottom: var(--border-line);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.dist-lists-search input {
|
||||
width: 100%;
|
||||
min-height: 34px;
|
||||
padding: 7px 9px;
|
||||
}
|
||||
|
||||
.dist-lists-list-frame {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dist-lists-list {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.dist-lists-list > button {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-height: 56px;
|
||||
padding: 8px 9px;
|
||||
border: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dist-lists-list > button:hover,
|
||||
.dist-lists-list > button:focus-visible {
|
||||
background: var(--primary-soft);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dist-lists-list > button.is-selected {
|
||||
background: var(--primary-soft-strong);
|
||||
box-shadow: inset 3px 0 0 var(--accent);
|
||||
}
|
||||
|
||||
.dist-lists-list > button > span:first-child {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dist-lists-list strong,
|
||||
.dist-lists-list small,
|
||||
.dist-lists-current-title strong,
|
||||
.dist-lists-current-title small,
|
||||
.dist-lists-source-cell strong,
|
||||
.dist-lists-source-cell small {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dist-lists-list strong,
|
||||
.dist-lists-current-title strong,
|
||||
.dist-lists-source-cell strong {
|
||||
color: var(--text-strong);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dist-lists-list small,
|
||||
.dist-lists-current-title small,
|
||||
.dist-lists-source-cell small {
|
||||
margin-top: 3px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.dist-lists-workspace {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.dist-lists-workspace-toolbar {
|
||||
min-height: 58px;
|
||||
padding: 8px 10px 8px 14px;
|
||||
}
|
||||
|
||||
.dist-lists-current-title {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.dist-lists-toolbar-actions {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.dist-lists-toolbar-actions .btn {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dist-lists-alerts {
|
||||
flex: 0 0 auto;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.dist-lists-alerts:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dist-lists-alerts .alert {
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
.dist-lists-content-frame {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dist-lists-content {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.dist-lists-empty,
|
||||
.dist-lists-inline-empty {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 110px;
|
||||
padding: 18px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dist-lists-definition-fields {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1.4fr) minmax(160px, 0.7fr) minmax(160px, 0.7fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.dist-lists-definition-fields .form-field:last-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.dist-lists-definition-fields input,
|
||||
.dist-lists-definition-fields select,
|
||||
.dist-lists-definition-fields textarea,
|
||||
.dist-lists-dialog-form input,
|
||||
.dist-lists-dialog-form select,
|
||||
.dist-lists-dialog-form textarea,
|
||||
.dist-lists-preview-inputs input,
|
||||
.dist-lists-parameter-row input,
|
||||
.dist-lists-parameter-row select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dist-lists-definition-fields textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.dist-lists-section {
|
||||
min-width: 0;
|
||||
margin-bottom: 14px;
|
||||
border: var(--border-line);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.dist-lists-entry-section {
|
||||
min-height: 260px;
|
||||
}
|
||||
|
||||
.dist-lists-section-heading {
|
||||
min-height: 44px;
|
||||
padding: 7px 10px;
|
||||
}
|
||||
|
||||
.dist-lists-section-heading > span {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dist-lists-section-heading strong,
|
||||
.dist-lists-section-heading small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dist-lists-section-heading strong {
|
||||
color: var(--text-strong);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dist-lists-section-heading small {
|
||||
margin-top: 2px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.dist-lists-parameter-list {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.dist-lists-parameter-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(130px, 1fr) minmax(150px, 1.2fr) minmax(120px, 0.8fr) minmax(130px, 1fr) auto 36px;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 7px;
|
||||
border-bottom: var(--border-line);
|
||||
}
|
||||
|
||||
.dist-lists-parameter-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.dist-lists-grid {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dist-lists-row-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dist-lists-preview-controls {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: var(--border-line);
|
||||
}
|
||||
|
||||
.dist-lists-preview-inputs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(160px, 1fr));
|
||||
gap: 10px;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.dist-lists-preview-actions {
|
||||
flex: 0 0 auto;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.dist-lists-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(110px, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.dist-lists-metrics > span {
|
||||
min-height: 62px;
|
||||
padding: 9px 10px;
|
||||
border: var(--border-line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.dist-lists-metrics small,
|
||||
.dist-lists-metrics strong {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dist-lists-metrics small {
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dist-lists-metrics strong {
|
||||
margin-top: 6px;
|
||||
color: var(--text-strong);
|
||||
font-size: 15px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.dist-lists-content > .alert {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.dist-lists-result-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin: 14px 0 8px;
|
||||
}
|
||||
|
||||
.dist-lists-result-heading code,
|
||||
.dist-lists-explanation-summary code {
|
||||
overflow: hidden;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dist-lists-candidates {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dist-lists-dialog-form {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dist-lists-entry-dialog {
|
||||
width: min(780px, calc(100vw - 32px));
|
||||
max-width: 780px;
|
||||
}
|
||||
|
||||
.dist-lists-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dist-lists-provider-notices {
|
||||
display: grid;
|
||||
max-height: 150px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dist-lists-provider-notices .alert {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.dist-lists-channel-fieldset {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
border: var(--border-line);
|
||||
}
|
||||
|
||||
.dist-lists-channel-fieldset legend {
|
||||
padding: 0 5px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.dist-lists-explanation-dialog {
|
||||
width: min(760px, calc(100vw - 32px));
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
.dist-lists-explanation-content {
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.dist-lists-explanation-content h3 {
|
||||
margin: 8px 0 0;
|
||||
color: var(--text-strong);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dist-lists-decision {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 6px 10px;
|
||||
padding: 10px;
|
||||
border: var(--border-line);
|
||||
border-left: 3px solid var(--accent);
|
||||
background: var(--panel-soft);
|
||||
}
|
||||
|
||||
.dist-lists-decision span strong,
|
||||
.dist-lists-decision span small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dist-lists-decision span small {
|
||||
margin-top: 3px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.dist-lists-decision p,
|
||||
.dist-lists-decision pre {
|
||||
grid-column: 1 / -1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dist-lists-explanation-content pre {
|
||||
max-height: 210px;
|
||||
margin: 0;
|
||||
padding: 9px;
|
||||
overflow: auto;
|
||||
border: var(--border-line);
|
||||
background: var(--panel-soft);
|
||||
color: var(--text);
|
||||
font-size: 11px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 1040px) {
|
||||
.dist-lists-shell {
|
||||
grid-template-columns: minmax(220px, 260px) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.dist-lists-workspace-toolbar {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.dist-lists-toolbar-actions {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dist-lists-definition-fields,
|
||||
.dist-lists-preview-inputs {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.dist-lists-parameter-row {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr)) auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.dist-lists-page {
|
||||
height: auto;
|
||||
min-height: calc(100vh - 115px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dist-lists-shell {
|
||||
display: flex;
|
||||
min-height: calc(100vh - 115px);
|
||||
overflow: visible;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dist-lists-sidebar {
|
||||
min-height: 230px;
|
||||
max-height: 36vh;
|
||||
border-right: 0;
|
||||
border-bottom: var(--border-line);
|
||||
}
|
||||
|
||||
.dist-lists-workspace {
|
||||
min-height: 560px;
|
||||
}
|
||||
|
||||
.dist-lists-workspace-toolbar,
|
||||
.dist-lists-preview-controls {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dist-lists-toolbar-actions,
|
||||
.dist-lists-preview-actions {
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dist-lists-definition-fields,
|
||||
.dist-lists-preview-inputs,
|
||||
.dist-lists-form-grid,
|
||||
.dist-lists-metrics,
|
||||
.dist-lists-channel-fieldset,
|
||||
.dist-lists-parameter-row {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference path="../../../govoplan-core/webui/src/vite-env.d.ts" />
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2020"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"preserveSymlinks": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@govoplan/core-webui": ["../../govoplan-core/webui/src/index.ts"],
|
||||
"@govoplan/core-webui/*": ["../../govoplan-core/webui/src/*"],
|
||||
"lucide-react": ["../../govoplan-core/webui/node_modules/lucide-react/dist/lucide-react.d.ts"],
|
||||
"react": ["../../govoplan-core/webui/node_modules/@types/react/index.d.ts"],
|
||||
"react/jsx-runtime": ["../../govoplan-core/webui/node_modules/@types/react/jsx-runtime.d.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user