74 lines
6.0 KiB
TypeScript
74 lines
6.0 KiB
TypeScript
import { beginActivity, showActivityFrame } from '../../platform/activity-store';
|
|
import { useState } from 'react';
|
|
import { useApp } from '../../app/AppProvider';
|
|
import { Dialog } from '../../components/Dialog/Dialog';
|
|
import { buildWorkViewExport, renderWorkViewCsv, renderWorkViewHtml, renderWorkViewMarkdown } from '../../exports/work-view-export';
|
|
import { downloadText } from '../../exports/text-download';
|
|
import { browserDownloadAdapter } from '../../platform/browser/browser-download-adapter';
|
|
import { useFilteredRecords } from '../search-filter/use-filtered-records';
|
|
import { selectWorkFacets } from '../search-filter/work-facets';
|
|
import { useContextClusterIndex } from '../context-layer/use-context-cluster-index';
|
|
import { useRelatedFindings } from '../related-findings/RelatedFindings';
|
|
import { useWorkPackages } from '../../remediation/use-work-packages';
|
|
import { WorkPackagesWindow } from '../../remediation/WorkPackagesWindow';
|
|
import { UNASSIGNED_WORK_PACKAGE, WORK_PACKAGE_FACET_ID } from '../../remediation/remediation-engine';
|
|
import { numberedClusterLabel, publishedClusterColour } from '../sidebar/context-cluster-display';
|
|
import { useComplianceWorkFacets } from '../../compliance/ui/compliance-work-facets';
|
|
|
|
export function ActionlistControls() {
|
|
const app = useApp();
|
|
const records = useFilteredRecords();
|
|
const contextClusters = useContextClusterIndex();
|
|
const remediation = useWorkPackages();
|
|
const related = useRelatedFindings();
|
|
const workPackages = remediation.status === 'ready' ? remediation.index : undefined;
|
|
const packageOverview = remediation.status === 'ready' ? remediation.overview : undefined;
|
|
const complianceFacets = useComplianceWorkFacets();
|
|
const [failed, setFailed] = useState(false);
|
|
const [packagesOpen, setPackagesOpen] = useState(false);
|
|
const exportView = async (format: 'csv' | 'md' | 'html') => {
|
|
if (!app.dataset || !app.activeSheet || !records.length) return;
|
|
const activity = beginActivity('Core Engine', `Preparing Work ${format.toUpperCase()} export`);
|
|
try {
|
|
await showActivityFrame();
|
|
const facets = selectWorkFacets(app.activeSheet, { ...app, contextClusters, workPackages, complianceFacets });
|
|
const statusLabel = app.activeProfile.presentation.statusFilters.find(filter => filter.value === app.statusFilter)?.label ?? app.statusFilter;
|
|
const filters = Object.entries(app.facetFilters).map(([id, filter]) => {
|
|
const facet = facets.find(item => item.id === id);
|
|
return `${facet?.label ?? id}: ${filter.values?.map(value => facet?.values.find(item => item.value === value)?.label ?? value).join(', ') ?? ''}${filter.minimum ? ` ≥ ${filter.minimum}` : ''}${filter.maximum ? ` ≤ ${filter.maximum}` : ''}`;
|
|
});
|
|
const report = buildWorkViewExport({ dataset: app.dataset, sheet: app.activeSheet, records,
|
|
generatedAt: new Date().toISOString(), reviews: app.reviews, titleColumn: app.titleColumn,
|
|
context: [
|
|
{ label: 'Scope', value: [`Project: ${app.projectId}`, `Assessments: ${app.activeAssessmentIds?.join(', ') ?? 'geladener Workspace'}`, `Scan runs: ${app.activeScanRunIds?.join(', ') ?? 'geladener Workspace'}`].join('\n') },
|
|
{ label: 'Filter', value: [`Result Status: ${statusLabel}`, ...filters, `Suche: ${app.search || '—'}`, `My Workspace: ${app.actionOnly ? 'Ja' : 'Nein'}`, `Drilldown: ${app.pathFindingIds.length ? `${app.pathFindingIds.length} Record IDs` : 'Nein'}`].join('\n') },
|
|
{ label: 'Sortierung', value: `${app.sortColumn} · ${app.sortDirection}` }
|
|
] });
|
|
const text = format === 'html' ? renderWorkViewHtml(report) : format === 'md' ? renderWorkViewMarkdown(report)
|
|
: renderWorkViewCsv(report, app.delimiter === 'auto' ? ';' : app.delimiter);
|
|
const stem = app.dataset.fileName.replace(/\.[^.]+$/, '').replace(/[<>:"/\\|?*\u0000-\u001f]/g, '_');
|
|
downloadText(browserDownloadAdapter, `${stem}-work-view.${format}`, text,
|
|
format === 'html' ? 'text/html;charset=utf-8' : format === 'md' ? 'text/markdown;charset=utf-8' : 'text/csv;charset=utf-8');
|
|
setFailed(false);
|
|
} catch { activity.fail(); setFailed(true); } finally { activity.complete(); }
|
|
};
|
|
return <div className="header-export-row" aria-label="Export current view including Results">
|
|
{/* Remediation Engine extension point: packages appear only while the engine is on and its result is ready. */}
|
|
{workPackages && <button className="btn export-btn work-packages-button" type="button" data-work-packages-open
|
|
aria-pressed={Boolean(packageOverview && packagesOpen)}
|
|
title={`Show all work packages and Unassigned for this assessment`} onClick={() => setPackagesOpen(open => !open)}>Work Packages</button>}
|
|
{(['csv', 'md', 'html'] as const).map(format => <button key={format} className="btn export-btn" type="button" disabled={!records.length}
|
|
title={`Export current view including Results (${records.length} records)`} onClick={() => exportView(format)}>{format.toUpperCase()}</button>)}
|
|
{packageOverview && packagesOpen && <WorkPackagesWindow overview={packageOverview}
|
|
clusterOrder={contextClusters?.publication.clusters.map(cluster => cluster.id)}
|
|
clusterLabels={Object.fromEntries(workPackages!.packages.map(p => [p.clusterId, numberedClusterLabel(p.clusterId, p.clusterLabel, contextClusters?.publication.clusters)]))}
|
|
clusterSeverities={Object.fromEntries(workPackages!.packages.map(p => [p.clusterId,
|
|
publishedClusterColour(contextClusters?.publication.clusters.find(cluster => cluster.id === p.clusterId))]))}
|
|
onSelectRelated={() => related?.showAllInWork()}
|
|
onClose={() => setPackagesOpen(false)}
|
|
onSelectUnassigned={() => app.setFacetFilter(WORK_PACKAGE_FACET_ID, { values: [UNASSIGNED_WORK_PACKAGE] })}
|
|
onSelect={workPackage => app.setFacetFilter(WORK_PACKAGE_FACET_ID, { values: [workPackage.id] })}/>}
|
|
{failed && <Dialog title="Export unavailable" onClose={() => setFailed(false)}><p>The current view could not be exported completely. No partial file was downloaded.</p></Dialog>}
|
|
</div>;
|
|
}
|