diff --git a/src/lib/components/textEditor/TextEditor.tsx b/src/lib/components/textEditor/TextEditor.tsx index a603ffe35eece008556fa05438ccddf298c2f533..302215403085fb9c820a2f6a06b66c19358eec35 100644 --- a/src/lib/components/textEditor/TextEditor.tsx +++ b/src/lib/components/textEditor/TextEditor.tsx @@ -22,7 +22,7 @@ type TextEditorProps = { handleSave: (elements: SerializedEditorState, generateEmail: boolean) => void; saveDisabled: boolean; variableOptions: string[]; - alarmMode: string; + insight: InsightModel; }; function InitialStateLoader({ editorState }: { editorState: SerializedEditorState | null }) { @@ -47,7 +47,7 @@ export function TextEditor({ saveDisabled, children, variableOptions, - alarmMode, + insight, }: TextEditorProps) { const contentEditableRef = useRef<HTMLDivElement>(null); const updateTimeoutRef = useRef<NodeJS.Timeout | null>(null); @@ -70,7 +70,7 @@ export function TextEditor({ return ( <LexicalComposer initialConfig={initialConfig}> <div className="editor-toolbar flex items-center bg-secondary-50 rounded mt-4 space-x-2" style={{ marginBottom: -8 }}> - <PreviewPlugin contentEditable={contentEditableRef} /> + <PreviewPlugin contentEditable={contentEditableRef} insight={insight} /> {showToolbar && <ToolbarPlugin />} </div> <div className="border p-2"> @@ -83,7 +83,7 @@ export function TextEditor({ </div> <div className="preview min-h-24 p-3 hidden"></div> </div> - <InsertVariablesPlugin variableOptions={variableOptions} alarmMode={alarmMode} /> + <InsertVariablesPlugin variableOptions={variableOptions} alarmMode={insight.alarmMode} /> <InitialStateLoader editorState={editorState} /> <div className="flex flex-row mt-3 justify-between"> diff --git a/src/lib/components/textEditor/plugins/PreviewPlugin.tsx b/src/lib/components/textEditor/plugins/PreviewPlugin.tsx index fa40d7059035dbb0dd5ec7ac239eb0c97250df74..56e8c298e05e03597dd6d7fed965fe08748211f8 100644 --- a/src/lib/components/textEditor/plugins/PreviewPlugin.tsx +++ b/src/lib/components/textEditor/plugins/PreviewPlugin.tsx @@ -9,8 +9,15 @@ import { VisualizationSettingsType } from '@/lib/vis/common'; // @ts-expect-error missing import import { newPlot, toImage } from 'plotly.js/dist/plotly'; +import { AppearanceMap, InsightModel } from 'ts-common'; +import { wsGetInsightAppearances, wsGetInsights, wsInsightAppearancesSubscription } from '@/lib/data-access/broker/wsInsightSharing'; -export function PreviewPlugin({ contentEditable }: { contentEditable: RefObject<HTMLDivElement | null> }): JSX.Element { +interface PreviewPluginProps { + contentEditable: RefObject<HTMLDivElement | null>; + insight: InsightModel; +} + +export function PreviewPlugin({ contentEditable, insight }: PreviewPluginProps): JSX.Element { const [editor] = useLexicalComposerContext(); function updatePreview() { @@ -19,7 +26,16 @@ export function PreviewPlugin({ contentEditable }: { contentEditable: RefObject< if (preview == null) return; const html = $generateHtmlFromNodes(editor as any); // any needed to avoid excessive ts error - preview.innerHTML = await populateTemplate(html); + + if (insight.alarmMode == 'entityAppearances') { + wsInsightAppearancesSubscription(async nodeAppearances => { + if (nodeAppearances == null) return; + preview.innerHTML = await populateTemplate(html, nodeAppearances); + }); + await wsGetInsightAppearances({ insightID: insight.id }); + } else { + preview.innerHTML = await populateTemplate(html); + } }); } @@ -47,7 +63,7 @@ export function PreviewPlugin({ contentEditable }: { contentEditable: RefObject< return string.replace(regexp, () => replacements[i++]); } - async function populateTemplate(html: string) { + async function populateTemplate(html: string, nodeAppearance?: AppearanceMap) { const regex = / *?{\{ *?(\w*?):([\w •]*?) *?\}\} *?/gm; return replaceAllAsync(html, regex, async (_: string, _type: string, name: string) => { @@ -63,6 +79,14 @@ export function PreviewPlugin({ contentEditable }: { contentEditable: RefObject< return ` ${value} `; } + case VariableType.list: { + if (nodeAppearance == null) { + throw new Error('No nodeAppearances available to populate tempate'); + } + const headers = ['nodeID', 'count', 'queries']; // TODO: get headers from data + return generateList(nodeAppearance, headers); + } + case VariableType.visualization: { const activeVisualization = vis.openVisualizationArray.find(x => x.name == name) as Vis1DProps & VisualizationSettingsType; @@ -186,3 +210,70 @@ export function PreviewPlugin({ contentEditable }: { contentEditable: RefObject< </ul> ); } + +export function generateTable(data: any[], headers: string[]): string { + if (!data || data.length === 0) { + return '<p>No data available</p>'; // Return a message if no data is provided + } + + let tableHTML = ` + <table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse; width: 100%; margin-top: 20px;"> + <thead> + <tr> + `; + + // Generate table headers dynamically from the headers array + headers.forEach(header => { + tableHTML += `<th>${header}</th>`; + }); + + tableHTML += `</tr></thead><tbody>`; + + // Generate rows for the table dynamically based on the data + data.forEach(row => { + tableHTML += `<tr>`; + headers.forEach(header => { + // For each header, grab the corresponding field in the data row + // If the field is an array (e.g. queries), convert it to a string for display + if (Array.isArray(row[header])) { + tableHTML += `<td>${row[header].join(', ')}</td>`; + } else { + tableHTML += `<td>${row[header] || ''}</td>`; + } + }); + tableHTML += `</tr>`; + }); + + // Close table tags + tableHTML += `</tbody></table>`; + + return tableHTML; +} + +export function generateList(data: any[], headers: string[]): string { + if (!data || data.length === 0) { + return '<p>No data available</p>'; + } + + let listHTML = `<ul style="margin-top: 20px;">`; + + data.forEach(row => { + listHTML += `<li><strong>${headers[0]}:</strong> ${row.nodeID || 'N/A'}`; + + for (let i = 1; i < headers.length; i++) { + const key = headers[i]; + let value = row[key]; + + if (Array.isArray(value)) { + value = value.join(', '); // Convert array to string + } + + listHTML += `, <strong>${headers[i]}:</strong> ${value ?? 'N/A'}`; + } + + listHTML += `</li>`; + }); + + listHTML += `</ul>`; + return listHTML; +} diff --git a/src/lib/data-access/broker/wsInsightSharing.ts b/src/lib/data-access/broker/wsInsightSharing.ts index 42dbc5471e12820110869ff24b1bc329194d8d66..ddc57a7031b6338449724851a1f2ab12bd07f04a 100644 --- a/src/lib/data-access/broker/wsInsightSharing.ts +++ b/src/lib/data-access/broker/wsInsightSharing.ts @@ -78,6 +78,23 @@ export const wsDeleteInsight: WsFrontendCall<{ id: number }, wsReturnKey.insight ); }; +export const wsGetInsightAppearances: WsFrontendCall<{ insightID: number }, wsReturnKey.insightAppearance> = (params, callback) => { + Broker.instance().sendMessage( + { + key: wsKeys.insight, + subKey: wsSubKeys.getAppearances, + body: { insightID: params.insightID }, + }, + callback, + ); +}; +export function wsInsightAppearancesSubscription(callback: ResponseCallback<wsReturnKey.insightAppearance>) { + const id = Broker.instance().subscribe(callback, wsReturnKey.insightAppearance); + return () => { + Broker.instance().unSubscribe(wsReturnKey.insightAppearance, id); + }; +} + export function wsInsightSubscription(callback: ResponseCallback<wsReturnKey.insightResult>) { const id = Broker.instance().subscribe(callback, wsReturnKey.insightResult); return () => { diff --git a/src/lib/insight-sharing/FormInsight.tsx b/src/lib/insight-sharing/FormInsight.tsx index 59e2f87e5523350cd83c18231f97013b89aab255..b13a422d5b37191b4edda4c3b271458c397c66dd 100644 --- a/src/lib/insight-sharing/FormInsight.tsx +++ b/src/lib/insight-sharing/FormInsight.tsx @@ -453,7 +453,7 @@ export function FormInsight(props: Props) { handleSave={handleSave} saveDisabled={!valid} variableOptions={selectedQueryID.entities} - alarmMode={localInsight.alarmMode} + insight={localInsight} > <Button label="Delete"