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 FROM oven/bun:alpine AS base
ENV BUN_HOME="/bun"
ENV PATH="$BUN_HOME:$PATH"
RUN corepack enable
WORKDIR /app WORKDIR /app
ARG IMAGE_TAG="dev-no-image-tag" ARG IMAGE_TAG="dev-no-image-tag"
ENV GRAPHPOLARIS_VERSION=$IMAGE_TAG ENV GRAPHPOLARIS_VERSION=$IMAGE_TAG
FROM base AS install FROM base AS install
WORKDIR /app
COPY package.json ./ COPY package.json ./
COPY bun.lockb ./ COPY bun.lockb ./
COPY pnpm-workspace.yaml ./
COPY turbo.json ./ COPY turbo.json ./
COPY apps/web/package.json ./apps/web/package.json COPY apps/web/package.json ./apps/web/package.json
COPY apps/docs/package.json ./apps/docs/package.json COPY apps/docs/package.json ./apps/docs/package.json
COPY libs/config/package.json ./libs/config/package.json COPY libs/config/package.json ./libs/config/package.json
COPY libs/shared/package.json ./libs/shared/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 FROM base AS build
...@@ -26,18 +26,18 @@ COPY libs /app/libs ...@@ -26,18 +26,18 @@ COPY libs /app/libs
COPY --from=install /app /app COPY --from=install /app /app
# Fixes: FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory # 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 NODE_OPTIONS="--max-old-space-size=8192"
ENV BUN_JSC_forceRAMSize="8192"
RUN bun run build RUN bun run build
FROM base AS env-build FROM base AS env-build
# Automatically set by build process: https://docs.docker.com/reference/dockerfile/#automatic-platform-args-in-the-global-scope # Automatically set by build process: https://docs.docker.com/reference/dockerfile/#automatic-platform-args-in-the-global-scope
ARG TARGETARCH ARG TARGETARCH
RUN npm install -g pkg
WORKDIR /app WORKDIR /app
COPY --from=install /app/node_modules /app/node_modules COPY --from=install /app/node_modules /app/node_modules
RUN if [ "$TARGETARCH" = "arm64" ]; then ARCHITECTURE=arm64; else ARCHITECTURE=x64; fi \ RUN if [ "$TARGETARCH" = "arm64" ]; then ARCHITECTURE=arm64; else ARCHITECTURE=x64; fi \
&& echo "System architecture: $ARCHITECTURE" \ && 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 FROM nginx:1.25-alpine
WORKDIR /app WORKDIR /app
......
...@@ -9,7 +9,7 @@ import { ...@@ -9,7 +9,7 @@ import {
} from '@graphpolaris/shared/lib/data-access'; } from '@graphpolaris/shared/lib/data-access';
import { addError, setCurrentTheme } from '@graphpolaris/shared/lib/data-access/store/configSlice'; import { addError, setCurrentTheme } from '@graphpolaris/shared/lib/data-access/store/configSlice';
import { resetGraphQueryResults, queryingBackend } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; 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 { Navbar } from '../components/navbar/navbar';
import { FrozenOverlay, Resizable } from '@graphpolaris/shared/lib/components/layout'; import { FrozenOverlay, Resizable } from '@graphpolaris/shared/lib/components/layout';
import { DashboardAlerts } from '@graphpolaris/shared/lib/data-access/security/dashboardAlerts'; import { DashboardAlerts } from '@graphpolaris/shared/lib/data-access/security/dashboardAlerts';
...@@ -36,7 +36,6 @@ export function App(props: App) { ...@@ -36,7 +36,6 @@ export function App(props: App) {
const ml = useML(); const ml = useML();
const session = useSessionCache(); const session = useSessionCache();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const queryBuilderSettings = useQuerybuilderSettings();
const [monitoringOpen, setMonitoringOpen] = useState<boolean>(false); const [monitoringOpen, setMonitoringOpen] = useState<boolean>(false);
const runQuery = () => { const runQuery = () => {
...@@ -45,7 +44,7 @@ export function App(props: App) { ...@@ -45,7 +44,7 @@ export function App(props: App) {
dispatch(resetGraphQueryResults()); dispatch(resetGraphQueryResults());
} else { } else {
dispatch(queryingBackend()); dispatch(queryingBackend());
wsQueryRequest(Query2BackendQuery(session.currentSaveState, query, queryBuilderSettings, ml, queryBuilderSettings.unionTypes)); wsQueryRequest(ml);
} }
} }
}; };
......
...@@ -105,7 +105,8 @@ export const Navbar = () => { ...@@ -105,7 +105,8 @@ export const Navbar = () => {
<DropdownItem <DropdownItem
value="Log out" value="Log out"
onClick={() => { 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 && ( {authCache.authorization?.demoUser?.R && (
......
...@@ -38,8 +38,7 @@ export const FEATURE_FLAGS = { ...@@ -38,8 +38,7 @@ export const FEATURE_FLAGS = {
export type FeatureFlagName = keyof typeof FEATURE_FLAGS; export type FeatureFlagName = keyof typeof FEATURE_FLAGS;
const canViewFeature = (flagId: FeatureFlagName): boolean => { export const canViewFeature = (flagId: FeatureFlagName): boolean => {
return FEATURE_FLAGS[flagId]; return FEATURE_FLAGS[flagId];
}; };
export { canViewFeature };
import { useRef } from 'react'; import { useEffect, useRef, useCallback } from 'react';
import { LexicalComposer } from '@lexical/react/LexicalComposer'; import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable'; import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
import { LexicalEditor, EditorState } from 'lexical'; import { SerializedEditorState} from 'lexical';
import { $generateHtmlFromNodes } from '@lexical/html';
import { MyOnChangePlugin } from './plugins/StateChangePlugin';
import { ToolbarPlugin } from './plugins/ToolbarPlugin'; import { ToolbarPlugin } from './plugins/ToolbarPlugin';
import { PreviewPlugin } from './plugins/PreviewPlugin'; import { PreviewPlugin } from './plugins/PreviewPlugin';
import { InsertVariablesPlugin } from './plugins/InsertVariablesPlugin'; import { InsertVariablesPlugin } from './plugins/InsertVariablesPlugin';
import { ErrorHandler } from './ErrorHandler'; import { ErrorHandler } from './ErrorHandler';
import { Placeholder } from './Placeholder'; import { Placeholder } from './Placeholder';
import { VariableNode } from './VariableNode'; 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 = { type TextEditorProps = {
editorState: EditorState | undefined; children: React.ReactNode;
setEditorState: (value: EditorState) => void; editorState: SerializedEditorState | null;
setEditorState: (value: SerializedEditorState) => void;
showToolbar: boolean; showToolbar: boolean;
placeholder?: string; placeholder?: string;
handleSave: (elements: SerializedEditorState) => void;
}; };
export function TextEditor({ editorState, setEditorState, showToolbar, placeholder }: TextEditorProps) { function InitialStateLoader({ editorState }: { editorState: SerializedEditorState | null}) {
function onChange(editorState: EditorState, editor: LexicalEditor) { const [editor] = useLexicalComposerContext();
setEditorState(editorState); const previousEditorStateRef = useRef<string | null>(null);
editor.read(() => {
// TODO: 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 = { const initialConfig = {
namespace: 'MyEditor', namespace: 'MyEditor',
editorState: editorState,
onError: ErrorHandler, onError: ErrorHandler,
nodes: [VariableNode], nodes: [VariableNode],
}; };
const contentEditableRef = useRef<HTMLDivElement>(null);
return ( return (
<LexicalComposer initialConfig={initialConfig}> <LexicalComposer initialConfig={initialConfig}>
<div className="editor-toolbar flex items-center bg-secondary-50 rounded mt-4 space-x-2"> <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 ...@@ -54,8 +72,13 @@ export function TextEditor({ editorState, setEditorState, showToolbar, placehold
</div> </div>
<div className="preview min-h-24 p-3 hidden"></div> <div className="preview min-h-24 p-3 hidden"></div>
</div> </div>
<MyOnChangePlugin onChange={onChange} />
<InsertVariablesPlugin /> <InsertVariablesPlugin />
<InitialStateLoader editorState={editorState} />
<div className='flex justify-end mt-3'>
{children}
<SaveButtonPlugin
onChange={handleSave}></SaveButtonPlugin>
</div>
</LexicalComposer> </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'; import { DecoratorNode, EditorConfig } from 'lexical';
export enum VariableType { export enum VariableType {
...@@ -6,6 +6,14 @@ export enum VariableType { ...@@ -6,6 +6,14 @@ export enum VariableType {
visualization = 'visualization', visualization = 'visualization',
} }
export type SerializedVariableNode = Spread<
{
name: string,
variableType: VariableType
},
SerializedLexicalNode
>;
export class VariableNode extends DecoratorNode<JSX.Element> { export class VariableNode extends DecoratorNode<JSX.Element> {
__name: string; __name: string;
__variableType: VariableType; __variableType: VariableType;
...@@ -39,6 +47,22 @@ export class VariableNode extends DecoratorNode<JSX.Element> { ...@@ -39,6 +47,22 @@ export class VariableNode extends DecoratorNode<JSX.Element> {
return `{{ ${self.__variableType}:${self.__name} }}`; 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 // View
createDOM(config: EditorConfig): HTMLElement { createDOM(config: EditorConfig): HTMLElement {
......
...@@ -44,6 +44,7 @@ export const InsertVariablesPlugin = () => { ...@@ -44,6 +44,7 @@ export const InsertVariablesPlugin = () => {
<> <>
{nodeTypes.map((nodeType) => ( {nodeTypes.map((nodeType) => (
<Input <Input
key={nodeType}
type="dropdown" type="dropdown"
label={`${nodeType} variable`} label={`${nodeType} variable`}
value="" value=""
......
...@@ -5,8 +5,8 @@ import { $generateHtmlFromNodes } from '@lexical/html'; ...@@ -5,8 +5,8 @@ import { $generateHtmlFromNodes } from '@lexical/html';
import { VariableType } from '../VariableNode'; import { VariableType } from '../VariableNode';
import { useVisualization, useGraphQueryResult } from '@graphpolaris/shared/lib/data-access'; import { useVisualization, useGraphQueryResult } from '@graphpolaris/shared/lib/data-access';
import { Visualizations } from '@graphpolaris/shared/lib/vis/components/VisualizationPanel'; import { Visualizations } from '@graphpolaris/shared/lib/vis/components/VisualizationPanel';
import { Vis1DComponent, type Vis1DProps } from '@graphpolaris/shared/lib/vis/visualizations/vis1D'; import { Vis1DComponent, type Vis1DProps, getAttributeValues } from '@graphpolaris/shared/lib/vis/visualizations/vis1D';
import { getPlotData } from '@graphpolaris/shared/lib/vis/visualizations/vis1D/components/CustomChartPlotly'; import { preparePlotData } from '@graphpolaris/shared/lib/vis/visualizations/vis1D/components/CustomChartPlotly';
import { VisualizationSettingsType } from '@graphpolaris/shared/lib/vis/common'; import { VisualizationSettingsType } from '@graphpolaris/shared/lib/vis/common';
// @ts-ignore // @ts-ignore
import { newPlot, toImage } from 'plotly.js/dist/plotly'; import { newPlot, toImage } from 'plotly.js/dist/plotly';
...@@ -25,6 +25,8 @@ export function PreviewPlugin({ contentEditable }: { contentEditable: RefObject< ...@@ -25,6 +25,8 @@ export function PreviewPlugin({ contentEditable }: { contentEditable: RefObject<
} }
const result = useGraphQueryResult(); const result = useGraphQueryResult();
/*
const getAttributeValues = useCallback( const getAttributeValues = useCallback(
(settings: Vis1DProps & VisualizationSettingsType, attributeKey: string | number) => { (settings: Vis1DProps & VisualizationSettingsType, attributeKey: string | number) => {
if (!settings.nodeLabel || !attributeKey) { if (!settings.nodeLabel || !attributeKey) {
...@@ -37,6 +39,7 @@ export function PreviewPlugin({ contentEditable }: { contentEditable: RefObject< ...@@ -37,6 +39,7 @@ export function PreviewPlugin({ contentEditable }: { contentEditable: RefObject<
}, },
[result], [result],
); );
*/
const vis = useVisualization(); const vis = useVisualization();
async function replaceAllAsync(string: string, regexp: RegExp, replacerFunction: CallableFunction) { async function replaceAllAsync(string: string, regexp: RegExp, replacerFunction: CallableFunction) {
...@@ -61,34 +64,57 @@ export function PreviewPlugin({ contentEditable }: { contentEditable: RefObject< ...@@ -61,34 +64,57 @@ export function PreviewPlugin({ contentEditable }: { contentEditable: RefObject<
return ` ${value} `; return ` ${value} `;
case VariableType.visualization: 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) { if (!activeVisualization) {
throw new Error('Tried to render non-existing visualization'); 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 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, width: 600,
height: 400, height: 400,
title: activeVisualization.title, title: activeVisualization.title,
font: { };
family: 'Inter, sans-serif',
size: 16, // Generate the plot
color: '#374151', const plot = await newPlot(document.createElement('div'), plotData, layout2);
},
xaxis: {
title: 'Category',
},
yaxis: {
title: 'Value',
},
});
const dataURI = await toImage(plot); const dataURI = await toImage(plot);
return `<img src="${dataURI}" width="300" height="200" alt="${activeVisualization.title}" />`; 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() { ...@@ -19,8 +19,9 @@ export function ToolbarPlugin() {
}; };
return [ return [
<div style={{ flex: '1 1 auto' }}></div>, <div key= "spacer" style={{ flex: '1 1 auto' }}></div>,
<Button <Button
key = "bold"
className="my-2" className="my-2"
variantType="secondary" variantType="secondary"
variant="ghost" variant="ghost"
...@@ -29,6 +30,7 @@ export function ToolbarPlugin() { ...@@ -29,6 +30,7 @@ export function ToolbarPlugin() {
onClick={formatBold} onClick={formatBold}
/>, />,
<Button <Button
key="italic"
className="my-2" className="my-2"
variantType="secondary" variantType="secondary"
variant="ghost" variant="ghost"
...@@ -37,6 +39,7 @@ export function ToolbarPlugin() { ...@@ -37,6 +39,7 @@ export function ToolbarPlugin() {
onClick={formatItalic} onClick={formatItalic}
/>, />,
<Button <Button
key="underline"
className="my-2 me-2" className="my-2 me-2"
variantType="secondary" variantType="secondary"
variant="ghost" variant="ghost"
......
...@@ -43,14 +43,14 @@ import { ...@@ -43,14 +43,14 @@ import {
setFetchingSaveStates, setFetchingSaveStates,
setStateAuthorization, setStateAuthorization,
} from '../store/sessionSlice'; } from '../store/sessionSlice';
import { URLParams, getParam, deleteParam } from './url'; import { URLParams, getParam } from './url';
import { VisState, setVisualizationState } from '../store/visualizationSlice'; import { VisState, setVisualizationState } from '../store/visualizationSlice';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { setSchemaAttributeDimensions, setSchemaAttributeInformation, setSchemaLoading } from '../store/schemaSlice'; import { setSchemaAttributeDimensions, setSchemaAttributeInformation, setSchemaLoading } from '../store/schemaSlice';
import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice'; import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice';
import { unSelect } from '../store/interactionSlice'; import { unSelect } from '../store/interactionSlice';
import { SchemaGraphStats } from '../../schema'; import { SchemaGraphStats } from '../../schema';
import { wsUserGetPolicy, wsUserPolicyCheck } from '../broker/wsUser'; import { wsUserGetPolicy } from '../broker/wsUser';
import { authorized } from '../store/authSlice'; import { authorized } from '../store/authSlice';
export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }) => { export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }) => {
......
...@@ -19,7 +19,7 @@ type QueryOrchestratorMessage = { ...@@ -19,7 +19,7 @@ type QueryOrchestratorMessage = {
queryID: string; queryID: string;
}; };
export type keyTypeI = 'broadcastState' | 'dbConnection' | 'schema' | 'query' | 'state' | 'user'; export type keyTypeI = 'broadcastState' | 'dbConnection' | 'schema' | 'query' | 'state' | 'user' | 'insight';
export type subKeyTypeI = export type subKeyTypeI =
// Crud // Crud
| 'create' | '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'; ...@@ -4,13 +4,25 @@ import { BackendQueryFormat } from '../../querybuilder';
import { Broker } from './broker'; import { Broker } from './broker';
import { QueryBuilderText } from '../store/querybuilderSlice'; import { QueryBuilderText } from '../store/querybuilderSlice';
import { GraphQueryResultFromBackendPayload } from '../store/graphQueryResultSlice'; import { GraphQueryResultFromBackendPayload } from '../store/graphQueryResultSlice';
import { ML, MLTypes } from '../store/mlSlice';
export function wsQueryRequest(query: BackendQueryFormat) { export function wsQueryRequest(ml: ML) {
if (query.cached === undefined) query.cached = false; const mlEnabled = Object.entries(ml)
.filter(([_,value]) => value.enabled)
.map(([key,_]) => {
return {
type: key as MLTypes,
//parameters: []
}
})
Broker.instance().sendMessage({ Broker.instance().sendMessage({
key: 'query', key: 'query',
subKey: 'get', 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'; ...@@ -44,6 +44,7 @@ import { PolicyUsersState, selectPolicyState } from './authorizationUsersSlice';
import { PolicyResourcesState, selectResourcesPolicyState } from './authorizationResourcesSlice'; import { PolicyResourcesState, selectResourcesPolicyState } from './authorizationResourcesSlice';
import { SaveStateAuthorizationHeaders, SaveStateI } from '..'; import { SaveStateAuthorizationHeaders, SaveStateI } from '..';
import { GraphStatistics } from '../../statistics'; import { GraphStatistics } from '../../statistics';
import { InsightResponse, selectInsights } from './insightSharingSlice';
// Use throughout your app instead of plain `useDispatch` and `useSelector` // Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch; export const useAppDispatch: () => AppDispatch = useDispatch;
...@@ -100,3 +101,6 @@ export const useUsersPolicy: () => PolicyUsersState = () => useAppSelector(selec ...@@ -100,3 +101,6 @@ export const useUsersPolicy: () => PolicyUsersState = () => useAppSelector(selec
// Authorization Resources Slice // Authorization Resources Slice
export const useResourcesPolicy: () => PolicyResourcesState = () => useAppSelector(selectResourcesPolicyState); 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'; ...@@ -11,6 +11,7 @@ import visualizationSlice from './visualizationSlice';
import interactionSlice from './interactionSlice'; import interactionSlice from './interactionSlice';
import policyUsersSlice from './authorizationUsersSlice'; import policyUsersSlice from './authorizationUsersSlice';
import policyPermissionSlice from './authorizationResourcesSlice'; import policyPermissionSlice from './authorizationResourcesSlice';
import insightSharingSlice from './insightSharingSlice';
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
...@@ -26,6 +27,7 @@ export const store = configureStore({ ...@@ -26,6 +27,7 @@ export const store = configureStore({
visualize: visualizationSlice, visualize: visualizationSlice,
policyUsers: policyUsersSlice, policyUsers: policyUsersSlice,
policyResources: policyPermissionSlice, policyResources: policyPermissionSlice,
insightSharing: insightSharingSlice,
}, },
middleware: (getDefaultMiddleware) => middleware: (getDefaultMiddleware) =>
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 { Dialog, DialogContent } from '../components';
import { SettingsPanel } from './SettingsPanel';
import { MonitorType, Sidebar } from './components/Sidebar'; 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 = { type Props = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
}; };
const reports = ['Sequence 1', 'Sequence 2'];
const alerts = ['Potential New Incident', 'Potential New Info'];
export function InsightDialog(props: 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 [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) => { const handleDelete = () => {
setActive(name); if (!selectedInsight) return;
setActiveCategory(category);
dispatch(deleteInsight({ id: selectedInsight.id }));
wsDeleteInsight(selectedInsight.id);
setActive('');
}; };
return ( return (
...@@ -30,20 +40,21 @@ export function InsightDialog(props: Props) { ...@@ -30,20 +40,21 @@ export function InsightDialog(props: Props) {
> >
<DialogContent className="w-5/6 h-5/6 rounded-none py-0 px-0"> <DialogContent className="w-5/6 h-5/6 rounded-none py-0 px-0">
<div className="flex w-full h-full"> <div className="flex w-full h-full">
<Sidebar <div className="w-1/4 border-r overflow-auto flex flex-col h-full">
reports={reports} <Sidebar setAdding={setAdding} setActive={setActive} />
alerts={alerts} </div>
changeActive={handleChangeActive}
setAdding={setAdding} <div className="w-3/4 p-8">
setActiveCategory={setActiveCategory} {adding ? (
/> <AddItem setAdding={setAdding} setActive={setActive} type={adding} />
<SettingsPanel ) : !selectedInsight ? (
activeCategory={activeCategory} <StartScreen setAdding={setAdding} />
active={active} ) : selectedInsight.type === 'report' ? (
adding={adding} <FormReport insight={selectedInsight} setActive={setActive} handleDelete={handleDelete} />
setAdding={setAdding} ) : (
setActiveCategory={setActiveCategory} <FormAlert insight={selectedInsight} setActive={setActive} handleDelete={handleDelete} />
/> )}
</div>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
......