Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • graphpolaris/frontend-v2
  • rijkheere/frontend-v-2-reordering-paoh
2 results
Show changes
Commits on Source (6)
Showing
with 755 additions and 84 deletions
FROM node:19-slim AS base
ENV BUN_HOME="/bun"
ENV PATH="$BUN_HOME:$PATH"
RUN corepack enable
FROM oven/bun:alpine AS base
WORKDIR /app
ARG IMAGE_TAG="dev-no-image-tag"
ENV GRAPHPOLARIS_VERSION=$IMAGE_TAG
FROM base AS install
WORKDIR /app
COPY package.json ./
COPY bun.lockb ./
COPY pnpm-workspace.yaml ./
COPY turbo.json ./
COPY apps/web/package.json ./apps/web/package.json
COPY apps/docs/package.json ./apps/docs/package.json
COPY libs/config/package.json ./libs/config/package.json
COPY libs/shared/package.json ./libs/shared/package.json
RUN bun install --frozen-lockfile
COPY libs/workspace ./libs/workspace
# For some reason we need to first install to then be able to install for prod with frozen lockfile
RUN bun install
RUN bun install --frozen-lockfile --production
FROM base AS build
......@@ -26,18 +26,18 @@ COPY libs /app/libs
COPY --from=install /app /app
# Fixes: FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
ENV NODE_OPTIONS="--max-old-space-size=8192"
ENV BUN_JSC_forceRAMSize="8192"
RUN bun run build
FROM base AS env-build
# Automatically set by build process: https://docs.docker.com/reference/dockerfile/#automatic-platform-args-in-the-global-scope
ARG TARGETARCH
RUN npm install -g pkg
WORKDIR /app
COPY --from=install /app/node_modules /app/node_modules
RUN if [ "$TARGETARCH" = "arm64" ]; then ARCHITECTURE=arm64; else ARCHITECTURE=x64; fi \
&& echo "System architecture: $ARCHITECTURE" \
&& npx pkg ./node_modules/@import-meta-env/cli/bin/import-meta-env.js --target node18-alpine-${ARCHITECTURE} --output import-meta-env-alpine
&& bunx pkg ./node_modules/@import-meta-env/cli/bin/import-meta-env.js --target node18-alpine-${ARCHITECTURE} --output import-meta-env-alpine
FROM nginx:1.25-alpine
WORKDIR /app
......
......@@ -9,7 +9,7 @@ import {
} from '@graphpolaris/shared/lib/data-access';
import { addError, setCurrentTheme } from '@graphpolaris/shared/lib/data-access/store/configSlice';
import { resetGraphQueryResults, queryingBackend } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice';
import { Query2BackendQuery, QueryMultiGraph } from '@graphpolaris/shared/lib/querybuilder';
import { QueryMultiGraph } from '@graphpolaris/shared/lib/querybuilder';
import { Navbar } from '../components/navbar/navbar';
import { FrozenOverlay, Resizable } from '@graphpolaris/shared/lib/components/layout';
import { DashboardAlerts } from '@graphpolaris/shared/lib/data-access/security/dashboardAlerts';
......@@ -36,7 +36,6 @@ export function App(props: App) {
const ml = useML();
const session = useSessionCache();
const dispatch = useAppDispatch();
const queryBuilderSettings = useQuerybuilderSettings();
const [monitoringOpen, setMonitoringOpen] = useState<boolean>(false);
const runQuery = () => {
......@@ -45,7 +44,7 @@ export function App(props: App) {
dispatch(resetGraphQueryResults());
} else {
dispatch(queryingBackend());
wsQueryRequest(Query2BackendQuery(session.currentSaveState, query, queryBuilderSettings, ml, queryBuilderSettings.unionTypes));
wsQueryRequest(ml);
}
}
};
......
......@@ -105,7 +105,8 @@ export const Navbar = () => {
<DropdownItem
value="Log out"
onClick={() => {
location.replace(`${getEnvVariable('GP_AUTH_URL')}/outpost.goauthentik.io/sign_out`);
const current = encodeURIComponent(window.location.href);
location.replace(`${getEnvVariable('GP_AUTH_URL')}if/flow/default-invalidation-flow/?next=${current}`);
}}
/>
{authCache.authorization?.demoUser?.R && (
......
......@@ -38,8 +38,7 @@ export const FEATURE_FLAGS = {
export type FeatureFlagName = keyof typeof FEATURE_FLAGS;
const canViewFeature = (flagId: FeatureFlagName): boolean => {
export const canViewFeature = (flagId: FeatureFlagName): boolean => {
return FEATURE_FLAGS[flagId];
};
export { canViewFeature };
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 | undefined;
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,
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
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 {
......
......@@ -44,6 +44,7 @@ export const InsertVariablesPlugin = () => {
<>
{nodeTypes.map((nodeType) => (
<Input
key={nodeType}
type="dropdown"
label={`${nodeType} variable`}
value=""
......
......@@ -5,8 +5,8 @@ import { $generateHtmlFromNodes } from '@lexical/html';
import { VariableType } from '../VariableNode';
import { useVisualization, useGraphQueryResult } from '@graphpolaris/shared/lib/data-access';
import { Visualizations } from '@graphpolaris/shared/lib/vis/components/VisualizationPanel';
import { Vis1DComponent, type Vis1DProps } from '@graphpolaris/shared/lib/vis/visualizations/vis1D';
import { getPlotData } from '@graphpolaris/shared/lib/vis/visualizations/vis1D/components/CustomChartPlotly';
import { Vis1DComponent, type Vis1DProps, getAttributeValues } from '@graphpolaris/shared/lib/vis/visualizations/vis1D';
import { preparePlotData } from '@graphpolaris/shared/lib/vis/visualizations/vis1D/components/CustomChartPlotly';
import { VisualizationSettingsType } from '@graphpolaris/shared/lib/vis/common';
// @ts-ignore
import { newPlot, toImage } from 'plotly.js/dist/plotly';
......@@ -25,6 +25,8 @@ export function PreviewPlugin({ contentEditable }: { contentEditable: RefObject<
}
const result = useGraphQueryResult();
/*
const getAttributeValues = useCallback(
(settings: Vis1DProps & VisualizationSettingsType, attributeKey: string | number) => {
if (!settings.nodeLabel || !attributeKey) {
......@@ -37,6 +39,7 @@ export function PreviewPlugin({ contentEditable }: { contentEditable: RefObject<
},
[result],
);
*/
const vis = useVisualization();
async function replaceAllAsync(string: string, regexp: RegExp, replacerFunction: CallableFunction) {
......@@ -61,34 +64,57 @@ export function PreviewPlugin({ contentEditable }: { contentEditable: RefObject<
return ` ${value} `;
case VariableType.visualization:
const activeVisualization = vis.openVisualizationArray.find(x => x.name == name) as Vis1DProps & VisualizationSettingsType;
const activeVisualization = vis.openVisualizationArray.find((x) => x.name == name) as Vis1DProps & VisualizationSettingsType;
if (!activeVisualization) {
throw new Error('Tried to render non-existing visualization');
}
let xAxisData = getAttributeValues(result, activeVisualization.selectedEntity, activeVisualization.xAxisLabel!);
let yAxisData: (string | number)[] = [];
let zAxisData: (string | number)[] = [];
if (activeVisualization.yAxisLabel != null) {
yAxisData = getAttributeValues(result, activeVisualization.selectedEntity, activeVisualization.yAxisLabel);
}
if (activeVisualization.zAxisLabel != null) {
zAxisData = getAttributeValues(result, activeVisualization.selectedEntity, activeVisualization.zAxisLabel);
}
//debugger;
const groupBy = activeVisualization.groupData;
const stack = activeVisualization.stack;
const showAxis = true;
const xAxisLabel = '';
const yAxisLabel = '';
const zAxisLabel = '';
const xAxisData = getAttributeValues(activeVisualization, activeVisualization.xAxisLabel!);
const yAxisData = getAttributeValues(activeVisualization, activeVisualization.yAxisLabel!);
debugger;
const plotType = activeVisualization.plotType;
const plotData = getPlotData(xAxisData, plotType, yAxisData);
const plot = await newPlot(document.createElement('div'), plotData, {
const { plotData, layout } = preparePlotData(
xAxisData,
plotType,
yAxisData,
zAxisData,
xAxisLabel,
yAxisLabel,
zAxisLabel,
showAxis,
groupBy,
stack,
);
const layout2 = {
...layout,
width: 600,
height: 400,
title: activeVisualization.title,
font: {
family: 'Inter, sans-serif',
size: 16,
color: '#374151',
},
xaxis: {
title: 'Category',
},
yaxis: {
title: 'Value',
},
});
};
// Generate the plot
const plot = await newPlot(document.createElement('div'), plotData, layout2);
const dataURI = await toImage(plot);
return `<img src="${dataURI}" width="300" height="200" alt="${activeVisualization.title}" />`;
}
......
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} />;
}
......@@ -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"
......
......@@ -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 }) => {
......
......@@ -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';
export type subKeyTypeI =
// Crud
| 'create'
......
import { Broker } from './broker';
import { InsightRequest, InsightResponse, InsightType } from '../store/insightSharingSlice';
type GetInsightsResponse = (data: { reports: InsightResponse[]; alerts: InsightResponse[] }, status: string) => void;
export function wsGetInsights(saveStateId: string, callback?: GetInsightsResponse) {
const internalCallback: GetInsightsResponse = (data, status) => {
if (callback) callback(data, status);
};
Broker.instance().sendMessage(
{
key: 'insight',
subKey: 'getAll',
body: JSON.stringify({ saveStateId: saveStateId })
},
internalCallback
);
}
export function wsCreateInsight(insight: InsightRequest, callback?: Function) {
Broker.instance().sendMessage(
{
key: 'insight',
subKey: 'create',
body: JSON.stringify(insight),
},
(data: any, status: string) => {
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(
id: string,
insight: InsightRequest,
callback?: (data: any, status: string) => void
) {
Broker.instance().sendMessage(
{
key: 'insight',
subKey: 'update',
body: JSON.stringify({ id: id, insight: 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, callback?: Function) {
Broker.instance().sendMessage(
{
key: 'insight',
subKey: 'delete',
body: JSON.stringify({ 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);
};
}
......@@ -4,13 +4,25 @@ import { BackendQueryFormat } from '../../querybuilder';
import { Broker } from './broker';
import { QueryBuilderText } from '../store/querybuilderSlice';
import { GraphQueryResultFromBackendPayload } from '../store/graphQueryResultSlice';
import { ML, MLTypes } from '../store/mlSlice';
export function wsQueryRequest(query: BackendQueryFormat) {
if (query.cached === undefined) query.cached = false;
export function wsQueryRequest(ml: ML) {
const mlEnabled = Object.entries(ml)
.filter(([_,value]) => value.enabled)
.map(([key,_]) => {
return {
type: key as MLTypes,
//parameters: []
}
})
Broker.instance().sendMessage({
key: 'query',
subKey: 'get',
body: query,
body: {
queryID: "0", // TODO: not used yet, but used when multiple queries per save state are supported
ml: mlEnabled,
cached: false
},
});
}
......
......@@ -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
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState } from '../store';
export type InsightType = 'report' | 'alert';
export type InsightRequest = {
name: string;
description: string;
recipients: string[];
frequency: string;
template: string;
saveStateId: string;
type: 'report' | 'alert';
}
export type InsightResponse = {
id: string;
createdAt: string;
updatedAt: string;
} & InsightRequest;
type InsightState = {
insights: InsightResponse[];
}
const initialState: InsightState = {
insights: []
};
const insightSharingSlice = createSlice({
name: 'insightSharing',
initialState,
reducers: {
setInsights(state, action: PayloadAction<InsightResponse[]>) {
state.insights = action.payload;
},
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;
}
state.insights.push(action.payload);
},
updateInsight(state, action: PayloadAction<InsightResponse>) {
const index = state.insights.findIndex((i) => i.id === action.payload.id);
if (index !== -1) {
state.insights[index] = { ...action.payload };
} else {
state.insights.push(action.payload);
}
},
deleteInsight(state, action: PayloadAction<{ id: string; }>) {
const index = state.insights.findIndex((i) => i.id === action.payload.id);
if (index !== -1) {
state.insights.splice(index, 1);
}
},
},
});
export const { setInsights, addInsight, updateInsight, deleteInsight } = insightSharingSlice.actions;
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;
......@@ -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({
......
import { useEffect, useState } from 'react';
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 { wsUpdateInsight, wsCreateInsight, wsDeleteInsight } from '@graphpolaris/shared/lib/data-access/broker/wsInsightSharing';
import { addError, addSuccess } from '@graphpolaris/shared/lib/data-access/store/configSlice';
import { SerializedEditorState } from 'lexical';
type Props = {
insight: InsightResponse;
setActive?: (val: string) => void;
handleDelete: () => void;
};
export function FormAlert(props: Props) {
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<SerializedEditorState | null>(null);
useEffect(() => {
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);
}
}
return () => {
isMounted = false;
setEditorState(null);
};
}, [props.insight]);
const handleSave = async (element: SerializedEditorState) => {
if (!name || name.trim() === '') {
dispatch(addError('Name is required'));
return;
}
const alertData: InsightRequest = {
name,
description,
recipients,
template: JSON.stringify(element),
saveStateId: session.currentSaveState || '',
type: 'alert' as const,
frequency: '',
};
setLoading(true);
if (props.insight.id) {
wsUpdateInsight(props.insight.id, alertData, (data: any, status: string) => {
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);
dispatch(addError('Failed to update alert.'));
}
});
} else {
wsCreateInsight(alertData, (data: any, status: string) => {
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);
dispatch(addError('Failed to create alert.'));
}
});
}
};
const handleDelete = async () => {
if (!props.insight.id) {
dispatch(addError('Cannot delete an alert without an ID.'));
return;
}
setLoading(true);
wsDeleteInsight(props.insight.id, (data: any, status: string) => {
setLoading(false);
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);
dispatch(addError('Failed to delete alert'));
}
});
};
if (loading) {
return <LoadingSpinner />;
}
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" />
<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"
/>
</div>
</AccordionBody>
</AccordionItem>
<AccordionItem className="pt-2 pb-4">
<AccordionHead showArrow={false}>
<span className="font-semibold">Alerting text</span>
</AccordionHead>
<AccordionBody>
<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>
);
}
import { 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 '@graphpolaris/shared/lib/data-access/store/insightSharingSlice';
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, addSuccess } from '@graphpolaris/shared/lib/data-access/store/configSlice';
type Props = {
insight: InsightResponse;
setActive: (val: string) => void;
handleDelete: () => void;
};
export function FormReport(props: Props) {
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(props.insight.recipients || []);
const [recipientInput, setRecipientInput] = useState<string>('');
const [frequency, setFrequency] = useState(props.insight.frequency || 'Daily');
const [editorState, setEditorState] = useState<SerializedEditorState| null>(null);
useEffect(() => {
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);
}
}
return () => {
isMounted = false;
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 (elements: SerializedEditorState) => {
if (!name || name.trim() === '') {
dispatch(addError('Please enter a name for the report.'));
return;
}
const reportData: InsightRequest = {
name,
description,
recipients,
frequency,
template: JSON.stringify(elements),
saveStateId: session.currentSaveState || '',
type: 'report' as const,
};
setLoading(true);
if (props.insight.id) {
wsUpdateInsight(props.insight.id, reportData, (data: any, status: string) => {
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);
dispatch(addError('Failed to update alert'));
}
});
} else {
wsCreateInsight(reportData, (data: any, status: string) => {
debugger;
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);
dispatch(addError('Failed to create alert'));
}
});
}
};
const handleDelete = async () => {
if (!props.insight.id) {
dispatch(addError('Cannot delete a report without an ID.'));
return;
}
setLoading(true);
wsDeleteInsight(props.insight.id, (data: any, status: string) => {
setLoading(false);
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);
dispatch(addError('Failed to delete report'));
}
});
};
return loading ? (
<LoadingSpinner />
) : (
<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"
/>
</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={`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>
);
}
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;
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 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, name: string) => {
setActive(name);
setActiveCategory(category);
const handleDelete = () => {
if (!selectedInsight) return;
dispatch(deleteInsight({ id: selectedInsight.id }));
wsDeleteInsight(selectedInsight.id);
setActive('');
};
return (
......@@ -30,20 +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
reports={reports}
alerts={alerts}
changeActive={handleChangeActive}
setAdding={setAdding}
setActiveCategory={setActiveCategory}
/>
<SettingsPanel
activeCategory={activeCategory}
active={active}
adding={adding}
setAdding={setAdding}
setActiveCategory={setActiveCategory}
/>
<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>
......