From c0b3a7d02d851ecdb7facee356e37670b6858766 Mon Sep 17 00:00:00 2001 From: Samed <sbalcioglu@graphpolaris.com> Date: Mon, 2 Dec 2024 11:38:37 +0100 Subject: [PATCH 1/9] feat: report backend implementation --- .../lib/components/textEditor/TextEditor.tsx | 4 +- .../textEditor/plugins/ToolbarPlugin.tsx | 5 +- libs/shared/lib/data-access/api/apiService.ts | 106 ++++++++ libs/shared/lib/data-access/broker/broker.tsx | 6 +- libs/shared/lib/data-access/broker/types.ts | 2 +- .../data-access/broker/wsInsightSharing.ts | 75 ++++++ .../data-access/store/insightSharingSlice.ts | 70 ++++++ libs/shared/lib/data-access/store/store.ts | 2 + .../lib/insight-sharing/InsightDialog.tsx | 10 +- .../lib/insight-sharing/SettingsPanel.tsx | 85 +++++-- .../insight-sharing/alerting/AlertingForm.tsx | 147 +++++++++++- .../insight-sharing/components/AddItem.tsx | 59 ++++- .../insight-sharing/components/Sidebar.tsx | 45 +++- .../reporting/ReportingForm.tsx | 226 ++++++++++++++---- 14 files changed, 743 insertions(+), 99 deletions(-) create mode 100644 libs/shared/lib/data-access/api/apiService.ts create mode 100644 libs/shared/lib/data-access/broker/wsInsightSharing.ts create mode 100644 libs/shared/lib/data-access/store/insightSharingSlice.ts diff --git a/libs/shared/lib/components/textEditor/TextEditor.tsx b/libs/shared/lib/components/textEditor/TextEditor.tsx index 2b931b872..5c8714452 100644 --- a/libs/shared/lib/components/textEditor/TextEditor.tsx +++ b/libs/shared/lib/components/textEditor/TextEditor.tsx @@ -15,7 +15,7 @@ import { VariableNode } from './VariableNode'; import { fontFamily } from 'html2canvas/dist/types/css/property-descriptors/font-family'; type TextEditorProps = { - editorState: EditorState | undefined; + editorState: EditorState | null; setEditorState: (value: EditorState) => void; showToolbar: boolean; placeholder?: string; @@ -31,7 +31,7 @@ export function TextEditor({ editorState, setEditorState, showToolbar, placehold const initialConfig = { namespace: 'MyEditor', - editorState: editorState, + editorState: editorState ? JSON.stringify(editorState) : null, onError: ErrorHandler, nodes: [VariableNode], }; diff --git a/libs/shared/lib/components/textEditor/plugins/ToolbarPlugin.tsx b/libs/shared/lib/components/textEditor/plugins/ToolbarPlugin.tsx index e4b1ec07e..041367db0 100644 --- a/libs/shared/lib/components/textEditor/plugins/ToolbarPlugin.tsx +++ b/libs/shared/lib/components/textEditor/plugins/ToolbarPlugin.tsx @@ -19,8 +19,9 @@ export function ToolbarPlugin() { }; return [ - <div style={{ flex: '1 1 auto' }}></div>, + <div key= "spacer" style={{ flex: '1 1 auto' }}></div>, <Button + key = "bold" className="my-2" variantType="secondary" variant="ghost" @@ -29,6 +30,7 @@ export function ToolbarPlugin() { onClick={formatBold} />, <Button + key="italic" className="my-2" variantType="secondary" variant="ghost" @@ -37,6 +39,7 @@ export function ToolbarPlugin() { onClick={formatItalic} />, <Button + key="underline" className="my-2 me-2" variantType="secondary" variant="ghost" diff --git a/libs/shared/lib/data-access/api/apiService.ts b/libs/shared/lib/data-access/api/apiService.ts new file mode 100644 index 000000000..a9ea11cfe --- /dev/null +++ b/libs/shared/lib/data-access/api/apiService.ts @@ -0,0 +1,106 @@ +import axios from 'axios'; +import { Insight } from '../store/insightSharingSlice'; + +const API_BASE_URL = 'http://localhost:8000/api'; + +const axiosInstance = axios.create({ + baseURL: API_BASE_URL, + headers: { + 'Content-Type': 'application/json', + } + }); + + export async function createReport(reportData: Partial<Insight>) { + console.log('Creating report with data:', reportData); + try { + const dataToSend = { ...reportData }; + delete dataToSend.id; + delete dataToSend.type; + + const response = await axiosInstance.post('/report/', dataToSend); + console.log('Report creation response:', response); + + const report = { ...response.data, type: 'report' }; + + return report; + } catch (error) { + console.error('Error creating report:', error); + if (axios.isAxiosError(error)) { + console.error('Response data:', error.response?.data); + console.error('Request URL:', error.config?.url); + } + throw error; + } + } + + export async function getReports(saveStateId: string) { + console.log('Fetching reports for saveStateId:', saveStateId); + try { + const response = await axiosInstance.get(`/report/save_state/${saveStateId}/`); + console.log('Reports fetch response:', response); + const reports = response.data.map((report: Insight) => ({ + ...report, + type: 'report', + })); + + return reports; + } catch (error) { + console.error('Error fetching reports:', error); + if (axios.isAxiosError(error)) { + console.error('Response data:', error.response?.data); + console.error('Request URL:', error.config?.url); + } + return []; + } + } + + export async function updateReport(reportId: string, reportData: Partial<Insight>) { + try { + console.log('Updating report with data:', reportData); + const response = await axiosInstance.put(`/report/${reportId}/`, reportData); + return response.data; + } catch (error) { + console.error('Error updating report:', error); + throw error; + } + } + + export async function deleteReport(reportId: string) { + try { + const response = await axiosInstance.delete(`report/${reportId}/`); + return response.data; + } catch (error) { + console.error('Error deleting report:', error); + throw error; + } + } + + export async function createAlert(alertData: any) { + try { + const response = await axiosInstance.post('alert/', alertData); + return response.data; + } catch (error) { + console.error('Error creating alert:', error); + throw error; + } + } + + export async function updateAlert(alertId: string, alertData: any) { + try { + const response = await axiosInstance.put(`alert/${alertId}`, alertData); + return response.data; + } catch (error) { + console.error('Error updating alert:', error); + throw error; + } + } + + export async function deleteAlert(alertId: string) { + try { + const response = await axiosInstance.delete(`alert/${alertId}`); + return response.data; + } catch (error) { + console.error('Error deleting alert:', error); + throw error; + } + } diff --git a/libs/shared/lib/data-access/broker/broker.tsx b/libs/shared/lib/data-access/broker/broker.tsx index 41cfe6f8e..ff09fb28a 100644 --- a/libs/shared/lib/data-access/broker/broker.tsx +++ b/libs/shared/lib/data-access/broker/broker.tsx @@ -200,9 +200,9 @@ export class Broker { } fullMessage.sessionID = this.authHeader?.sessionID ?? ''; - if (message.body && typeof message.body !== 'string') { - fullMessage.body = JSON.stringify(message.body); - } + //if (message.body && typeof message.body !== 'string') { + // fullMessage.body = JSON.stringify(message.body); + //} if (this.webSocket && this.connected && this.webSocket.readyState === 1) this.webSocket.send(JSON.stringify(fullMessage)); else diff --git a/libs/shared/lib/data-access/broker/types.ts b/libs/shared/lib/data-access/broker/types.ts index dc3dd7c22..462762e18 100644 --- a/libs/shared/lib/data-access/broker/types.ts +++ b/libs/shared/lib/data-access/broker/types.ts @@ -19,7 +19,7 @@ type QueryOrchestratorMessage = { queryID: string; }; -export type keyTypeI = 'broadcastState' | 'dbConnection' | 'schema' | 'query' | 'state' | 'user'; +export type keyTypeI = 'broadcastState' | 'dbConnection' | 'schema' | 'query' | 'state' | 'user' | 'insight'| 'report' | 'alert'; export type subKeyTypeI = // Crud | 'create' diff --git a/libs/shared/lib/data-access/broker/wsInsightSharing.ts b/libs/shared/lib/data-access/broker/wsInsightSharing.ts new file mode 100644 index 000000000..98f18b7ec --- /dev/null +++ b/libs/shared/lib/data-access/broker/wsInsightSharing.ts @@ -0,0 +1,75 @@ +import { Broker } from './broker'; +import { Insight, InsightType } from '../store/insightSharingSlice'; + +type GetInsightsResponse = (data: { reports: Insight[]; alerts: Insight[] }, status: string) => void; +export function wsGetInsights(callback?: GetInsightsResponse) { + const internalCallback: GetInsightsResponse = (data, status) => { + if (callback) callback(data, status); + }; + + Broker.instance().sendMessage( + { + key: 'insight', + subKey: 'getAll', + }, + internalCallback + ); +} + +export function wsCreateInsight(insight: Insight, callback?: Function) { + const messageKey = insight.type === 'report' ? 'report' : 'alert'; + Broker.instance().sendMessage( + { + key: messageKey, + subKey: 'create', + body: insight, + }, + (data: any, status: string) => { + if (status === 'Bad Request') { + console.error('Failed to create insight:', data); + } + if (callback) callback(data, status); + } + ); +} + +export function wsUpdateInsight(insight: Insight, callback?: Function) { + const messageKey = insight.type === 'report' ? 'report' : 'alert'; + Broker.instance().sendMessage( + { + key: messageKey, + subKey: 'update', + body: insight, + }, + (data: any, status: string) => { + if (status === 'Bad Request') { + console.error('Failed to update insight:', data); + } + if (callback) callback(data, status); + } + ); +} + +export function wsDeleteInsight(id: string, type: InsightType, callback?: Function) { + const messageKey = type === 'report' ? 'report' : 'alert'; + Broker.instance().sendMessage( + { + key: messageKey, + subKey: 'delete', + body: { id }, + }, + (data: any, status: string) => { + if (status === 'Bad Request') { + console.error('Failed to delete insight:', data); + } + if (callback) callback(data, status); + } + ); +} + +export function wsInsightSubscription(callback: (data: any, status: string) => void) { + const id = Broker.instance().subscribe(callback, 'insight_result'); + return () => { + Broker.instance().unSubscribe('insight_result', id); + }; +} diff --git a/libs/shared/lib/data-access/store/insightSharingSlice.ts b/libs/shared/lib/data-access/store/insightSharingSlice.ts new file mode 100644 index 000000000..c2859b576 --- /dev/null +++ b/libs/shared/lib/data-access/store/insightSharingSlice.ts @@ -0,0 +1,70 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { RootState } from '../store'; + +export type InsightType = 'report' | 'alert'; + +export interface Insight { + id: string; + name: string; + description: string; + recipients: string[]; + frequency: string; + template: string; + save_state_id: string; + createdAt: string; + updatedAt: string; + type: 'report' | 'alert'; +} + +interface InsightState { + insights: Insight[]; + reports: Insight[]; + alerts: Insight[]; +} + +const initialState: InsightState = { + reports: [], + alerts: [], + insights: [] +}; + +const insightSharingSlice = createSlice({ + name: 'insightSharing', + initialState, + reducers: { + setInsights(state, action: PayloadAction<Insight[] | { reports: Insight[] }>) { + const insights = Array.isArray(action.payload) ? action.payload : action.payload.reports; + state.reports = insights.filter((insight) => insight.type === 'report'); + state.alerts = insights.filter((insight) => insight.type === 'alert'); + }, + addInsight(state, action: PayloadAction<Insight>) { + if (action.payload.type === 'report') { + state.reports.push(action.payload); + } else { + state.alerts.push(action.payload); + } + }, + updateInsight(state, action: PayloadAction<Insight>) { + const insights = action.payload.type === 'report' ? state.reports : state.alerts; + const index = insights.findIndex((i) => i.id === action.payload.id); + if (index !== -1) { + insights[index] = { ...action.payload }; + } else { + insights.push(action.payload); + } + }, + deleteInsight(state, action: PayloadAction<{ id: string; type: 'report' | 'alert' }>) { + const insights = action.payload.type === 'report' ? state.reports : state.alerts; + const index = insights.findIndex((i) => i.id === action.payload.id); + if (index !== -1) { + insights.splice(index, 1); + } + }, + }, +}); + +export const { setInsights, addInsight, updateInsight, deleteInsight } = insightSharingSlice.actions; +export const selectReports = (state: RootState): Insight[] => state.insightSharing.reports; +export const selectAlerts = (state: RootState): Insight[] => state.insightSharing.alerts; + +export default insightSharingSlice.reducer; diff --git a/libs/shared/lib/data-access/store/store.ts b/libs/shared/lib/data-access/store/store.ts index c1aea4606..3b1ea1c4d 100644 --- a/libs/shared/lib/data-access/store/store.ts +++ b/libs/shared/lib/data-access/store/store.ts @@ -11,6 +11,7 @@ import visualizationSlice from './visualizationSlice'; import interactionSlice from './interactionSlice'; import policyUsersSlice from './authorizationUsersSlice'; import policyPermissionSlice from './authorizationResourcesSlice'; +import insightSharingSlice from './insightSharingSlice'; export const store = configureStore({ reducer: { @@ -26,6 +27,7 @@ export const store = configureStore({ visualize: visualizationSlice, policyUsers: policyUsersSlice, policyResources: policyPermissionSlice, + insightSharing: insightSharingSlice, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ diff --git a/libs/shared/lib/insight-sharing/InsightDialog.tsx b/libs/shared/lib/insight-sharing/InsightDialog.tsx index 0af6191f2..71d93bf91 100644 --- a/libs/shared/lib/insight-sharing/InsightDialog.tsx +++ b/libs/shared/lib/insight-sharing/InsightDialog.tsx @@ -8,16 +8,13 @@ type Props = { onClose: () => void; }; -const reports = ['Sequence 1', 'Sequence 2']; -const alerts = ['Potential New Incident', 'Potential New Info']; - export function InsightDialog(props: Props) { const [adding, setAdding] = useState<boolean>(false); const [active, setActive] = useState<string>(''); const [activeCategory, setActiveCategory] = useState<MonitorType | undefined>(undefined); - const handleChangeActive = (category: MonitorType, name: string) => { - setActive(name); + const handleChangeActive = (category: MonitorType, id: string) => { + setActive(id); setActiveCategory(category); }; @@ -31,8 +28,6 @@ export function InsightDialog(props: Props) { <DialogContent className="w-5/6 h-5/6 rounded-none py-0 px-0"> <div className="flex w-full h-full"> <Sidebar - reports={reports} - alerts={alerts} changeActive={handleChangeActive} setAdding={setAdding} setActiveCategory={setActiveCategory} @@ -43,6 +38,7 @@ export function InsightDialog(props: Props) { adding={adding} setAdding={setAdding} setActiveCategory={setActiveCategory} + setActive={setActive} /> </div> </DialogContent> diff --git a/libs/shared/lib/insight-sharing/SettingsPanel.tsx b/libs/shared/lib/insight-sharing/SettingsPanel.tsx index 9d0a7a676..54c65d266 100644 --- a/libs/shared/lib/insight-sharing/SettingsPanel.tsx +++ b/libs/shared/lib/insight-sharing/SettingsPanel.tsx @@ -1,10 +1,17 @@ import React, { useEffect, useState } from 'react'; import { ReportingForm } from './reporting/ReportingForm'; import { AlertingForm } from './alerting/AlertingForm'; -import { Button, Icon } from '../components'; import { AddItem } from './components/AddItem'; import { StartScreen } from './components/StartScreen'; import { MonitorType } from './components/Sidebar'; +import { useAppDispatch, useAppSelector } from '../data-access'; +import { + deleteInsight, + selectReports, + selectAlerts, + Insight, +} from '../data-access/store/insightSharingSlice'; +import { wsDeleteInsight } from '../data-access/broker/wsInsightSharing'; type Props = { active: string; @@ -12,42 +19,70 @@ type Props = { adding: boolean; setAdding: (val: boolean) => void; setActiveCategory: (val: MonitorType | undefined) => void; + setActive: (val: string) => void; }; export function SettingsPanel(props: Props) { - const [contacts, setContacts] = useState<string[]>([]); + const dispatch = useAppDispatch(); + const reports = useAppSelector(selectReports); + const alerts = useAppSelector(selectAlerts); - const getContacts = (): string[] => ['Jan', 'Piet']; + const [currentInsight, setCurrentInsight] = useState<Insight | null>(null); useEffect(() => { - if (!contacts) { - const userContacts = getContacts(); - setContacts(userContacts); + if (props.activeCategory && props.active) { + const insights = props.activeCategory === 'report' ? reports : alerts; + const insight = insights.find((i) => i.id === props.active); + if (insight) { + setCurrentInsight(insight); + } + } else { + setCurrentInsight(null); } - }, []); + }, [props.activeCategory, props.active, reports, alerts]); + + const handleDelete = () => { + if (currentInsight) { + dispatch(deleteInsight({ id: currentInsight.id, type: currentInsight.type })); + wsDeleteInsight(currentInsight.id, currentInsight.type); + props.setActive(''); + props.setActiveCategory(undefined); + } + }; return props.activeCategory ? ( <div className="w-3/4 p-4"> {props.adding ? ( - <AddItem category={props.activeCategory} /> - ) : ( - props.active && - props.activeCategory && ( - <div> - {props.activeCategory === 'report' ? ( - <ReportingForm activeTemplate={props.active} /> - ) : ( - <AlertingForm activeTemplate={props.active} /> - )} - </div> - ) - )} - <div className="flex justify-end mt-2"> - <Button label="Delete" variantType="secondary" variant="outline" /> - <Button label="Save" variantType="primary" className="ml-2" /> - </div> + <AddItem + category={props.activeCategory} + setAdding={props.setAdding} + setActive={props.setActive} + setActiveCategory={props.setActiveCategory} + /> + ) : currentInsight ? ( + <div> + {props.activeCategory === 'report' ? ( + <ReportingForm + insight={currentInsight} + setActiveCategory={props.setActiveCategory} + setActive={props.setActive} + handleDelete={handleDelete} + /> + ) : ( + <AlertingForm + insight={currentInsight} + setActiveCategory={props.setActiveCategory} + setActive={props.setActive} + handleDelete={handleDelete} + /> + )} + </div> + ) : null} </div> ) : ( - <StartScreen setAdding={props.setAdding} setActiveCategory={props.setActiveCategory} /> + <StartScreen + setAdding={props.setAdding} + setActiveCategory={props.setActiveCategory} + /> ); } diff --git a/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx b/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx index 11ea8d907..1870a1ffd 100644 --- a/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx +++ b/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx @@ -1,18 +1,135 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Accordion, AccordionItem, AccordionHead, AccordionBody } from '../../components/accordion'; import { TextEditor } from '../../components/textEditor'; +import { Insight, updateInsight, deleteInsight } from '../../data-access/store/insightSharingSlice'; +import { useAppDispatch, useSessionCache } from '../../data-access'; +import { Button, Input, LoadingSpinner } from '../../components'; import { EditorState } from 'lexical'; +import { createAlert, updateAlert, deleteAlert } from '../../data-access/api/apiService'; +import { MonitorType } from '../components/Sidebar'; type Props = { - activeTemplate: string; + insight: Insight; + setActiveCategory?: (val: MonitorType | undefined) => void; + setActive?: (val: string) => void; + handleDelete: () => void; }; export function AlertingForm(props: Props) { - const [editorState, setEditorState] = useState<EditorState | undefined>(undefined); + const dispatch = useAppDispatch(); + const session = useSessionCache(); + const [loading, setLoading] = useState(false); + const [name, setName] = useState(props.insight.name); + const [description, setDescription] = useState(props.insight.description); + const [recipients, setRecipients] = useState<string[]>(props.insight.recipients || []); + const [recipientInput, setRecipientInput] = useState<string>(''); + const [editorState, setEditorState] = useState<EditorState | null>(null); + + useEffect(() => { + setName(props.insight.name); + setDescription(props.insight.description); + setRecipientInput(props.insight.recipients ? props.insight.recipients.join(', ') : ''); + setRecipients(props.insight.recipients || []); + if (props.insight.template) { + try { + const parsedTemplate = JSON.parse(props.insight.template); + setEditorState(parsedTemplate); + } catch (e) { + setEditorState(null); + console.error('Failed to parse template:', e); + } + } else { + setEditorState(null); + } + }, [props.insight]); + + const handleSave = async () => { + if (!name || name.trim() === '') { + alert('Please enter a name for the alert.'); + return; + } + if (!description || description.trim() === '') { + alert('Please enter a description for the alert.'); + return; + } + if (!editorState) { + alert('Please enter content in the text editor before saving.'); + return; + } + + const alertData = { + id: props.insight.id, + name, + description, + recipients, + template: JSON.stringify(editorState), + save_state_id: session.currentSaveState || '', + type: 'alert' as const, + frequency: '' + }; + + setLoading(true); + try { + let savedAlert; + if (props.insight.id) { + savedAlert = await updateAlert(props.insight.id, alertData); + dispatch(updateInsight(savedAlert)); + } else { + savedAlert = await createAlert(alertData); + dispatch(updateInsight(savedAlert)); + } + console.log('Alert saved successfully'); + } catch (error) { + console.error('Failed to save alert:', error); + alert('Failed to save alert.'); + } finally { + setLoading(false); + } + }; + + const handleDelete = async () => { + if (!props.insight.id) { + alert('Cannot delete an alert without an ID.'); + return; + } + + setLoading(true); + try { + await deleteAlert(props.insight.id); + dispatch(deleteInsight({ id: props.insight.id, type: props.insight.type })); + if (props.setActive && props.setActiveCategory) { + props.setActive(''); + props.setActiveCategory(undefined); + } + } catch (error) { + console.error('Failed to delete alert:', error); + alert('Failed to delete alert.'); + } finally { + setLoading(false); + } + }; + + if (loading) { + return <LoadingSpinner />; + } return ( <div> - <span className="text-lg text-secondary-600 font-bold mb-4">Alert ID: {props.activeTemplate}</span> + <span className="text-lg text-secondary-600 font-bold mb-4">Alert ID: {props.insight.id}</span> + <Input + label="Name" + type="text" + value={name} + onChange={setName} + className="mb-4" + /> + <Input + label="Description" + type="text" + value={description} + onChange={setDescription} + className="mb-4" + /> <Accordion defaultOpenAll={true} className="border-t divide-y"> <AccordionItem className="pt-2 pb-4"> <AccordionHead showArrow={false}> @@ -20,7 +137,20 @@ export function AlertingForm(props: Props) { </AccordionHead> <AccordionBody> <div> - <input className="border" /> + <Input + type="text" + value={recipientInput} + onChange={(value) => { + setRecipientInput(String(value)); + const recipientList = String(value) + .split(/[, ]/) + .map(r => r.trim()) + .filter(r => r.length > 0); + setRecipients(recipientList); + }} + placeholder="Enter recipient(s)" + className="w-full" + /> </div> </AccordionBody> </AccordionItem> @@ -30,6 +160,7 @@ export function AlertingForm(props: Props) { </AccordionHead> <AccordionBody> <TextEditor + key={props.insight.id} editorState={editorState} setEditorState={setEditorState} showToolbar={true} @@ -38,6 +169,10 @@ export function AlertingForm(props: Props) { </AccordionBody> </AccordionItem> </Accordion> + <div className="flex justify-end mt-2"> + <Button label="Delete" variantType="secondary" variant="outline" onClick={handleDelete} /> + <Button label="Save" variantType="primary" className="ml-2" onClick={handleSave} /> + </div> </div> ); -} +} \ No newline at end of file diff --git a/libs/shared/lib/insight-sharing/components/AddItem.tsx b/libs/shared/lib/insight-sharing/components/AddItem.tsx index 771ab6bcc..78ce7c378 100644 --- a/libs/shared/lib/insight-sharing/components/AddItem.tsx +++ b/libs/shared/lib/insight-sharing/components/AddItem.tsx @@ -1,18 +1,71 @@ import React, { useState } from 'react'; -import { Input } from '../../components'; +import { Input, Button } from '../../components'; import { MonitorType } from './Sidebar'; +import { useAppDispatch, useSessionCache } from '../../data-access'; +import { addInsight } from '../../data-access/store/insightSharingSlice'; +import { createAlert, createReport } from '../../data-access/api/apiService'; +import { v4 as uuidv4 } from 'uuid'; type Props = { category: MonitorType; + setAdding: (val: boolean) => void; + setActive: (val: string) => void; + setActiveCategory: (val: MonitorType | undefined) => void; }; export function AddItem(props: Props) { - const [value, setValue] = useState<string>(''); + const [name, setName] = useState<string>(''); + const [description, setDescription] = useState<string>(''); + const [loading, setLoading] = useState(false); + const dispatch = useAppDispatch(); + const session = useSessionCache(); + + const handleSave = async () => { + if (!name.trim()) { + alert('Please enter a name.'); + return; + } + if (!description.trim()) { + alert('Please enter a description.'); + return; + } + + const newInsight = { + name, + description, + recipients: [], + template: '', + frequency: props.category === 'report' ? 'Daily' : undefined, + save_state_id: session.currentSaveState, + type: props.category, + }; + setLoading(true); + try { + let savedInsight; + if (props.category === 'report') { + savedInsight = await createReport(newInsight); + } else { + savedInsight = await createAlert(newInsight) + } + + dispatch(addInsight(savedInsight)); + props.setActive(savedInsight.id); + props.setActiveCategory(props.category); + props.setAdding(false); + } catch (error) { + console.error('Failed to create insight:', error); + alert('Failed to create new ' + props.category); + } finally { + setLoading(false); + } + }; return ( <div> <span className="text-lg text-secondary-600 font-bold mb-4">Add a new {props.category}ing service</span> - <Input type="text" label="Name" value={value} onChange={setValue} /> + <Input type="text" label="Name" value={name} onChange={setName} className="mb-2" /> + <Input type="text" label="Description" value={description} onChange={setDescription} className="mb-2" /> + <Button label="Save" onClick={handleSave} disabled={!name || !description} className="mt-2" /> </div> ); } diff --git a/libs/shared/lib/insight-sharing/components/Sidebar.tsx b/libs/shared/lib/insight-sharing/components/Sidebar.tsx index d8ebcfe1c..d4f5c15f6 100644 --- a/libs/shared/lib/insight-sharing/components/Sidebar.tsx +++ b/libs/shared/lib/insight-sharing/components/Sidebar.tsx @@ -1,18 +1,41 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Button } from '../../components'; import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '../../components/accordion'; +import { useAppDispatch, useAppSelector } from '../../data-access'; +import { setInsights } from '../../data-access/store/insightSharingSlice'; +import { getReports } from '../../data-access/api/apiService'; +import { useSessionCache } from '../../data-access'; export type MonitorType = 'report' | 'alert'; type SidebarProps = { - reports: string[]; - alerts: string[]; changeActive: (category: MonitorType, val: string) => void; setAdding: (val: boolean) => void; setActiveCategory: (val: MonitorType | undefined) => void; }; export function Sidebar(props: SidebarProps) { + const dispatch = useAppDispatch(); + const session = useSessionCache(); + const reports = useAppSelector((state) => state.insightSharing.reports); + const alerts = useAppSelector((state) => state.insightSharing.alerts); + + useEffect(() => { + async function fetchReports() { + try { + if (session.currentSaveState && session.currentSaveState !== '') { + const fetchedReports = await getReports(session.currentSaveState); + dispatch(setInsights(fetchedReports)); + } + } catch (error) { + console.error('Failed to fetch reports:', error); + } + } + + fetchReports(); + }, [session.currentSaveState, dispatch]); + + return ( <div className="w-1/4 border-r overflow-auto flex flex-col h-full"> <span className="text-lg text-secondary-700 font-semibold px-2 py-4">Insight Sharing</span> @@ -41,16 +64,16 @@ export function Sidebar(props: SidebarProps) { </AccordionHead> <AccordionBody className="ml-0"> <ul className="space-y-2"> - {props.reports.map((name, index) => ( + {reports.map((report) => ( <li - key={index} + key={report.id} className="cursor-pointer p-2 hover:bg-secondary-50" onClick={() => { - props.changeActive('report', name); + props.changeActive('report', report.id); props.setAdding(false); }} > - {name} + {report.name} </li> ))} </ul> @@ -80,16 +103,16 @@ export function Sidebar(props: SidebarProps) { </AccordionHead> <AccordionBody className="ml-0"> <ul className="space-y-2"> - {props.alerts.map((name, index) => ( + {alerts.map((alert) => ( <li - key={index} + key={alert.id} className="cursor-pointer p-2 hover:bg-secondary-50" onClick={() => { - props.changeActive('alert', name); + props.changeActive('alert', alert.id); props.setAdding(false); }} > - {name} + {alert.name} </li> ))} </ul> diff --git a/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx b/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx index 9b2645b91..ec1284d4a 100644 --- a/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx +++ b/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx @@ -1,57 +1,203 @@ -import React, { useState } from 'react'; -import { Input, LoadingSpinner } from '../../components'; +import React, { useState, useEffect } from 'react'; +import { Button, Input, LoadingSpinner } from '../../components'; import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '../../components/accordion'; import { TextEditor } from '../../components/textEditor'; +import { Insight, updateInsight, deleteInsight } from '../../data-access/store/insightSharingSlice'; +import { useAppDispatch, useSessionCache } from '../../data-access'; +import { createReport, updateReport, deleteReport } from '../../data-access/api/apiService'; +import { MonitorType } from '../components/Sidebar'; import { EditorState } from 'lexical'; type Props = { - activeTemplate: string; + insight: Insight; + setActiveCategory: (val: MonitorType | undefined) => void; + setActive: (val: string) => void; + handleDelete: () => void; }; export function ReportingForm(props: Props) { + const dispatch = useAppDispatch(); + const session = useSessionCache(); const [loading, setLoading] = useState(false); - const [editorState, setEditorState] = useState<EditorState | undefined>(undefined); + const [name, setName] = useState(props.insight.name); + const [description, setDescription] = useState(props.insight.description); + const [recipients, setRecipients] = useState(props.insight.recipients || []); + const [recipientInput, setRecipientInput] = useState<string>(''); + const [frequency, setFrequency] = useState(props.insight.frequency || 'Daily'); + const [editorState, setEditorState] = useState<EditorState | null>(null); + + useEffect(() => { + setName(props.insight.name); + setDescription(props.insight.description); + setRecipientInput(props.insight.recipients ? props.insight.recipients.join(', ') : ''); + setRecipients(props.insight.recipients || []); + setFrequency(props.insight.frequency || 'Daily'); + if (props.insight.template) { + try { + const parsedTemplate = JSON.parse(props.insight.template); + setEditorState(parsedTemplate); + } catch (e) { + setEditorState(null); + console.error('Failed to parse template:', e); + } + } else { + setEditorState(null); + } + }, [props.insight]); + + const handleSetFrequency = (value: string | number) => { + setFrequency(value as string); + }; + + const handleSetName = (value: string | number) => { + setName(value as string); + }; + + const handleSetDescription = (value: string | number) => { + setDescription(value as string); + }; + + const handleSave = async () => { + if (!name || name.trim() === '') { + alert('Please enter a name for the report.'); + return; + } + if (!description || description.trim() === '') { + alert('Please enter a description for the report.'); + return; + } + if (!editorState) { + alert('Please enter content in the text editor before saving.'); + return; + } + + const reportData: Partial<Insight> = { + id: props.insight.id, + name, + description, + recipients, + frequency, + template: JSON.stringify(editorState), // Convert editorState to string + save_state_id: session.currentSaveState, + type: 'report', + }; + + setLoading(true); + try { + let savedReport; + if (props.insight.id) { + savedReport = await updateReport(props.insight.id, reportData); + dispatch(updateInsight(savedReport)); + } else { + savedReport = await createReport(reportData); + dispatch(updateInsight(savedReport)); + } + console.log('Report saved successfully'); + } catch (error) { + console.error('Failed to save report:', error); + alert('Failed to save report.'); + } finally { + setLoading(false); + } + }; + + const handleDelete = async () => { + if (!props.insight.id) { + alert('Cannot delete a report without an ID.'); + return; + } + + setLoading(true); + try { + await deleteReport(props.insight.id); + dispatch(deleteInsight({ id: props.insight.id, type: props.insight.type })); + props.setActive(''); + props.setActiveCategory(undefined); + } catch (error) { + console.error('Failed to delete report:', error); + alert('Failed to delete report.'); + } finally { + setLoading(false); + } + }; return loading ? ( <LoadingSpinner /> ) : ( - props.activeTemplate && ( - <div> - <span className="text-lg text-secondary-600 font-bold mb-4">Report ID: {props.activeTemplate}</span> - <Accordion defaultOpenAll={true} className="border-t divide-y"> - <AccordionItem className="pt-2 pb-4"> - <AccordionHead showArrow={false}> - <span className="font-semibold">Recipient(s)</span> - </AccordionHead> - <AccordionBody> - <div> - <input className="border" /> - </div> - </AccordionBody> - </AccordionItem> - <AccordionItem className="pt-2 pb-4"> - <AccordionHead showArrow={false}> - <span className="font-semibold">Repeat</span> - </AccordionHead> - <AccordionBody> - <Input label="Frequency" type="dropdown" value={'Daily'} onChange={() => {}} options={['Daily', 'Weekly']} className="mb-1" /> - </AccordionBody> - </AccordionItem> - <AccordionItem className="pt-2 pb-4"> - <AccordionHead showArrow={false}> - <span className="font-semibold">Email template</span> - </AccordionHead> - <AccordionBody> - <TextEditor - editorState={editorState} - setEditorState={setEditorState} - showToolbar={true} - placeholder="Start typing your report template..." + <div> + <span className="text-lg text-secondary-600 font-bold mb-4">Report ID: {props.insight.id}</span> + <Input + label="Name" + type="text" + value={name} + onChange={handleSetName} + className="mb-4" + /> + <Input + label="Description" + type="text" + value={description} + onChange={handleSetDescription} + className="mb-4" + /> + <Accordion defaultOpenAll={true} className="border-t divide-y"> + <AccordionItem className="pt-2 pb-4"> + <AccordionHead showArrow={false}> + <span className="font-semibold">Recipient(s)</span> + </AccordionHead> + <AccordionBody> + <div> + <Input + type="text" + value={recipientInput} + onChange={(value) => { + setRecipientInput(String(value)); + const recipientList = String(value) + .split(/[, ]/) + .map(r => r.trim()) + .filter(r => r.length > 0); + setRecipients(recipientList); + }} + placeholder="Enter recipient(s)" + className="w-full" /> - </AccordionBody> - </AccordionItem> - </Accordion> + </div> + </AccordionBody> + </AccordionItem> + <AccordionItem className="pt-2 pb-4"> + <AccordionHead showArrow={false}> + <span className="font-semibold">Repeat</span> + </AccordionHead> + <AccordionBody> + <Input + label="Frequency" + type="dropdown" + value={frequency} + onChange={handleSetFrequency} + options={['Daily', 'Weekly']} + className="mb-1" + /> + </AccordionBody> + </AccordionItem> + <AccordionItem className="pt-2 pb-4"> + <AccordionHead showArrow={false}> + <span className="font-semibold">Email Template</span> + </AccordionHead> + <AccordionBody> + <TextEditor + key={props.insight.id} + editorState={editorState} + setEditorState={setEditorState} + showToolbar={true} + placeholder="Start typing your report template..." + /> + </AccordionBody> + </AccordionItem> + </Accordion> + <div className="flex justify-end mt-2"> + <Button label="Delete" variantType="secondary" variant="outline" onClick={handleDelete} /> + <Button label="Save" variantType="primary" className="ml-2" onClick={handleSave} /> </div> - ) + </div> ); } -- GitLab From e0156142babef079e01249df6189e178297fab4d Mon Sep 17 00:00:00 2001 From: Samed <sbalcioglu@graphpolaris.com> Date: Mon, 2 Dec 2024 11:48:21 +0100 Subject: [PATCH 2/9] feat: updated reportingform --- libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx b/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx index ec1284d4a..b73983f51 100644 --- a/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx +++ b/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx @@ -77,7 +77,7 @@ export function ReportingForm(props: Props) { description, recipients, frequency, - template: JSON.stringify(editorState), // Convert editorState to string + template: JSON.stringify(editorState), save_state_id: session.currentSaveState, type: 'report', }; -- GitLab From 5541f407485ebbe57f399fa2640e6c2f194014ab Mon Sep 17 00:00:00 2001 From: Samed <sbalcioglu@graphpolaris.com> Date: Tue, 3 Dec 2024 12:56:48 +0100 Subject: [PATCH 3/9] chore: updates --- libs/shared/lib/data-access/api/apiService.ts | 106 ------------------ libs/shared/lib/data-access/broker/types.ts | 2 +- .../data-access/broker/wsInsightSharing.ts | 21 ++-- .../data-access/store/insightSharingSlice.ts | 31 ++--- .../lib/insight-sharing/SettingsPanel.tsx | 4 +- .../insight-sharing/alerting/AlertingForm.tsx | 23 ++-- .../insight-sharing/components/AddItem.tsx | 37 +++--- .../insight-sharing/components/Sidebar.tsx | 18 +-- .../reporting/ReportingForm.tsx | 75 +++++++------ 9 files changed, 114 insertions(+), 203 deletions(-) delete mode 100644 libs/shared/lib/data-access/api/apiService.ts diff --git a/libs/shared/lib/data-access/api/apiService.ts b/libs/shared/lib/data-access/api/apiService.ts deleted file mode 100644 index a9ea11cfe..000000000 --- a/libs/shared/lib/data-access/api/apiService.ts +++ /dev/null @@ -1,106 +0,0 @@ -import axios from 'axios'; -import { Insight } from '../store/insightSharingSlice'; - -const API_BASE_URL = 'http://localhost:8000/api'; - -const axiosInstance = axios.create({ - baseURL: API_BASE_URL, - headers: { - 'Content-Type': 'application/json', - } - }); - - export async function createReport(reportData: Partial<Insight>) { - console.log('Creating report with data:', reportData); - try { - const dataToSend = { ...reportData }; - delete dataToSend.id; - delete dataToSend.type; - - const response = await axiosInstance.post('/report/', dataToSend); - console.log('Report creation response:', response); - - const report = { ...response.data, type: 'report' }; - - return report; - } catch (error) { - console.error('Error creating report:', error); - if (axios.isAxiosError(error)) { - console.error('Response data:', error.response?.data); - console.error('Request URL:', error.config?.url); - } - throw error; - } - } - - export async function getReports(saveStateId: string) { - console.log('Fetching reports for saveStateId:', saveStateId); - try { - const response = await axiosInstance.get(`/report/save_state/${saveStateId}/`); - console.log('Reports fetch response:', response); - const reports = response.data.map((report: Insight) => ({ - ...report, - type: 'report', - })); - - return reports; - } catch (error) { - console.error('Error fetching reports:', error); - if (axios.isAxiosError(error)) { - console.error('Response data:', error.response?.data); - console.error('Request URL:', error.config?.url); - } - return []; - } - } - - export async function updateReport(reportId: string, reportData: Partial<Insight>) { - try { - console.log('Updating report with data:', reportData); - const response = await axiosInstance.put(`/report/${reportId}/`, reportData); - return response.data; - } catch (error) { - console.error('Error updating report:', error); - throw error; - } - } - - export async function deleteReport(reportId: string) { - try { - const response = await axiosInstance.delete(`report/${reportId}/`); - return response.data; - } catch (error) { - console.error('Error deleting report:', error); - throw error; - } - } - - export async function createAlert(alertData: any) { - try { - const response = await axiosInstance.post('alert/', alertData); - return response.data; - } catch (error) { - console.error('Error creating alert:', error); - throw error; - } - } - - export async function updateAlert(alertId: string, alertData: any) { - try { - const response = await axiosInstance.put(`alert/${alertId}`, alertData); - return response.data; - } catch (error) { - console.error('Error updating alert:', error); - throw error; - } - } - - export async function deleteAlert(alertId: string) { - try { - const response = await axiosInstance.delete(`alert/${alertId}`); - return response.data; - } catch (error) { - console.error('Error deleting alert:', error); - throw error; - } - } diff --git a/libs/shared/lib/data-access/broker/types.ts b/libs/shared/lib/data-access/broker/types.ts index 462762e18..8d6decc5a 100644 --- a/libs/shared/lib/data-access/broker/types.ts +++ b/libs/shared/lib/data-access/broker/types.ts @@ -19,7 +19,7 @@ type QueryOrchestratorMessage = { queryID: string; }; -export type keyTypeI = 'broadcastState' | 'dbConnection' | 'schema' | 'query' | 'state' | 'user' | 'insight'| 'report' | 'alert'; +export type keyTypeI = 'broadcastState' | 'dbConnection' | 'schema' | 'query' | 'state' | 'user' | 'insight'; export type subKeyTypeI = // Crud | 'create' diff --git a/libs/shared/lib/data-access/broker/wsInsightSharing.ts b/libs/shared/lib/data-access/broker/wsInsightSharing.ts index 98f18b7ec..b391d49f1 100644 --- a/libs/shared/lib/data-access/broker/wsInsightSharing.ts +++ b/libs/shared/lib/data-access/broker/wsInsightSharing.ts @@ -1,7 +1,7 @@ import { Broker } from './broker'; -import { Insight, InsightType } from '../store/insightSharingSlice'; +import { InsightRequest, InsightResponse, InsightType } from '../store/insightSharingSlice'; -type GetInsightsResponse = (data: { reports: Insight[]; alerts: Insight[] }, status: string) => void; +type GetInsightsResponse = (data: { reports: InsightResponse[]; alerts: InsightResponse[] }, status: string) => void; export function wsGetInsights(callback?: GetInsightsResponse) { const internalCallback: GetInsightsResponse = (data, status) => { if (callback) callback(data, status); @@ -16,15 +16,16 @@ export function wsGetInsights(callback?: GetInsightsResponse) { ); } -export function wsCreateInsight(insight: Insight, callback?: Function) { +export function wsCreateInsight(insight: InsightRequest, callback?: Function) { const messageKey = insight.type === 'report' ? 'report' : 'alert'; Broker.instance().sendMessage( { - key: messageKey, + key: 'insight', subKey: 'create', - body: insight, + body: JSON.stringify(insight), }, (data: any, status: string) => { + debugger; if (status === 'Bad Request') { console.error('Failed to create insight:', data); } @@ -33,13 +34,13 @@ export function wsCreateInsight(insight: Insight, callback?: Function) { ); } -export function wsUpdateInsight(insight: Insight, callback?: Function) { +export function wsUpdateInsight(insight: InsightRequest, callback?: Function) { const messageKey = insight.type === 'report' ? 'report' : 'alert'; Broker.instance().sendMessage( { - key: messageKey, + key: 'insight', subKey: 'update', - body: insight, + body: JSON.stringify(insight), }, (data: any, status: string) => { if (status === 'Bad Request') { @@ -54,9 +55,9 @@ export function wsDeleteInsight(id: string, type: InsightType, callback?: Functi const messageKey = type === 'report' ? 'report' : 'alert'; Broker.instance().sendMessage( { - key: messageKey, + key: 'insight', subKey: 'delete', - body: { id }, + body: JSON.stringify({ id, type }), }, (data: any, status: string) => { if (status === 'Bad Request') { diff --git a/libs/shared/lib/data-access/store/insightSharingSlice.ts b/libs/shared/lib/data-access/store/insightSharingSlice.ts index c2859b576..2c06055bd 100644 --- a/libs/shared/lib/data-access/store/insightSharingSlice.ts +++ b/libs/shared/lib/data-access/store/insightSharingSlice.ts @@ -3,23 +3,26 @@ import { RootState } from '../store'; export type InsightType = 'report' | 'alert'; -export interface Insight { - id: string; +export type InsightRequest = { name: string; description: string; recipients: string[]; frequency: string; template: string; - save_state_id: string; - createdAt: string; - updatedAt: string; + saveStateId: string; type: 'report' | 'alert'; } -interface InsightState { - insights: Insight[]; - reports: Insight[]; - alerts: Insight[]; +export type InsightResponse = { + id: string; + createdAt: string; + updatedAt: string; +} & InsightRequest; + +type InsightState = { + insights: InsightResponse[]; + reports: InsightResponse[]; + alerts: InsightResponse[]; } const initialState: InsightState = { @@ -32,19 +35,19 @@ const insightSharingSlice = createSlice({ name: 'insightSharing', initialState, reducers: { - setInsights(state, action: PayloadAction<Insight[] | { reports: Insight[] }>) { + setInsights(state, action: PayloadAction<InsightResponse[] | { reports: InsightResponse[] }>) { const insights = Array.isArray(action.payload) ? action.payload : action.payload.reports; state.reports = insights.filter((insight) => insight.type === 'report'); state.alerts = insights.filter((insight) => insight.type === 'alert'); }, - addInsight(state, action: PayloadAction<Insight>) { + addInsight(state, action: PayloadAction<InsightResponse>) { if (action.payload.type === 'report') { state.reports.push(action.payload); } else { state.alerts.push(action.payload); } }, - updateInsight(state, action: PayloadAction<Insight>) { + updateInsight(state, action: PayloadAction<InsightResponse>) { const insights = action.payload.type === 'report' ? state.reports : state.alerts; const index = insights.findIndex((i) => i.id === action.payload.id); if (index !== -1) { @@ -64,7 +67,7 @@ const insightSharingSlice = createSlice({ }); export const { setInsights, addInsight, updateInsight, deleteInsight } = insightSharingSlice.actions; -export const selectReports = (state: RootState): Insight[] => state.insightSharing.reports; -export const selectAlerts = (state: RootState): Insight[] => state.insightSharing.alerts; +export const selectReports = (state: RootState): InsightResponse[] => state.insightSharing.reports; +export const selectAlerts = (state: RootState): InsightResponse[] => state.insightSharing.alerts; export default insightSharingSlice.reducer; diff --git a/libs/shared/lib/insight-sharing/SettingsPanel.tsx b/libs/shared/lib/insight-sharing/SettingsPanel.tsx index 54c65d266..ba6d02f74 100644 --- a/libs/shared/lib/insight-sharing/SettingsPanel.tsx +++ b/libs/shared/lib/insight-sharing/SettingsPanel.tsx @@ -9,7 +9,7 @@ import { deleteInsight, selectReports, selectAlerts, - Insight, + InsightResponse, } from '../data-access/store/insightSharingSlice'; import { wsDeleteInsight } from '../data-access/broker/wsInsightSharing'; @@ -27,7 +27,7 @@ export function SettingsPanel(props: Props) { const reports = useAppSelector(selectReports); const alerts = useAppSelector(selectAlerts); - const [currentInsight, setCurrentInsight] = useState<Insight | null>(null); + const [currentInsight, setCurrentInsight] = useState<InsightResponse | null>(null); useEffect(() => { if (props.activeCategory && props.active) { diff --git a/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx b/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx index 1870a1ffd..64ea13c85 100644 --- a/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx +++ b/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx @@ -1,15 +1,14 @@ import React, { useEffect, useState } from 'react'; import { Accordion, AccordionItem, AccordionHead, AccordionBody } from '../../components/accordion'; import { TextEditor } from '../../components/textEditor'; -import { Insight, updateInsight, deleteInsight } from '../../data-access/store/insightSharingSlice'; +import { InsightResponse, updateInsight, deleteInsight } from '../../data-access/store/insightSharingSlice'; import { useAppDispatch, useSessionCache } from '../../data-access'; import { Button, Input, LoadingSpinner } from '../../components'; import { EditorState } from 'lexical'; -import { createAlert, updateAlert, deleteAlert } from '../../data-access/api/apiService'; import { MonitorType } from '../components/Sidebar'; type Props = { - insight: Insight; + insight: InsightResponse; setActiveCategory?: (val: MonitorType | undefined) => void; setActive?: (val: string) => void; handleDelete: () => void; @@ -71,13 +70,14 @@ export function AlertingForm(props: Props) { setLoading(true); try { let savedAlert; - if (props.insight.id) { - savedAlert = await updateAlert(props.insight.id, alertData); - dispatch(updateInsight(savedAlert)); - } else { - savedAlert = await createAlert(alertData); - dispatch(updateInsight(savedAlert)); - } + debugger; + // if (props.insight.id) { + // savedAlert = await updateAlert(props.insight.id, alertData); + // dispatch(updateInsight(savedAlert)); + // } else { + // savedAlert = await createAlert(alertData); + // dispatch(updateInsight(savedAlert)); + // } console.log('Alert saved successfully'); } catch (error) { console.error('Failed to save alert:', error); @@ -95,7 +95,8 @@ export function AlertingForm(props: Props) { setLoading(true); try { - await deleteAlert(props.insight.id); + debugger; + // await deleteAlert(props.insight.id); dispatch(deleteInsight({ id: props.insight.id, type: props.insight.type })); if (props.setActive && props.setActiveCategory) { props.setActive(''); diff --git a/libs/shared/lib/insight-sharing/components/AddItem.tsx b/libs/shared/lib/insight-sharing/components/AddItem.tsx index 78ce7c378..6db91c0d8 100644 --- a/libs/shared/lib/insight-sharing/components/AddItem.tsx +++ b/libs/shared/lib/insight-sharing/components/AddItem.tsx @@ -3,7 +3,6 @@ import { Input, Button } from '../../components'; import { MonitorType } from './Sidebar'; import { useAppDispatch, useSessionCache } from '../../data-access'; import { addInsight } from '../../data-access/store/insightSharingSlice'; -import { createAlert, createReport } from '../../data-access/api/apiService'; import { v4 as uuidv4 } from 'uuid'; type Props = { @@ -40,24 +39,26 @@ export function AddItem(props: Props) { type: props.category, }; setLoading(true); - try { - let savedInsight; - if (props.category === 'report') { - savedInsight = await createReport(newInsight); - } else { - savedInsight = await createAlert(newInsight) - } + + debugger; + // try { + // let savedInsight; + // if (props.category === 'report') { + // savedInsight = await createInsih(newInsight); + // } else { + // savedInsight = await createAlert(newInsight) + // } - dispatch(addInsight(savedInsight)); - props.setActive(savedInsight.id); - props.setActiveCategory(props.category); - props.setAdding(false); - } catch (error) { - console.error('Failed to create insight:', error); - alert('Failed to create new ' + props.category); - } finally { - setLoading(false); - } + // dispatch(addInsight(savedInsight)); + // props.setActive(savedInsight.id); + // props.setActiveCategory(props.category); + // props.setAdding(false); + // } catch (error) { + // console.error('Failed to create insight:', error); + // alert('Failed to create new ' + props.category); + // } finally { + // setLoading(false); + // } }; return ( diff --git a/libs/shared/lib/insight-sharing/components/Sidebar.tsx b/libs/shared/lib/insight-sharing/components/Sidebar.tsx index d4f5c15f6..059bbde9e 100644 --- a/libs/shared/lib/insight-sharing/components/Sidebar.tsx +++ b/libs/shared/lib/insight-sharing/components/Sidebar.tsx @@ -3,7 +3,6 @@ import { Button } from '../../components'; import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '../../components/accordion'; import { useAppDispatch, useAppSelector } from '../../data-access'; import { setInsights } from '../../data-access/store/insightSharingSlice'; -import { getReports } from '../../data-access/api/apiService'; import { useSessionCache } from '../../data-access'; export type MonitorType = 'report' | 'alert'; @@ -22,14 +21,15 @@ export function Sidebar(props: SidebarProps) { useEffect(() => { async function fetchReports() { - try { - if (session.currentSaveState && session.currentSaveState !== '') { - const fetchedReports = await getReports(session.currentSaveState); - dispatch(setInsights(fetchedReports)); - } - } catch (error) { - console.error('Failed to fetch reports:', error); - } + debugger; + // try { + // if (session.currentSaveState && session.currentSaveState !== '') { + // const fetchedReports = await getReports(session.currentSaveState); + // dispatch(setInsights(fetchedReports)); + // } + // } catch (error) { + // console.error('Failed to fetch reports:', error); + // } } fetchReports(); diff --git a/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx b/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx index b73983f51..e3df37f75 100644 --- a/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx +++ b/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx @@ -2,14 +2,14 @@ import React, { useState, useEffect } from 'react'; import { Button, Input, LoadingSpinner } from '../../components'; import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '../../components/accordion'; import { TextEditor } from '../../components/textEditor'; -import { Insight, updateInsight, deleteInsight } from '../../data-access/store/insightSharingSlice'; +import { InsightResponse, updateInsight, deleteInsight, InsightRequest } from '../../data-access/store/insightSharingSlice'; import { useAppDispatch, useSessionCache } from '../../data-access'; -import { createReport, updateReport, deleteReport } from '../../data-access/api/apiService'; import { MonitorType } from '../components/Sidebar'; import { EditorState } from 'lexical'; +import { wsCreateInsight, wsDeleteInsight, wsUpdateInsight } from '../../data-access/broker/wsInsightSharing'; type Props = { - insight: Insight; + insight: InsightResponse; setActiveCategory: (val: MonitorType | undefined) => void; setActive: (val: string) => void; handleDelete: () => void; @@ -70,35 +70,45 @@ export function ReportingForm(props: Props) { alert('Please enter content in the text editor before saving.'); return; } + if(!session.currentSaveState){ + console.warn("No active save state found - this might affect report creation/updating"); + } - const reportData: Partial<Insight> = { - id: props.insight.id, + const reportData: InsightRequest = { name, description, recipients, frequency, template: JSON.stringify(editorState), - save_state_id: session.currentSaveState, + saveStateId: session.currentSaveState || "", type: 'report', }; setLoading(true); - try { - let savedReport; - if (props.insight.id) { - savedReport = await updateReport(props.insight.id, reportData); - dispatch(updateInsight(savedReport)); - } else { - savedReport = await createReport(reportData); - dispatch(updateInsight(savedReport)); - } - console.log('Report saved successfully'); - } catch (error) { - console.error('Failed to save report:', error); - alert('Failed to save report.'); - } finally { - setLoading(false); - } + // if (props.insight.id) { + // wsUpdateInsight(reportData, (data: any, status: string) => { + // setLoading(false); + // if (status === 'success') { + // dispatch(updateInsight(data)); + // console.log('Report updated successfully'); + // } else { + // console.error('Failed to update report:', data); + // alert('Failed to update report.'); + // } + // }); + // } else { + wsCreateInsight(reportData, (data: any, status: string) => { + debugger; + setLoading(false); + if (status === 'success') { + dispatch(updateInsight(data)); + console.log('Report created successfully'); + } else { + console.error('Failed to create report:', data); + alert('Failed to create report.'); + } + }); + // } }; const handleDelete = async () => { @@ -108,17 +118,18 @@ export function ReportingForm(props: Props) { } setLoading(true); - try { - await deleteReport(props.insight.id); - dispatch(deleteInsight({ id: props.insight.id, type: props.insight.type })); - props.setActive(''); - props.setActiveCategory(undefined); - } catch (error) { - console.error('Failed to delete report:', error); - alert('Failed to delete report.'); - } finally { + wsDeleteInsight(props.insight.id, props.insight.type, (data: any, status: string) => { setLoading(false); - } + if (status === 'success') { + dispatch(deleteInsight({ id: props.insight.id, type: props.insight.type })); + props.setActive(''); + props.setActiveCategory(undefined); + console.log('Report deleted successfully'); + } else { + console.error('Failed to delete report:', data); + alert('Failed to delete report.'); + } + }); }; return loading ? ( -- GitLab From 99b7c11163e6e2fb5fffb0fc0bb7ea71e0684eaa Mon Sep 17 00:00:00 2001 From: Samed <sbalcioglu@graphpolaris.com> Date: Tue, 3 Dec 2024 16:25:11 +0100 Subject: [PATCH 4/9] feat: frontend receives from ws --- libs/shared/lib/data-access/broker/broker.tsx | 6 +- .../data-access/broker/wsInsightSharing.ts | 33 ++++++-- .../data-access/store/insightSharingSlice.ts | 17 ++++- .../insight-sharing/alerting/AlertingForm.tsx | 75 ++++++++++--------- .../insight-sharing/components/AddItem.tsx | 41 ++++------ .../insight-sharing/components/Sidebar.tsx | 15 ++-- .../reporting/ReportingForm.tsx | 37 +++++---- 7 files changed, 123 insertions(+), 101 deletions(-) diff --git a/libs/shared/lib/data-access/broker/broker.tsx b/libs/shared/lib/data-access/broker/broker.tsx index ff09fb28a..41cfe6f8e 100644 --- a/libs/shared/lib/data-access/broker/broker.tsx +++ b/libs/shared/lib/data-access/broker/broker.tsx @@ -200,9 +200,9 @@ export class Broker { } fullMessage.sessionID = this.authHeader?.sessionID ?? ''; - //if (message.body && typeof message.body !== 'string') { - // fullMessage.body = JSON.stringify(message.body); - //} + if (message.body && typeof message.body !== 'string') { + fullMessage.body = JSON.stringify(message.body); + } if (this.webSocket && this.connected && this.webSocket.readyState === 1) this.webSocket.send(JSON.stringify(fullMessage)); else diff --git a/libs/shared/lib/data-access/broker/wsInsightSharing.ts b/libs/shared/lib/data-access/broker/wsInsightSharing.ts index b391d49f1..b9432157e 100644 --- a/libs/shared/lib/data-access/broker/wsInsightSharing.ts +++ b/libs/shared/lib/data-access/broker/wsInsightSharing.ts @@ -2,7 +2,7 @@ import { Broker } from './broker'; import { InsightRequest, InsightResponse, InsightType } from '../store/insightSharingSlice'; type GetInsightsResponse = (data: { reports: InsightResponse[]; alerts: InsightResponse[] }, status: string) => void; -export function wsGetInsights(callback?: GetInsightsResponse) { +export function wsGetInsights(saveStateId: string, callback?: GetInsightsResponse) { const internalCallback: GetInsightsResponse = (data, status) => { if (callback) callback(data, status); }; @@ -11,13 +11,13 @@ export function wsGetInsights(callback?: GetInsightsResponse) { { key: 'insight', subKey: 'getAll', + body: JSON.stringify({saveStateId: saveStateId}) }, internalCallback ); } export function wsCreateInsight(insight: InsightRequest, callback?: Function) { - const messageKey = insight.type === 'report' ? 'report' : 'alert'; Broker.instance().sendMessage( { key: 'insight', @@ -25,22 +25,39 @@ export function wsCreateInsight(insight: InsightRequest, callback?: Function) { body: JSON.stringify(insight), }, (data: any, status: string) => { - debugger; if (status === 'Bad Request') { console.error('Failed to create insight:', data); - } if (callback) callback(data, status); + return; + } + + if (!data || typeof data !== 'object') { + console.error('Invalid repsonse data', data) + if (callback) callback(null, 'error'); + return; + } + + if (!data.type || !data.id) { + console.error('Missing fields in response', data) + if (callback) callback(null, 'error'); + return; + } + + if (callback) callback(data,status); } ); } -export function wsUpdateInsight(insight: InsightRequest, callback?: Function) { - const messageKey = insight.type === 'report' ? 'report' : 'alert'; +export function wsUpdateInsight( + id: string, + insight: InsightRequest, + callback?: (data: any, status: string) => void +) { Broker.instance().sendMessage( { key: 'insight', subKey: 'update', - body: JSON.stringify(insight), + body: JSON.stringify({id: id, insight: insight}), }, (data: any, status: string) => { if (status === 'Bad Request') { @@ -52,7 +69,7 @@ export function wsUpdateInsight(insight: InsightRequest, callback?: Function) { } export function wsDeleteInsight(id: string, type: InsightType, callback?: Function) { - const messageKey = type === 'report' ? 'report' : 'alert'; + Broker.instance().sendMessage( { key: 'insight', diff --git a/libs/shared/lib/data-access/store/insightSharingSlice.ts b/libs/shared/lib/data-access/store/insightSharingSlice.ts index 2c06055bd..d9485069d 100644 --- a/libs/shared/lib/data-access/store/insightSharingSlice.ts +++ b/libs/shared/lib/data-access/store/insightSharingSlice.ts @@ -40,10 +40,21 @@ const insightSharingSlice = createSlice({ state.reports = insights.filter((insight) => insight.type === 'report'); state.alerts = insights.filter((insight) => insight.type === 'alert'); }, - addInsight(state, action: PayloadAction<InsightResponse>) { - if (action.payload.type === 'report') { + addInsight: (state, action: PayloadAction<InsightResponse>) => { + if (!action.payload) { + console.error('Received null payload in addInsight'); + return; + } + + const type = action.payload.type; + if (!type) { + console.error('Insight type is missing in payload', action.payload); + return; + } + + if (type === 'report') { state.reports.push(action.payload); - } else { + } else if (type === 'alert') { state.alerts.push(action.payload); } }, diff --git a/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx b/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx index 64ea13c85..0b53289b6 100644 --- a/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx +++ b/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx @@ -1,11 +1,12 @@ import React, { useEffect, useState } from 'react'; import { Accordion, AccordionItem, AccordionHead, AccordionBody } from '../../components/accordion'; import { TextEditor } from '../../components/textEditor'; -import { InsightResponse, updateInsight, deleteInsight } from '../../data-access/store/insightSharingSlice'; +import { InsightResponse, updateInsight, deleteInsight, addInsight, InsightRequest } from '../../data-access/store/insightSharingSlice'; import { useAppDispatch, useSessionCache } from '../../data-access'; import { Button, Input, LoadingSpinner } from '../../components'; import { EditorState } from 'lexical'; import { MonitorType } from '../components/Sidebar'; +import { wsUpdateInsight, wsCreateInsight, wsDeleteInsight } from '../../data-access/broker/wsInsightSharing'; type Props = { insight: InsightResponse; @@ -56,34 +57,42 @@ export function AlertingForm(props: Props) { return; } - const alertData = { - id: props.insight.id, + const alertData : InsightRequest = { name, description, recipients, template: JSON.stringify(editorState), - save_state_id: session.currentSaveState || '', + saveStateId: session.currentSaveState || '', type: 'alert' as const, frequency: '' }; setLoading(true); - try { - let savedAlert; - debugger; - // if (props.insight.id) { - // savedAlert = await updateAlert(props.insight.id, alertData); - // dispatch(updateInsight(savedAlert)); - // } else { - // savedAlert = await createAlert(alertData); - // dispatch(updateInsight(savedAlert)); - // } - console.log('Alert saved successfully'); - } catch (error) { - console.error('Failed to save alert:', error); - alert('Failed to save alert.'); - } finally { - setLoading(false); + + if (props.insight.id) { + wsUpdateInsight(props.insight.id, alertData, (data: any, status: string) => { + setLoading(false); + if (status === 'success') { + dispatch(updateInsight(data)); + console.log('Alert updated successfully'); + } else { + console.error('Failed to update alert:', data); + alert('Failed to update alert.'); + } + }); + } else { + wsCreateInsight(alertData, (data: any, status: string) => { + setLoading(false); + if (status === 'success') { + dispatch(addInsight(data)); + console.log('Alert created successfully'); + if (props.setActive) props.setActive(data.id); + if (props.setActiveCategory) props.setActiveCategory('alert'); + } else { + console.error('Failed to create alert:', data); + alert('Failed to create alert.'); + } + }); } }; @@ -94,20 +103,18 @@ export function AlertingForm(props: Props) { } setLoading(true); - try { - debugger; - // await deleteAlert(props.insight.id); - dispatch(deleteInsight({ id: props.insight.id, type: props.insight.type })); - if (props.setActive && props.setActiveCategory) { - props.setActive(''); - props.setActiveCategory(undefined); - } - } catch (error) { - console.error('Failed to delete alert:', error); - alert('Failed to delete alert.'); - } finally { + wsDeleteInsight(props.insight.id, 'alert', (data: any, status: string) => { setLoading(false); - } + if (status === 'success') { + dispatch(deleteInsight({ id: props.insight.id, type: 'alert' })); + if (props.setActive) props.setActive(''); + if (props.setActiveCategory) props.setActiveCategory(undefined); + console.log('Alert deleted successfully'); + } else { + console.error('Failed to delete alert:', data); + alert('Failed to delete alert.'); + } + }); }; if (loading) { @@ -161,7 +168,7 @@ export function AlertingForm(props: Props) { </AccordionHead> <AccordionBody> <TextEditor - key={props.insight.id} + key={props.insight.id || 'new'} editorState={editorState} setEditorState={setEditorState} showToolbar={true} diff --git a/libs/shared/lib/insight-sharing/components/AddItem.tsx b/libs/shared/lib/insight-sharing/components/AddItem.tsx index 6db91c0d8..d58e3709b 100644 --- a/libs/shared/lib/insight-sharing/components/AddItem.tsx +++ b/libs/shared/lib/insight-sharing/components/AddItem.tsx @@ -4,6 +4,7 @@ import { MonitorType } from './Sidebar'; import { useAppDispatch, useSessionCache } from '../../data-access'; import { addInsight } from '../../data-access/store/insightSharingSlice'; import { v4 as uuidv4 } from 'uuid'; +import { wsCreateInsight } from '../../data-access/broker/wsInsightSharing'; type Props = { category: MonitorType; @@ -24,41 +25,31 @@ export function AddItem(props: Props) { alert('Please enter a name.'); return; } - if (!description.trim()) { - alert('Please enter a description.'); - return; - } const newInsight = { name, description, recipients: [], template: '', - frequency: props.category === 'report' ? 'Daily' : undefined, - save_state_id: session.currentSaveState, + frequency: props.category === 'report' ? 'Daily' : '', + saveStateId: session.currentSaveState || '', type: props.category, }; setLoading(true); - debugger; - // try { - // let savedInsight; - // if (props.category === 'report') { - // savedInsight = await createInsih(newInsight); - // } else { - // savedInsight = await createAlert(newInsight) - // } - - // dispatch(addInsight(savedInsight)); - // props.setActive(savedInsight.id); - // props.setActiveCategory(props.category); - // props.setAdding(false); - // } catch (error) { - // console.error('Failed to create insight:', error); - // alert('Failed to create new ' + props.category); - // } finally { - // setLoading(false); - // } + + wsCreateInsight(newInsight, (data: any, status: string) => { + setLoading(false); + if (status === 'success') { + dispatch(addInsight(data)); + props.setActive(data.id); + props.setActiveCategory(props.category); + props.setAdding(false); + } else { + console.error('Failed to create insight:', data); + alert('Failed to create new ' + props.category); + } + }); }; return ( diff --git a/libs/shared/lib/insight-sharing/components/Sidebar.tsx b/libs/shared/lib/insight-sharing/components/Sidebar.tsx index 059bbde9e..a3b7bd841 100644 --- a/libs/shared/lib/insight-sharing/components/Sidebar.tsx +++ b/libs/shared/lib/insight-sharing/components/Sidebar.tsx @@ -4,6 +4,7 @@ import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '../../co import { useAppDispatch, useAppSelector } from '../../data-access'; import { setInsights } from '../../data-access/store/insightSharingSlice'; import { useSessionCache } from '../../data-access'; +import { wsGetInsights } from '../../data-access/broker/wsInsightSharing'; export type MonitorType = 'report' | 'alert'; @@ -21,15 +22,11 @@ export function Sidebar(props: SidebarProps) { useEffect(() => { async function fetchReports() { - debugger; - // try { - // if (session.currentSaveState && session.currentSaveState !== '') { - // const fetchedReports = await getReports(session.currentSaveState); - // dispatch(setInsights(fetchedReports)); - // } - // } catch (error) { - // console.error('Failed to fetch reports:', error); - // } + if (session.currentSaveState && session.currentSaveState !== '') { + wsGetInsights(session.currentSaveState, (data: any, status: string)=>{ + dispatch(setInsights(data)); + }) + } } fetchReports(); diff --git a/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx b/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx index e3df37f75..0bff6cbbb 100644 --- a/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx +++ b/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx @@ -70,9 +70,6 @@ export function ReportingForm(props: Props) { alert('Please enter content in the text editor before saving.'); return; } - if(!session.currentSaveState){ - console.warn("No active save state found - this might affect report creation/updating"); - } const reportData: InsightRequest = { name, @@ -81,34 +78,36 @@ export function ReportingForm(props: Props) { frequency, template: JSON.stringify(editorState), saveStateId: session.currentSaveState || "", - type: 'report', + type: 'report' as const, }; setLoading(true); - // if (props.insight.id) { - // wsUpdateInsight(reportData, (data: any, status: string) => { - // setLoading(false); - // if (status === 'success') { - // dispatch(updateInsight(data)); - // console.log('Report updated successfully'); - // } else { - // console.error('Failed to update report:', data); - // alert('Failed to update report.'); - // } - // }); - // } else { + if (props.insight.id) { + wsUpdateInsight(props.insight.id, reportData, (data: any, status: string) => { + setLoading(false); + if (status === 'success') { + dispatch(updateInsight(data)); + console.log('Report updated successfully'); + } else { + console.error('Failed to update report:', data); + alert('Failed to update report.'); + } + }); + } else { wsCreateInsight(reportData, (data: any, status: string) => { debugger; setLoading(false); if (status === 'success') { dispatch(updateInsight(data)); console.log('Report created successfully'); + if (props.setActive) props.setActive(data.id); + if (props.setActiveCategory) props.setActiveCategory('report'); } else { console.error('Failed to create report:', data); alert('Failed to create report.'); } }); - // } + } }; const handleDelete = async () => { @@ -196,7 +195,7 @@ export function ReportingForm(props: Props) { </AccordionHead> <AccordionBody> <TextEditor - key={props.insight.id} + key={props.insight.id || 'new'} editorState={editorState} setEditorState={setEditorState} showToolbar={true} @@ -211,4 +210,4 @@ export function ReportingForm(props: Props) { </div> </div> ); -} +} \ No newline at end of file -- GitLab From 3eafc3ced8f09d5e03b49ff06efc696417ed02c6 Mon Sep 17 00:00:00 2001 From: Leonardo <leomilho@gmail.com> Date: Tue, 3 Dec 2024 17:10:49 +0100 Subject: [PATCH 5/9] chore: remove webpage alerts --- .../insight-sharing/alerting/AlertingForm.tsx | 39 +++++-------------- .../reporting/ReportingForm.tsx | 37 +++++------------- 2 files changed, 19 insertions(+), 57 deletions(-) diff --git a/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx b/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx index 0b53289b6..2ec1ad5b5 100644 --- a/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx +++ b/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx @@ -7,6 +7,7 @@ import { Button, Input, LoadingSpinner } from '../../components'; import { EditorState } from 'lexical'; import { MonitorType } from '../components/Sidebar'; import { wsUpdateInsight, wsCreateInsight, wsDeleteInsight } from '../../data-access/broker/wsInsightSharing'; +import { addError } from '../../data-access/store/configSlice'; type Props = { insight: InsightResponse; @@ -45,30 +46,22 @@ export function AlertingForm(props: Props) { const handleSave = async () => { if (!name || name.trim() === '') { - alert('Please enter a name for the alert.'); - return; - } - if (!description || description.trim() === '') { - alert('Please enter a description for the alert.'); - return; - } - if (!editorState) { - alert('Please enter content in the text editor before saving.'); + dispatch(addError('Name is required')); return; } - const alertData : InsightRequest = { + const alertData: InsightRequest = { name, description, recipients, template: JSON.stringify(editorState), saveStateId: session.currentSaveState || '', type: 'alert' as const, - frequency: '' + frequency: '', }; setLoading(true); - + if (props.insight.id) { wsUpdateInsight(props.insight.id, alertData, (data: any, status: string) => { setLoading(false); @@ -124,20 +117,8 @@ export function AlertingForm(props: Props) { return ( <div> <span className="text-lg text-secondary-600 font-bold mb-4">Alert ID: {props.insight.id}</span> - <Input - label="Name" - type="text" - value={name} - onChange={setName} - className="mb-4" - /> - <Input - label="Description" - type="text" - value={description} - onChange={setDescription} - className="mb-4" - /> + <Input label="Name" type="text" value={name} onChange={setName} className="mb-4" /> + <Input label="Description" type="text" value={description} onChange={setDescription} className="mb-4" /> <Accordion defaultOpenAll={true} className="border-t divide-y"> <AccordionItem className="pt-2 pb-4"> <AccordionHead showArrow={false}> @@ -152,8 +133,8 @@ export function AlertingForm(props: Props) { setRecipientInput(String(value)); const recipientList = String(value) .split(/[, ]/) - .map(r => r.trim()) - .filter(r => r.length > 0); + .map((r) => r.trim()) + .filter((r) => r.length > 0); setRecipients(recipientList); }} placeholder="Enter recipient(s)" @@ -183,4 +164,4 @@ export function AlertingForm(props: Props) { </div> </div> ); -} \ No newline at end of file +} diff --git a/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx b/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx index 0bff6cbbb..fd827f26f 100644 --- a/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx +++ b/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx @@ -7,6 +7,7 @@ import { useAppDispatch, useSessionCache } from '../../data-access'; import { MonitorType } from '../components/Sidebar'; import { EditorState } from 'lexical'; import { wsCreateInsight, wsDeleteInsight, wsUpdateInsight } from '../../data-access/broker/wsInsightSharing'; +import { addError } from '../../data-access/store/configSlice'; type Props = { insight: InsightResponse; @@ -59,15 +60,7 @@ export function ReportingForm(props: Props) { const handleSave = async () => { if (!name || name.trim() === '') { - alert('Please enter a name for the report.'); - return; - } - if (!description || description.trim() === '') { - alert('Please enter a description for the report.'); - return; - } - if (!editorState) { - alert('Please enter content in the text editor before saving.'); + dispatch(addError('Please enter a name for the report.')); return; } @@ -77,7 +70,7 @@ export function ReportingForm(props: Props) { recipients, frequency, template: JSON.stringify(editorState), - saveStateId: session.currentSaveState || "", + saveStateId: session.currentSaveState || '', type: 'report' as const, }; @@ -107,7 +100,7 @@ export function ReportingForm(props: Props) { alert('Failed to create report.'); } }); - } + } }; const handleDelete = async () => { @@ -136,20 +129,8 @@ export function ReportingForm(props: Props) { ) : ( <div> <span className="text-lg text-secondary-600 font-bold mb-4">Report ID: {props.insight.id}</span> - <Input - label="Name" - type="text" - value={name} - onChange={handleSetName} - className="mb-4" - /> - <Input - label="Description" - type="text" - value={description} - onChange={handleSetDescription} - className="mb-4" - /> + <Input label="Name" type="text" value={name} onChange={handleSetName} className="mb-4" /> + <Input label="Description" type="text" value={description} onChange={handleSetDescription} className="mb-4" /> <Accordion defaultOpenAll={true} className="border-t divide-y"> <AccordionItem className="pt-2 pb-4"> <AccordionHead showArrow={false}> @@ -164,8 +145,8 @@ export function ReportingForm(props: Props) { setRecipientInput(String(value)); const recipientList = String(value) .split(/[, ]/) - .map(r => r.trim()) - .filter(r => r.length > 0); + .map((r) => r.trim()) + .filter((r) => r.length > 0); setRecipients(recipientList); }} placeholder="Enter recipient(s)" @@ -210,4 +191,4 @@ export function ReportingForm(props: Props) { </div> </div> ); -} \ No newline at end of file +} -- GitLab From 9d73651b2a4fdf15c940cb7b1ba790fda178b6a0 Mon Sep 17 00:00:00 2001 From: Dennis Collaris <d.collaris@me.com> Date: Wed, 4 Dec 2024 13:06:46 +0100 Subject: [PATCH 6/9] feat: add import/export capabilities to variable nodes --- .../components/textEditor/VariableNode.tsx | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/libs/shared/lib/components/textEditor/VariableNode.tsx b/libs/shared/lib/components/textEditor/VariableNode.tsx index 300242055..a2341e498 100644 --- a/libs/shared/lib/components/textEditor/VariableNode.tsx +++ b/libs/shared/lib/components/textEditor/VariableNode.tsx @@ -1,4 +1,4 @@ -import type { NodeKey, LexicalEditor, DOMExportOutput } from 'lexical'; +import type { NodeKey, LexicalEditor, DOMExportOutput, SerializedLexicalNode, Spread } from 'lexical'; import { DecoratorNode, EditorConfig } from 'lexical'; export enum VariableType { @@ -6,6 +6,14 @@ export enum VariableType { visualization = 'visualization', } +export type SerializedVariableNode = Spread< + { + name: string, + variableType: VariableType + }, + SerializedLexicalNode +>; + export class VariableNode extends DecoratorNode<JSX.Element> { __name: string; __variableType: VariableType; @@ -39,6 +47,22 @@ export class VariableNode extends DecoratorNode<JSX.Element> { return `{{ ${self.__variableType}:${self.__name} }}`; } + // Import and export + + exportJSON(): SerializedVariableNode { + return { + type: this.getType(), + variableType: this.__variableType, + name: this.__name, + version: 1, + }; + } + + static importJSON(jsonNode: SerializedVariableNode): VariableNode { + const node = new VariableNode(jsonNode.name, jsonNode.variableType); + return node; + } + // View createDOM(config: EditorConfig): HTMLElement { -- GitLab From fac6a8827d9b17c66ecd4a177dcaaa95dc7f9f80 Mon Sep 17 00:00:00 2001 From: Leonardo <leomilho@gmail.com> Date: Tue, 3 Dec 2024 18:40:01 +0100 Subject: [PATCH 7/9] chore: style fixes and missing code fixes --- libs/shared/lib/data-access/api/eventBus.tsx | 4 +- .../data-access/broker/wsInsightSharing.ts | 40 ++++----- libs/shared/lib/data-access/store/hooks.ts | 4 + .../data-access/store/insightSharingSlice.ts | 36 +++----- .../AlertingForm.tsx => FormAlert.tsx} | 31 ++++--- .../ReportingForm.tsx => FormReport.tsx} | 30 ++++--- .../lib/insight-sharing/InsightDialog.tsx | 55 +++++++----- .../lib/insight-sharing/SettingsPanel.tsx | 88 ------------------ .../insight-sharing/components/AddItem.tsx | 23 ++--- .../insight-sharing/components/Sidebar.tsx | 89 +++++++++---------- .../components/StartScreen.tsx | 11 +-- 11 files changed, 161 insertions(+), 250 deletions(-) rename libs/shared/lib/insight-sharing/{alerting/AlertingForm.tsx => FormAlert.tsx} (84%) rename libs/shared/lib/insight-sharing/{reporting/ReportingForm.tsx => FormReport.tsx} (87%) delete mode 100644 libs/shared/lib/insight-sharing/SettingsPanel.tsx diff --git a/libs/shared/lib/data-access/api/eventBus.tsx b/libs/shared/lib/data-access/api/eventBus.tsx index 03d9e7f29..d58c915cf 100644 --- a/libs/shared/lib/data-access/api/eventBus.tsx +++ b/libs/shared/lib/data-access/api/eventBus.tsx @@ -43,14 +43,14 @@ import { setFetchingSaveStates, setStateAuthorization, } from '../store/sessionSlice'; -import { URLParams, getParam, deleteParam } from './url'; +import { URLParams, getParam } from './url'; import { VisState, setVisualizationState } from '../store/visualizationSlice'; import { isEqual } from 'lodash-es'; import { setSchemaAttributeDimensions, setSchemaAttributeInformation, setSchemaLoading } from '../store/schemaSlice'; import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; import { unSelect } from '../store/interactionSlice'; import { SchemaGraphStats } from '../../schema'; -import { wsUserGetPolicy, wsUserPolicyCheck } from '../broker/wsUser'; +import { wsUserGetPolicy } from '../broker/wsUser'; import { authorized } from '../store/authSlice'; export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }) => { diff --git a/libs/shared/lib/data-access/broker/wsInsightSharing.ts b/libs/shared/lib/data-access/broker/wsInsightSharing.ts index b9432157e..04c366a3e 100644 --- a/libs/shared/lib/data-access/broker/wsInsightSharing.ts +++ b/libs/shared/lib/data-access/broker/wsInsightSharing.ts @@ -11,7 +11,7 @@ export function wsGetInsights(saveStateId: string, callback?: GetInsightsRespons { key: 'insight', subKey: 'getAll', - body: JSON.stringify({saveStateId: saveStateId}) + body: JSON.stringify({ saveStateId: saveStateId }) }, internalCallback ); @@ -27,29 +27,29 @@ export function wsCreateInsight(insight: InsightRequest, callback?: Function) { (data: any, status: string) => { if (status === 'Bad Request') { console.error('Failed to create insight:', data); - if (callback) callback(data, status); - return; - } + if (callback) callback(data, status); + return; + } - if (!data || typeof data !== 'object') { - console.error('Invalid repsonse data', data) - if (callback) callback(null, 'error'); - return; - } + if (!data || typeof data !== 'object') { + console.error('Invalid repsonse data', data) + if (callback) callback(null, 'error'); + return; + } - if (!data.type || !data.id) { - console.error('Missing fields in response', data) - if (callback) callback(null, 'error'); - return; - } + if (!data.type || !data.id) { + console.error('Missing fields in response', data) + if (callback) callback(null, 'error'); + return; + } - if (callback) callback(data,status); + if (callback) callback(data, status); } ); } -export function wsUpdateInsight( - id: string, +export function wsUpdateInsight( + id: string, insight: InsightRequest, callback?: (data: any, status: string) => void ) { @@ -57,7 +57,7 @@ export function wsUpdateInsight( { key: 'insight', subKey: 'update', - body: JSON.stringify({id: id, insight: insight}), + body: JSON.stringify({ id: id, insight: insight }), }, (data: any, status: string) => { if (status === 'Bad Request') { @@ -68,13 +68,13 @@ export function wsUpdateInsight( ); } -export function wsDeleteInsight(id: string, type: InsightType, callback?: Function) { +export function wsDeleteInsight(id: string, callback?: Function) { Broker.instance().sendMessage( { key: 'insight', subKey: 'delete', - body: JSON.stringify({ id, type }), + body: JSON.stringify({ id }), }, (data: any, status: string) => { if (status === 'Bad Request') { diff --git a/libs/shared/lib/data-access/store/hooks.ts b/libs/shared/lib/data-access/store/hooks.ts index 7df070ec3..444a28b38 100644 --- a/libs/shared/lib/data-access/store/hooks.ts +++ b/libs/shared/lib/data-access/store/hooks.ts @@ -44,6 +44,7 @@ import { PolicyUsersState, selectPolicyState } from './authorizationUsersSlice'; import { PolicyResourcesState, selectResourcesPolicyState } from './authorizationResourcesSlice'; import { SaveStateAuthorizationHeaders, SaveStateI } from '..'; import { GraphStatistics } from '../../statistics'; +import { InsightResponse, selectInsights } from './insightSharingSlice'; // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch: () => AppDispatch = useDispatch; @@ -100,3 +101,6 @@ export const useUsersPolicy: () => PolicyUsersState = () => useAppSelector(selec // Authorization Resources Slice export const useResourcesPolicy: () => PolicyResourcesState = () => useAppSelector(selectResourcesPolicyState); + +// Insights - Reports and Alerts +export const useInsights: () => InsightResponse[] = () => useAppSelector(selectInsights); \ No newline at end of file diff --git a/libs/shared/lib/data-access/store/insightSharingSlice.ts b/libs/shared/lib/data-access/store/insightSharingSlice.ts index d9485069d..a3a4105e0 100644 --- a/libs/shared/lib/data-access/store/insightSharingSlice.ts +++ b/libs/shared/lib/data-access/store/insightSharingSlice.ts @@ -21,13 +21,9 @@ export type InsightResponse = { type InsightState = { insights: InsightResponse[]; - reports: InsightResponse[]; - alerts: InsightResponse[]; } const initialState: InsightState = { - reports: [], - alerts: [], insights: [] }; @@ -35,10 +31,8 @@ const insightSharingSlice = createSlice({ name: 'insightSharing', initialState, reducers: { - setInsights(state, action: PayloadAction<InsightResponse[] | { reports: InsightResponse[] }>) { - const insights = Array.isArray(action.payload) ? action.payload : action.payload.reports; - state.reports = insights.filter((insight) => insight.type === 'report'); - state.alerts = insights.filter((insight) => insight.type === 'alert'); + setInsights(state, action: PayloadAction<InsightResponse[]>) { + state.insights = action.payload; }, addInsight: (state, action: PayloadAction<InsightResponse>) => { if (!action.payload) { @@ -52,33 +46,29 @@ const insightSharingSlice = createSlice({ return; } - if (type === 'report') { - state.reports.push(action.payload); - } else if (type === 'alert') { - state.alerts.push(action.payload); - } + state.insights.push(action.payload); }, updateInsight(state, action: PayloadAction<InsightResponse>) { - const insights = action.payload.type === 'report' ? state.reports : state.alerts; - const index = insights.findIndex((i) => i.id === action.payload.id); + const index = state.insights.findIndex((i) => i.id === action.payload.id); if (index !== -1) { - insights[index] = { ...action.payload }; + state.insights[index] = { ...action.payload }; } else { - insights.push(action.payload); + state.insights.push(action.payload); } }, - deleteInsight(state, action: PayloadAction<{ id: string; type: 'report' | 'alert' }>) { - const insights = action.payload.type === 'report' ? state.reports : state.alerts; - const index = insights.findIndex((i) => i.id === action.payload.id); + deleteInsight(state, action: PayloadAction<{ id: string; }>) { + const index = state.insights.findIndex((i) => i.id === action.payload.id); if (index !== -1) { - insights.splice(index, 1); + state.insights.splice(index, 1); } }, }, }); export const { setInsights, addInsight, updateInsight, deleteInsight } = insightSharingSlice.actions; -export const selectReports = (state: RootState): InsightResponse[] => state.insightSharing.reports; -export const selectAlerts = (state: RootState): InsightResponse[] => state.insightSharing.alerts; + +export const selectReports = (state: RootState): InsightResponse[] => state.insightSharing.insights.filter((i) => i.type === 'report'); +export const selectAlerts = (state: RootState): InsightResponse[] => state.insightSharing.insights.filter((i) => i.type === 'alert'); +export const selectInsights = (state: RootState): InsightResponse[] => state.insightSharing.insights; export default insightSharingSlice.reducer; diff --git a/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx b/libs/shared/lib/insight-sharing/FormAlert.tsx similarity index 84% rename from libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx rename to libs/shared/lib/insight-sharing/FormAlert.tsx index 2ec1ad5b5..e9ebe20f7 100644 --- a/libs/shared/lib/insight-sharing/alerting/AlertingForm.tsx +++ b/libs/shared/lib/insight-sharing/FormAlert.tsx @@ -1,22 +1,27 @@ import React, { useEffect, useState } from 'react'; -import { Accordion, AccordionItem, AccordionHead, AccordionBody } from '../../components/accordion'; -import { TextEditor } from '../../components/textEditor'; -import { InsightResponse, updateInsight, deleteInsight, addInsight, InsightRequest } from '../../data-access/store/insightSharingSlice'; -import { useAppDispatch, useSessionCache } from '../../data-access'; -import { Button, Input, LoadingSpinner } from '../../components'; +import { Accordion, AccordionItem, AccordionHead, AccordionBody } from '../components/accordion'; +import { TextEditor } from '../components/textEditor'; +import { + InsightResponse, + updateInsight, + deleteInsight, + addInsight, + InsightRequest, +} from '@graphpolaris/shared/lib/data-access/store/insightSharingSlice'; +import { useAppDispatch, useSessionCache } from '@graphpolaris/shared/lib/data-access'; +import { Button, Input, LoadingSpinner } from '../components'; import { EditorState } from 'lexical'; -import { MonitorType } from '../components/Sidebar'; -import { wsUpdateInsight, wsCreateInsight, wsDeleteInsight } from '../../data-access/broker/wsInsightSharing'; -import { addError } from '../../data-access/store/configSlice'; +import { MonitorType } from './components/Sidebar'; +import { wsUpdateInsight, wsCreateInsight, wsDeleteInsight } from '@graphpolaris/shared/lib/data-access/broker/wsInsightSharing'; +import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; type Props = { insight: InsightResponse; - setActiveCategory?: (val: MonitorType | undefined) => void; setActive?: (val: string) => void; handleDelete: () => void; }; -export function AlertingForm(props: Props) { +export function FormAlert(props: Props) { const dispatch = useAppDispatch(); const session = useSessionCache(); const [loading, setLoading] = useState(false); @@ -80,7 +85,6 @@ export function AlertingForm(props: Props) { dispatch(addInsight(data)); console.log('Alert created successfully'); if (props.setActive) props.setActive(data.id); - if (props.setActiveCategory) props.setActiveCategory('alert'); } else { console.error('Failed to create alert:', data); alert('Failed to create alert.'); @@ -96,12 +100,11 @@ export function AlertingForm(props: Props) { } setLoading(true); - wsDeleteInsight(props.insight.id, 'alert', (data: any, status: string) => { + wsDeleteInsight(props.insight.id, (data: any, status: string) => { setLoading(false); if (status === 'success') { - dispatch(deleteInsight({ id: props.insight.id, type: 'alert' })); + dispatch(deleteInsight({ id: props.insight.id })); if (props.setActive) props.setActive(''); - if (props.setActiveCategory) props.setActiveCategory(undefined); console.log('Alert deleted successfully'); } else { console.error('Failed to delete alert:', data); diff --git a/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx b/libs/shared/lib/insight-sharing/FormReport.tsx similarity index 87% rename from libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx rename to libs/shared/lib/insight-sharing/FormReport.tsx index fd827f26f..de7744595 100644 --- a/libs/shared/lib/insight-sharing/reporting/ReportingForm.tsx +++ b/libs/shared/lib/insight-sharing/FormReport.tsx @@ -1,22 +1,26 @@ import React, { useState, useEffect } from 'react'; -import { Button, Input, LoadingSpinner } from '../../components'; -import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '../../components/accordion'; -import { TextEditor } from '../../components/textEditor'; -import { InsightResponse, updateInsight, deleteInsight, InsightRequest } from '../../data-access/store/insightSharingSlice'; -import { useAppDispatch, useSessionCache } from '../../data-access'; -import { MonitorType } from '../components/Sidebar'; +import { Button, Input, LoadingSpinner } from '../components'; +import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '../components/accordion'; +import { TextEditor } from '../components/textEditor'; +import { + InsightResponse, + updateInsight, + deleteInsight, + InsightRequest, +} from '@graphpolaris/shared/lib/data-access/store/insightSharingSlice'; +import { useAppDispatch, useSessionCache } from '@graphpolaris/shared/lib/data-access'; +import { MonitorType } from './components/Sidebar'; import { EditorState } from 'lexical'; -import { wsCreateInsight, wsDeleteInsight, wsUpdateInsight } from '../../data-access/broker/wsInsightSharing'; -import { addError } from '../../data-access/store/configSlice'; +import { wsCreateInsight, wsDeleteInsight, wsUpdateInsight } from '@graphpolaris/shared/lib/data-access/broker/wsInsightSharing'; +import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; type Props = { insight: InsightResponse; - setActiveCategory: (val: MonitorType | undefined) => void; setActive: (val: string) => void; handleDelete: () => void; }; -export function ReportingForm(props: Props) { +export function FormReport(props: Props) { const dispatch = useAppDispatch(); const session = useSessionCache(); const [loading, setLoading] = useState(false); @@ -94,7 +98,6 @@ export function ReportingForm(props: Props) { dispatch(updateInsight(data)); console.log('Report created successfully'); if (props.setActive) props.setActive(data.id); - if (props.setActiveCategory) props.setActiveCategory('report'); } else { console.error('Failed to create report:', data); alert('Failed to create report.'); @@ -110,12 +113,11 @@ export function ReportingForm(props: Props) { } setLoading(true); - wsDeleteInsight(props.insight.id, props.insight.type, (data: any, status: string) => { + wsDeleteInsight(props.insight.id, (data: any, status: string) => { setLoading(false); if (status === 'success') { - dispatch(deleteInsight({ id: props.insight.id, type: props.insight.type })); + dispatch(deleteInsight({ id: props.insight.id })); props.setActive(''); - props.setActiveCategory(undefined); console.log('Report deleted successfully'); } else { console.error('Failed to delete report:', data); diff --git a/libs/shared/lib/insight-sharing/InsightDialog.tsx b/libs/shared/lib/insight-sharing/InsightDialog.tsx index 71d93bf91..44f1d2fe1 100644 --- a/libs/shared/lib/insight-sharing/InsightDialog.tsx +++ b/libs/shared/lib/insight-sharing/InsightDialog.tsx @@ -1,7 +1,13 @@ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { Dialog, DialogContent } from '../components'; -import { SettingsPanel } from './SettingsPanel'; import { MonitorType, Sidebar } from './components/Sidebar'; +import { AddItem } from './components/AddItem'; +import { useAppDispatch, useInsights } from '..'; +import { StartScreen } from './components/StartScreen'; +import { FormReport } from './FormReport'; +import { FormAlert } from './FormAlert'; +import { wsDeleteInsight } from '../data-access/broker/wsInsightSharing'; +import { deleteInsight } from '../data-access/store/insightSharingSlice'; type Props = { open: boolean; @@ -9,13 +15,20 @@ type Props = { }; export function InsightDialog(props: Props) { - const [adding, setAdding] = useState<boolean>(false); + const dispatch = useAppDispatch(); + + const [adding, setAdding] = useState<false | MonitorType>(false); const [active, setActive] = useState<string>(''); - const [activeCategory, setActiveCategory] = useState<MonitorType | undefined>(undefined); + const insights = useInsights(); + + const selectedInsight = useMemo(() => insights.find((i) => i.id === active), [active, insights]); - const handleChangeActive = (category: MonitorType, id: string) => { - setActive(id); - setActiveCategory(category); + const handleDelete = () => { + if (!selectedInsight) return; + + dispatch(deleteInsight({ id: selectedInsight.id })); + wsDeleteInsight(selectedInsight.id); + setActive(''); }; return ( @@ -27,19 +40,21 @@ export function InsightDialog(props: Props) { > <DialogContent className="w-5/6 h-5/6 rounded-none py-0 px-0"> <div className="flex w-full h-full"> - <Sidebar - changeActive={handleChangeActive} - setAdding={setAdding} - setActiveCategory={setActiveCategory} - /> - <SettingsPanel - activeCategory={activeCategory} - active={active} - adding={adding} - setAdding={setAdding} - setActiveCategory={setActiveCategory} - setActive={setActive} - /> + <div className="w-1/4 border-r overflow-auto flex flex-col h-full"> + <Sidebar setAdding={setAdding} setActive={setActive} /> + </div> + + <div className="w-3/4 p-8"> + {adding ? ( + <AddItem setAdding={setAdding} setActive={setActive} type={adding} /> + ) : !selectedInsight ? ( + <StartScreen setAdding={setAdding} /> + ) : selectedInsight.type === 'report' ? ( + <FormReport insight={selectedInsight} setActive={setActive} handleDelete={handleDelete} /> + ) : ( + <FormAlert insight={selectedInsight} setActive={setActive} handleDelete={handleDelete} /> + )} + </div> </div> </DialogContent> </Dialog> diff --git a/libs/shared/lib/insight-sharing/SettingsPanel.tsx b/libs/shared/lib/insight-sharing/SettingsPanel.tsx deleted file mode 100644 index ba6d02f74..000000000 --- a/libs/shared/lib/insight-sharing/SettingsPanel.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { ReportingForm } from './reporting/ReportingForm'; -import { AlertingForm } from './alerting/AlertingForm'; -import { AddItem } from './components/AddItem'; -import { StartScreen } from './components/StartScreen'; -import { MonitorType } from './components/Sidebar'; -import { useAppDispatch, useAppSelector } from '../data-access'; -import { - deleteInsight, - selectReports, - selectAlerts, - InsightResponse, -} from '../data-access/store/insightSharingSlice'; -import { wsDeleteInsight } from '../data-access/broker/wsInsightSharing'; - -type Props = { - active: string; - activeCategory: MonitorType | undefined; - adding: boolean; - setAdding: (val: boolean) => void; - setActiveCategory: (val: MonitorType | undefined) => void; - setActive: (val: string) => void; -}; - -export function SettingsPanel(props: Props) { - const dispatch = useAppDispatch(); - const reports = useAppSelector(selectReports); - const alerts = useAppSelector(selectAlerts); - - const [currentInsight, setCurrentInsight] = useState<InsightResponse | null>(null); - - useEffect(() => { - if (props.activeCategory && props.active) { - const insights = props.activeCategory === 'report' ? reports : alerts; - const insight = insights.find((i) => i.id === props.active); - if (insight) { - setCurrentInsight(insight); - } - } else { - setCurrentInsight(null); - } - }, [props.activeCategory, props.active, reports, alerts]); - - const handleDelete = () => { - if (currentInsight) { - dispatch(deleteInsight({ id: currentInsight.id, type: currentInsight.type })); - wsDeleteInsight(currentInsight.id, currentInsight.type); - props.setActive(''); - props.setActiveCategory(undefined); - } - }; - - return props.activeCategory ? ( - <div className="w-3/4 p-4"> - {props.adding ? ( - <AddItem - category={props.activeCategory} - setAdding={props.setAdding} - setActive={props.setActive} - setActiveCategory={props.setActiveCategory} - /> - ) : currentInsight ? ( - <div> - {props.activeCategory === 'report' ? ( - <ReportingForm - insight={currentInsight} - setActiveCategory={props.setActiveCategory} - setActive={props.setActive} - handleDelete={handleDelete} - /> - ) : ( - <AlertingForm - insight={currentInsight} - setActiveCategory={props.setActiveCategory} - setActive={props.setActive} - handleDelete={handleDelete} - /> - )} - </div> - ) : null} - </div> - ) : ( - <StartScreen - setAdding={props.setAdding} - setActiveCategory={props.setActiveCategory} - /> - ); -} diff --git a/libs/shared/lib/insight-sharing/components/AddItem.tsx b/libs/shared/lib/insight-sharing/components/AddItem.tsx index d58e3709b..fe8f3f173 100644 --- a/libs/shared/lib/insight-sharing/components/AddItem.tsx +++ b/libs/shared/lib/insight-sharing/components/AddItem.tsx @@ -1,22 +1,19 @@ import React, { useState } from 'react'; import { Input, Button } from '../../components'; -import { MonitorType } from './Sidebar'; +import { MonitorType as InsightType, MonitorType } from './Sidebar'; import { useAppDispatch, useSessionCache } from '../../data-access'; import { addInsight } from '../../data-access/store/insightSharingSlice'; -import { v4 as uuidv4 } from 'uuid'; import { wsCreateInsight } from '../../data-access/broker/wsInsightSharing'; type Props = { - category: MonitorType; - setAdding: (val: boolean) => void; + setAdding: (val: false | MonitorType) => void; setActive: (val: string) => void; - setActiveCategory: (val: MonitorType | undefined) => void; + type: InsightType; }; export function AddItem(props: Props) { const [name, setName] = useState<string>(''); const [description, setDescription] = useState<string>(''); - const [loading, setLoading] = useState(false); const dispatch = useAppDispatch(); const session = useSessionCache(); @@ -31,30 +28,26 @@ export function AddItem(props: Props) { description, recipients: [], template: '', - frequency: props.category === 'report' ? 'Daily' : '', + frequency: props.type === 'report' ? 'Daily' : '', saveStateId: session.currentSaveState || '', - type: props.category, + type: props.type, }; - setLoading(true); - - + wsCreateInsight(newInsight, (data: any, status: string) => { - setLoading(false); if (status === 'success') { dispatch(addInsight(data)); props.setActive(data.id); - props.setActiveCategory(props.category); props.setAdding(false); } else { console.error('Failed to create insight:', data); - alert('Failed to create new ' + props.category); + alert('Failed to create new ' + props.type); } }); }; return ( <div> - <span className="text-lg text-secondary-600 font-bold mb-4">Add a new {props.category}ing service</span> + <span className="text-lg text-secondary-600 font-bold mb-4">Add a new {props.type}ing service</span> <Input type="text" label="Name" value={name} onChange={setName} className="mb-2" /> <Input type="text" label="Description" value={description} onChange={setDescription} className="mb-2" /> <Button label="Save" onClick={handleSave} disabled={!name || !description} className="mt-2" /> diff --git a/libs/shared/lib/insight-sharing/components/Sidebar.tsx b/libs/shared/lib/insight-sharing/components/Sidebar.tsx index a3b7bd841..96aefb1f1 100644 --- a/libs/shared/lib/insight-sharing/components/Sidebar.tsx +++ b/libs/shared/lib/insight-sharing/components/Sidebar.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; import { Button } from '../../components'; import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '../../components/accordion'; -import { useAppDispatch, useAppSelector } from '../../data-access'; +import { useAppDispatch, useInsights } from '../../data-access'; import { setInsights } from '../../data-access/store/insightSharingSlice'; import { useSessionCache } from '../../data-access'; import { wsGetInsights } from '../../data-access/broker/wsInsightSharing'; @@ -9,42 +9,34 @@ import { wsGetInsights } from '../../data-access/broker/wsInsightSharing'; export type MonitorType = 'report' | 'alert'; type SidebarProps = { - changeActive: (category: MonitorType, val: string) => void; - setAdding: (val: boolean) => void; - setActiveCategory: (val: MonitorType | undefined) => void; + setAdding: (val: false | MonitorType) => void; + setActive: (val: string) => void; }; export function Sidebar(props: SidebarProps) { const dispatch = useAppDispatch(); const session = useSessionCache(); - const reports = useAppSelector((state) => state.insightSharing.reports); - const alerts = useAppSelector((state) => state.insightSharing.alerts); + const insights = useInsights(); useEffect(() => { - async function fetchReports() { - if (session.currentSaveState && session.currentSaveState !== '') { - wsGetInsights(session.currentSaveState, (data: any, status: string)=>{ - dispatch(setInsights(data)); - }) - } + if (session.currentSaveState && session.currentSaveState !== '') { + wsGetInsights(session.currentSaveState, (data: any, status: string) => { + dispatch(setInsights(data)); + }); } - - fetchReports(); - }, [session.currentSaveState, dispatch]); - + }, [session.currentSaveState]); return ( - <div className="w-1/4 border-r overflow-auto flex flex-col h-full"> + <> <span className="text-lg text-secondary-700 font-semibold px-2 py-4">Insight Sharing</span> - <Accordion defaultOpenIndex={0}> + <Accordion defaultOpenAll={true}> <AccordionItem className=""> <AccordionHead className="border-b bg-secondary-50 hover:bg-secondary-100 p-1"> <div className="w-full flex justify-between"> <span className="font-semibold">Reports</span> <div onClick={(e) => { - props.setAdding(true); - props.setActiveCategory('report'); + props.setAdding('report'); e.stopPropagation(); }} > @@ -61,18 +53,20 @@ export function Sidebar(props: SidebarProps) { </AccordionHead> <AccordionBody className="ml-0"> <ul className="space-y-2"> - {reports.map((report) => ( - <li - key={report.id} - className="cursor-pointer p-2 hover:bg-secondary-50" - onClick={() => { - props.changeActive('report', report.id); - props.setAdding(false); - }} - > - {report.name} - </li> - ))} + {insights + .filter((insight) => insight.type === 'report') + .map((report) => ( + <li + key={report.id} + className="cursor-pointer p-2 hover:bg-secondary-50" + onClick={() => { + props.setAdding(false); + props.setActive(report.id); + }} + > + {report.name} + </li> + ))} </ul> </AccordionBody> </AccordionItem> @@ -82,8 +76,7 @@ export function Sidebar(props: SidebarProps) { <span className="font-semibold">Alerts</span> <div onClick={(e) => { - props.setAdding(true); - props.setActiveCategory('alert'); + props.setAdding('alert'); e.stopPropagation(); }} > @@ -100,22 +93,24 @@ export function Sidebar(props: SidebarProps) { </AccordionHead> <AccordionBody className="ml-0"> <ul className="space-y-2"> - {alerts.map((alert) => ( - <li - key={alert.id} - className="cursor-pointer p-2 hover:bg-secondary-50" - onClick={() => { - props.changeActive('alert', alert.id); - props.setAdding(false); - }} - > - {alert.name} - </li> - ))} + {insights + .filter((insight) => insight.type === 'alert') + .map((alert) => ( + <li + key={alert.id} + className="cursor-pointer p-2 hover:bg-secondary-50" + onClick={() => { + props.setAdding(false); + props.setActive(alert.id); + }} + > + {alert.name} + </li> + ))} </ul> </AccordionBody> </AccordionItem> </Accordion> - </div> + </> ); } diff --git a/libs/shared/lib/insight-sharing/components/StartScreen.tsx b/libs/shared/lib/insight-sharing/components/StartScreen.tsx index 801331c40..377ad49d9 100644 --- a/libs/shared/lib/insight-sharing/components/StartScreen.tsx +++ b/libs/shared/lib/insight-sharing/components/StartScreen.tsx @@ -3,13 +3,12 @@ import { Button, Icon } from '../../components'; import { MonitorType } from './Sidebar'; type Props = { - setAdding: (val: boolean) => void; - setActiveCategory: (val: MonitorType | undefined) => void; + setAdding: (val: false | MonitorType) => void; }; export function StartScreen(props: Props) { return ( - <div className="w-full flex justify-center items-center"> + <div className="w-full h-full flex justify-center items-center"> <div className=""> <span className="text-lg text-secondary-700 font-bold mb-4">Start</span> <div> @@ -19,8 +18,7 @@ export function StartScreen(props: Props) { variant="outline" className="mb-2" onClick={() => { - props.setAdding(true); - props.setActiveCategory('report'); + props.setAdding('report'); }} /> <Button @@ -28,8 +26,7 @@ export function StartScreen(props: Props) { label="New alert" variant="outline" onClick={() => { - props.setAdding(true); - props.setActiveCategory('alert'); + props.setAdding('alert'); }} /> </div> -- GitLab From 9881c8b5f6c68db6b2ad0b5d155589dec99dfc1b Mon Sep 17 00:00:00 2001 From: Samed <sbalcioglu@graphpolaris.com> Date: Fri, 6 Dec 2024 16:46:14 +0100 Subject: [PATCH 8/9] fix: insight saves properly in fe & be --- .../lib/components/textEditor/TextEditor.tsx | 59 +++++++++++------ .../plugins/InsertVariablesPlugin.tsx | 1 + .../textEditor/plugins/SaveButtonPlugin.tsx | 18 ++++++ libs/shared/lib/insight-sharing/FormAlert.tsx | 63 +++++++++--------- .../shared/lib/insight-sharing/FormReport.tsx | 64 ++++++++++--------- 5 files changed, 128 insertions(+), 77 deletions(-) create mode 100644 libs/shared/lib/components/textEditor/plugins/SaveButtonPlugin.tsx diff --git a/libs/shared/lib/components/textEditor/TextEditor.tsx b/libs/shared/lib/components/textEditor/TextEditor.tsx index 5c8714452..32664ad31 100644 --- a/libs/shared/lib/components/textEditor/TextEditor.tsx +++ b/libs/shared/lib/components/textEditor/TextEditor.tsx @@ -1,43 +1,61 @@ -import { useRef } from 'react'; +import { useEffect, useRef, useCallback } from 'react'; import { LexicalComposer } from '@lexical/react/LexicalComposer'; import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; import { ContentEditable } from '@lexical/react/LexicalContentEditable'; import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; -import { LexicalEditor, EditorState } from 'lexical'; -import { $generateHtmlFromNodes } from '@lexical/html'; -import { MyOnChangePlugin } from './plugins/StateChangePlugin'; +import { SerializedEditorState} from 'lexical'; import { ToolbarPlugin } from './plugins/ToolbarPlugin'; import { PreviewPlugin } from './plugins/PreviewPlugin'; import { InsertVariablesPlugin } from './plugins/InsertVariablesPlugin'; import { ErrorHandler } from './ErrorHandler'; import { Placeholder } from './Placeholder'; import { VariableNode } from './VariableNode'; -import { fontFamily } from 'html2canvas/dist/types/css/property-descriptors/font-family'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { SaveButtonPlugin } from './plugins/SaveButtonPlugin'; type TextEditorProps = { - editorState: EditorState | null; - setEditorState: (value: EditorState) => void; + children: React.ReactNode; + editorState: SerializedEditorState | null; + setEditorState: (value: SerializedEditorState) => void; showToolbar: boolean; placeholder?: string; + handleSave: (elements: SerializedEditorState) => void; }; -export function TextEditor({ editorState, setEditorState, showToolbar, placeholder }: TextEditorProps) { - function onChange(editorState: EditorState, editor: LexicalEditor) { - setEditorState(editorState); - editor.read(() => { - // TODO: +function InitialStateLoader({ editorState }: { editorState: SerializedEditorState | null}) { + const [editor] = useLexicalComposerContext(); + const previousEditorStateRef = useRef<string | null>(null); + + useEffect(() => { + if (!editor || !editorState) return; + queueMicrotask(() => { + editor.setEditorState(editor.parseEditorState(editorState)); }); - } + + }, [editor, editorState]); + + return null; +} + +export function TextEditor({ editorState, showToolbar, placeholder, handleSave, children }: TextEditorProps) { + const contentEditableRef = useRef<HTMLDivElement>(null); + const updateTimeoutRef = useRef<NodeJS.Timeout | null>(null); + const isUpdatingRef = useRef(false); + + useEffect(() => { + return () => { + if (updateTimeoutRef.current) { + clearTimeout(updateTimeoutRef.current); + } + }; + }, []); const initialConfig = { namespace: 'MyEditor', - editorState: editorState ? JSON.stringify(editorState) : null, onError: ErrorHandler, nodes: [VariableNode], }; - const contentEditableRef = useRef<HTMLDivElement>(null); - return ( <LexicalComposer initialConfig={initialConfig}> <div className="editor-toolbar flex items-center bg-secondary-50 rounded mt-4 space-x-2"> @@ -54,8 +72,13 @@ export function TextEditor({ editorState, setEditorState, showToolbar, placehold </div> <div className="preview min-h-24 p-3 hidden"></div> </div> - <MyOnChangePlugin onChange={onChange} /> <InsertVariablesPlugin /> + <InitialStateLoader editorState={editorState} /> + <div className='flex justify-end mt-3'> + {children} + <SaveButtonPlugin + onChange={handleSave}></SaveButtonPlugin> + </div> </LexicalComposer> ); -} +} \ No newline at end of file diff --git a/libs/shared/lib/components/textEditor/plugins/InsertVariablesPlugin.tsx b/libs/shared/lib/components/textEditor/plugins/InsertVariablesPlugin.tsx index c854fe4a2..c996a62e5 100644 --- a/libs/shared/lib/components/textEditor/plugins/InsertVariablesPlugin.tsx +++ b/libs/shared/lib/components/textEditor/plugins/InsertVariablesPlugin.tsx @@ -44,6 +44,7 @@ export const InsertVariablesPlugin = () => { <> {nodeTypes.map((nodeType) => ( <Input + key={nodeType} type="dropdown" label={`${nodeType} variable`} value="" diff --git a/libs/shared/lib/components/textEditor/plugins/SaveButtonPlugin.tsx b/libs/shared/lib/components/textEditor/plugins/SaveButtonPlugin.tsx new file mode 100644 index 000000000..b4c8944ed --- /dev/null +++ b/libs/shared/lib/components/textEditor/plugins/SaveButtonPlugin.tsx @@ -0,0 +1,18 @@ +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { Button } from '../../buttons'; +import { SerializedEditorState } from 'lexical'; + +export function SaveButtonPlugin({ onChange }: { onChange: (elements: SerializedEditorState) => void }) { + const [editor] = useLexicalComposerContext(); + + function handleSave() { + editor.read(() => { + const data = editor.getEditorState(); + const jsonData = data.toJSON(); + + onChange(jsonData); + }) + } + + return <Button label="Save" variantType="primary" className="ml-2" onClick={handleSave} />; +} diff --git a/libs/shared/lib/insight-sharing/FormAlert.tsx b/libs/shared/lib/insight-sharing/FormAlert.tsx index e9ebe20f7..e010d4474 100644 --- a/libs/shared/lib/insight-sharing/FormAlert.tsx +++ b/libs/shared/lib/insight-sharing/FormAlert.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { Accordion, AccordionItem, AccordionHead, AccordionBody } from '../components/accordion'; import { TextEditor } from '../components/textEditor'; import { @@ -10,10 +10,9 @@ import { } from '@graphpolaris/shared/lib/data-access/store/insightSharingSlice'; import { useAppDispatch, useSessionCache } from '@graphpolaris/shared/lib/data-access'; import { Button, Input, LoadingSpinner } from '../components'; -import { EditorState } from 'lexical'; -import { MonitorType } from './components/Sidebar'; import { wsUpdateInsight, wsCreateInsight, wsDeleteInsight } from '@graphpolaris/shared/lib/data-access/broker/wsInsightSharing'; import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; +import { SerializedEditorState } from 'lexical'; type Props = { insight: InsightResponse; @@ -29,27 +28,36 @@ export function FormAlert(props: Props) { const [description, setDescription] = useState(props.insight.description); const [recipients, setRecipients] = useState<string[]>(props.insight.recipients || []); const [recipientInput, setRecipientInput] = useState<string>(''); - const [editorState, setEditorState] = useState<EditorState | null>(null); + const [editorState, setEditorState] = useState<SerializedEditorState | null>(null); useEffect(() => { - setName(props.insight.name); - setDescription(props.insight.description); - setRecipientInput(props.insight.recipients ? props.insight.recipients.join(', ') : ''); - setRecipients(props.insight.recipients || []); - if (props.insight.template) { - try { - const parsedTemplate = JSON.parse(props.insight.template); - setEditorState(parsedTemplate); - } catch (e) { + let isMounted = true; + + if (isMounted) { + setName(props.insight.name); + setDescription(props.insight.description); + setRecipientInput(props.insight.recipients ? props.insight.recipients.join(', ') : ''); + setRecipients(props.insight.recipients || []); + if (props.insight.template) { + try { + const parsedTemplate = JSON.parse(props.insight.template); + setEditorState(parsedTemplate); + } catch (e) { + setEditorState(null); + console.error('Failed to parse template:', e); + } + } else { setEditorState(null); - console.error('Failed to parse template:', e); } - } else { - setEditorState(null); } + + return () => { + isMounted = false; + setEditorState(null); + }; }, [props.insight]); - const handleSave = async () => { + const handleSave = async (element: SerializedEditorState) => { if (!name || name.trim() === '') { dispatch(addError('Name is required')); return; @@ -59,7 +67,7 @@ export function FormAlert(props: Props) { name, description, recipients, - template: JSON.stringify(editorState), + template: JSON.stringify(element), saveStateId: session.currentSaveState || '', type: 'alert' as const, frequency: '', @@ -151,20 +159,17 @@ export function FormAlert(props: Props) { <span className="font-semibold">Alerting text</span> </AccordionHead> <AccordionBody> - <TextEditor - key={props.insight.id || 'new'} - editorState={editorState} - setEditorState={setEditorState} - showToolbar={true} - placeholder="Start typing your alert template..." - /> + <TextEditor + key={`editor-${props.insight.id}`} + editorState={editorState} + setEditorState={setEditorState} + showToolbar={true} + placeholder="Start typing your alert template..." + handleSave={handleSave} + ><Button label="Delete" variantType="secondary" variant="outline" onClick={handleDelete} /></TextEditor> </AccordionBody> </AccordionItem> </Accordion> - <div className="flex justify-end mt-2"> - <Button label="Delete" variantType="secondary" variant="outline" onClick={handleDelete} /> - <Button label="Save" variantType="primary" className="ml-2" onClick={handleSave} /> - </div> </div> ); } diff --git a/libs/shared/lib/insight-sharing/FormReport.tsx b/libs/shared/lib/insight-sharing/FormReport.tsx index de7744595..759737574 100644 --- a/libs/shared/lib/insight-sharing/FormReport.tsx +++ b/libs/shared/lib/insight-sharing/FormReport.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { Button, Input, LoadingSpinner } from '../components'; import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '../components/accordion'; import { TextEditor } from '../components/textEditor'; @@ -9,8 +9,7 @@ import { InsightRequest, } from '@graphpolaris/shared/lib/data-access/store/insightSharingSlice'; import { useAppDispatch, useSessionCache } from '@graphpolaris/shared/lib/data-access'; -import { MonitorType } from './components/Sidebar'; -import { EditorState } from 'lexical'; +import { SerializedEditorState } from 'lexical'; import { wsCreateInsight, wsDeleteInsight, wsUpdateInsight } from '@graphpolaris/shared/lib/data-access/broker/wsInsightSharing'; import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; @@ -29,25 +28,33 @@ export function FormReport(props: Props) { const [recipients, setRecipients] = useState(props.insight.recipients || []); const [recipientInput, setRecipientInput] = useState<string>(''); const [frequency, setFrequency] = useState(props.insight.frequency || 'Daily'); - const [editorState, setEditorState] = useState<EditorState | null>(null); + const [editorState, setEditorState] = useState<SerializedEditorState| null>(null); useEffect(() => { - setName(props.insight.name); - setDescription(props.insight.description); - setRecipientInput(props.insight.recipients ? props.insight.recipients.join(', ') : ''); - setRecipients(props.insight.recipients || []); - setFrequency(props.insight.frequency || 'Daily'); - if (props.insight.template) { - try { - const parsedTemplate = JSON.parse(props.insight.template); - setEditorState(parsedTemplate); - } catch (e) { + let isMounted = true; + + if (isMounted) { + setName(props.insight.name); + setDescription(props.insight.description); + setRecipientInput(props.insight.recipients ? props.insight.recipients.join(', ') : ''); + setRecipients(props.insight.recipients || []); + if (props.insight.template) { + try { + const parsedTemplate = JSON.parse(props.insight.template); + setEditorState(parsedTemplate); + } catch (e) { + setEditorState(null); + console.error('Failed to parse template:', e); + } + } else { setEditorState(null); - console.error('Failed to parse template:', e); } - } else { - setEditorState(null); } + + return () => { + isMounted = false; + setEditorState(null); + }; }, [props.insight]); const handleSetFrequency = (value: string | number) => { @@ -62,7 +69,7 @@ export function FormReport(props: Props) { setDescription(value as string); }; - const handleSave = async () => { + const handleSave = async (elements: SerializedEditorState) => { if (!name || name.trim() === '') { dispatch(addError('Please enter a name for the report.')); return; @@ -73,7 +80,7 @@ export function FormReport(props: Props) { description, recipients, frequency, - template: JSON.stringify(editorState), + template: JSON.stringify(elements), saveStateId: session.currentSaveState || '', type: 'report' as const, }; @@ -177,20 +184,17 @@ export function FormReport(props: Props) { <span className="font-semibold">Email Template</span> </AccordionHead> <AccordionBody> - <TextEditor - key={props.insight.id || 'new'} - editorState={editorState} - setEditorState={setEditorState} - showToolbar={true} - placeholder="Start typing your report template..." - /> + <TextEditor + key={`editor-${props.insight.id}`} + editorState={editorState} + setEditorState={setEditorState} + showToolbar={true} + placeholder="Start typing your alert template..." + handleSave={handleSave} + ><Button label="Delete" variantType="secondary" variant="outline" onClick={handleDelete} /></TextEditor> </AccordionBody> </AccordionItem> </Accordion> - <div className="flex justify-end mt-2"> - <Button label="Delete" variantType="secondary" variant="outline" onClick={handleDelete} /> - <Button label="Save" variantType="primary" className="ml-2" onClick={handleSave} /> - </div> </div> ); } -- GitLab From d6878f4f142ce4c9f8ef51fd4debd814beaf6ab6 Mon Sep 17 00:00:00 2001 From: Samed <sbalcioglu@graphpolaris.com> Date: Mon, 9 Dec 2024 13:51:15 +0100 Subject: [PATCH 9/9] chore: added correct alerts & confirmation msg --- libs/shared/lib/insight-sharing/FormAlert.tsx | 13 ++++++++----- libs/shared/lib/insight-sharing/FormReport.tsx | 13 ++++++++----- .../lib/insight-sharing/components/AddItem.tsx | 6 ++++-- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/libs/shared/lib/insight-sharing/FormAlert.tsx b/libs/shared/lib/insight-sharing/FormAlert.tsx index e010d4474..4f4d2c6af 100644 --- a/libs/shared/lib/insight-sharing/FormAlert.tsx +++ b/libs/shared/lib/insight-sharing/FormAlert.tsx @@ -11,7 +11,7 @@ import { import { useAppDispatch, useSessionCache } from '@graphpolaris/shared/lib/data-access'; import { Button, Input, LoadingSpinner } from '../components'; import { wsUpdateInsight, wsCreateInsight, wsDeleteInsight } from '@graphpolaris/shared/lib/data-access/broker/wsInsightSharing'; -import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; +import { addError, addSuccess } from '@graphpolaris/shared/lib/data-access/store/configSlice'; import { SerializedEditorState } from 'lexical'; type Props = { @@ -80,10 +80,11 @@ export function FormAlert(props: Props) { setLoading(false); if (status === 'success') { dispatch(updateInsight(data)); + dispatch(addSuccess('Alert updated successfully')); console.log('Alert updated successfully'); } else { console.error('Failed to update alert:', data); - alert('Failed to update alert.'); + dispatch(addError('Failed to update alert.')); } }); } else { @@ -91,11 +92,12 @@ export function FormAlert(props: Props) { setLoading(false); if (status === 'success') { dispatch(addInsight(data)); + dispatch(addSuccess('Alert created successfully')); console.log('Alert created successfully'); if (props.setActive) props.setActive(data.id); } else { console.error('Failed to create alert:', data); - alert('Failed to create alert.'); + dispatch(addError('Failed to create alert.')); } }); } @@ -103,7 +105,7 @@ export function FormAlert(props: Props) { const handleDelete = async () => { if (!props.insight.id) { - alert('Cannot delete an alert without an ID.'); + dispatch(addError('Cannot delete an alert without an ID.')); return; } @@ -113,10 +115,11 @@ export function FormAlert(props: Props) { if (status === 'success') { dispatch(deleteInsight({ id: props.insight.id })); if (props.setActive) props.setActive(''); + dispatch(addSuccess('Alert deleted successfully')); console.log('Alert deleted successfully'); } else { console.error('Failed to delete alert:', data); - alert('Failed to delete alert.'); + dispatch(addError('Failed to delete alert')); } }); }; diff --git a/libs/shared/lib/insight-sharing/FormReport.tsx b/libs/shared/lib/insight-sharing/FormReport.tsx index 759737574..e41546b2f 100644 --- a/libs/shared/lib/insight-sharing/FormReport.tsx +++ b/libs/shared/lib/insight-sharing/FormReport.tsx @@ -11,7 +11,7 @@ import { import { useAppDispatch, useSessionCache } from '@graphpolaris/shared/lib/data-access'; import { SerializedEditorState } from 'lexical'; import { wsCreateInsight, wsDeleteInsight, wsUpdateInsight } from '@graphpolaris/shared/lib/data-access/broker/wsInsightSharing'; -import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; +import { addError, addSuccess } from '@graphpolaris/shared/lib/data-access/store/configSlice'; type Props = { insight: InsightResponse; @@ -91,10 +91,11 @@ export function FormReport(props: Props) { setLoading(false); if (status === 'success') { dispatch(updateInsight(data)); + dispatch(addSuccess('Report updated successfully')); console.log('Report updated successfully'); } else { console.error('Failed to update report:', data); - alert('Failed to update report.'); + dispatch(addError('Failed to update alert')); } }); } else { @@ -103,11 +104,12 @@ export function FormReport(props: Props) { setLoading(false); if (status === 'success') { dispatch(updateInsight(data)); + dispatch(addSuccess('Report created successfully')); console.log('Report created successfully'); if (props.setActive) props.setActive(data.id); } else { console.error('Failed to create report:', data); - alert('Failed to create report.'); + dispatch(addError('Failed to create alert')); } }); } @@ -115,7 +117,7 @@ export function FormReport(props: Props) { const handleDelete = async () => { if (!props.insight.id) { - alert('Cannot delete a report without an ID.'); + dispatch(addError('Cannot delete a report without an ID.')); return; } @@ -125,10 +127,11 @@ export function FormReport(props: Props) { if (status === 'success') { dispatch(deleteInsight({ id: props.insight.id })); props.setActive(''); + dispatch(addSuccess('Report deleted successfully')); console.log('Report deleted successfully'); } else { console.error('Failed to delete report:', data); - alert('Failed to delete report.'); + dispatch(addError('Failed to delete report')); } }); }; diff --git a/libs/shared/lib/insight-sharing/components/AddItem.tsx b/libs/shared/lib/insight-sharing/components/AddItem.tsx index fe8f3f173..b207b9ec6 100644 --- a/libs/shared/lib/insight-sharing/components/AddItem.tsx +++ b/libs/shared/lib/insight-sharing/components/AddItem.tsx @@ -4,6 +4,7 @@ import { MonitorType as InsightType, MonitorType } from './Sidebar'; import { useAppDispatch, useSessionCache } from '../../data-access'; import { addInsight } from '../../data-access/store/insightSharingSlice'; import { wsCreateInsight } from '../../data-access/broker/wsInsightSharing'; +import { addError, addSuccess, } from '@graphpolaris/shared/lib/data-access/store/configSlice'; type Props = { setAdding: (val: false | MonitorType) => void; @@ -19,7 +20,7 @@ export function AddItem(props: Props) { const handleSave = async () => { if (!name.trim()) { - alert('Please enter a name.'); + dispatch(addError('Name is required')); return; } @@ -38,9 +39,10 @@ export function AddItem(props: Props) { dispatch(addInsight(data)); props.setActive(data.id); props.setAdding(false); + dispatch(addSuccess('Succesfully created ' + props.type)); } else { console.error('Failed to create insight:', data); - alert('Failed to create new ' + props.type); + dispatch(addError('Failed to create new ' + props.type)) } }); }; -- GitLab