Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Debug server #70

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
34 changes: 34 additions & 0 deletions packages/yii-dev-panel-sdk/src/Component/LogEntry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {FilePresent} from '@mui/icons-material';
import {Alert, AlertTitle, Link} from '@mui/material';
import Box from '@mui/material/Box';
import {JsonRenderer} from '@yiisoft/yii-dev-panel-sdk/Component/JsonRenderer';
import {phpLoggerLevelToAlertColor} from '@yiisoft/yii-dev-panel-sdk/Helper/collorMapper';
import {parseFilePathWithLineAnchor} from '@yiisoft/yii-dev-panel-sdk/Helper/filePathParser';
import {PhpLoggerLevel} from '@yiisoft/yii-dev-panel-sdk/Types/logger';

export type LogEntry = {
context: object;
level: PhpLoggerLevel;
line?: string;
message: string;
time: number;
};
type LogEntryProps = {
entry: LogEntry;
};
export const LogEntry = ({entry}: LogEntryProps) => {
return (
<Alert variant="outlined" severity={phpLoggerLevelToAlertColor(entry.level)} icon={false}>
<AlertTitle>{entry.message}</AlertTitle>
<Box>
<JsonRenderer value={entry.context} depth={2} />
{'line' in entry && (
<Link href={`/inspector/files?path=${parseFilePathWithLineAnchor(entry.line)}`}>
{entry.line}
<FilePresent fontSize="small" />
</Link>
)}
</Box>
</Alert>
);
};
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
// TODO support custom events and decode payload to object
class ServerSentEvents {
export class ServerSentEvents {
private eventSource: EventSource = null;
private listeners: ((event: MessageEvent) => void)[] = [];
constructor(private url: string) {}

subscribe(subscriber: (event: MessageEvent) => void) {
if (this.eventSource === null || this.eventSource.readyState === EventSource.CLOSED) {
this.eventSource = new EventSource(this.url);
this.eventSource.onopen = () => {
console.log('ServerSentEvents: connected');
};
this.eventSource.onerror = () => {
console.log('ServerSentEvents: error', this.listeners);
this.listeners.forEach((listener) => {
this.eventSource.addEventListener('message', listener);
});
};
}
this.listeners.push(subscriber);
this.eventSource.addEventListener('message', this.handle.bind(this));
@@ -32,5 +41,4 @@ class ServerSentEvents {
}
}

export const createServerSentEventsObserver = (backendUrl: string) =>
new ServerSentEvents(backendUrl + '/debug/api/event-stream');
export const createServerSentEventsObserver = (url: string) => new ServerSentEvents(url);
41 changes: 41 additions & 0 deletions packages/yii-dev-panel-sdk/src/Component/useDevServerEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {createServerSentEventsObserver} from '@yiisoft/yii-dev-panel-sdk/Component/ServerSentEventsObserver';
import {useEffect, useRef} from 'react';

type DebugUpdatedType = {
type: EventTypesEnum.DebugUpdated;
payload: {};
};

export enum EventTypesEnum {
DebugUpdated = 'debug-updated',
}

export type EventTypes = DebugUpdatedType;

export const useServerSentEvents = (
backendUrl: string,
onMessage: (event: MessageEvent<EventTypes>) => void,
subscribe = true,
) => {
const prevOnMessage = useRef(onMessage);
const ServerSentEventsObserverRef = useRef(createServerSentEventsObserver(backendUrl + '/debug/api/dev'));

useEffect(() => {
if (prevOnMessage.current) {
ServerSentEventsObserverRef.current.unsubscribe(prevOnMessage.current);
}
if (!subscribe) {
return () => {
ServerSentEventsObserverRef.current.unsubscribe(onMessage);
};
}

ServerSentEventsObserverRef.current.subscribe(onMessage);
prevOnMessage.current = onMessage;

return () => {
ServerSentEventsObserverRef.current.unsubscribe(onMessage);
ServerSentEventsObserverRef.current.close();
};
}, [onMessage, subscribe]);
};
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ export const useServerSentEvents = (
subscribe = true,
) => {
const prevOnMessage = useRef(onMessage);
const ServerSentEventsObserverRef = useRef(createServerSentEventsObserver(backendUrl));
const ServerSentEventsObserverRef = useRef(createServerSentEventsObserver(backendUrl + '/debug/api/event-stream'));

useEffect(() => {
if (prevOnMessage.current) {
19 changes: 19 additions & 0 deletions packages/yii-dev-panel-sdk/src/Helper/collorMapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {AlertColor} from '@mui/material';
import {PhpLoggerLevel} from '@yiisoft/yii-dev-panel-sdk/Types/logger';

export const phpLoggerLevelToAlertColor = (status: PhpLoggerLevel): AlertColor => {
switch (status) {
case 'emergency':
case 'alert':
case 'critical':
case 'error':
return 'error';
case 'warning':
return 'warning';
case 'notice':
case 'info':
case 'debug':
return 'info';
}
return 'success';
};
4 changes: 2 additions & 2 deletions packages/yii-dev-panel-sdk/src/Helper/formatDate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {format, fromUnixTime} from 'date-fns';

export function formatDate(unixTimeStamp: number) {
return format(fromUnixTime(unixTimeStamp), 'do MMM HH:mm:ss');
return format(unixTimeStamp, 'do MMM HH:mm:ss');
}

export function formatMicrotime(unixTimeStamp: number) {
@@ -12,7 +12,7 @@ export function formatMicrotime(unixTimeStamp: number) {
}
export function formatWithMicrotime(unixTimeStamp: number, dateFormat: string) {
const float = String(unixTimeStamp).split('.');
return format(fromUnixTime(+float[0]), dateFormat) + (float.length === 2 ? '.' + float[1].padEnd(6, '0') : '');
return format(unixTimeStamp, dateFormat) + (float.length === 2 ? '.' + float[1].padEnd(6, '0') : '');
}

export function formatMillisecondsAsDuration(milliseconds: number) {
1 change: 1 addition & 0 deletions packages/yii-dev-panel-sdk/src/Types/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type PhpLoggerLevel = 'emergency' | 'alert' | 'critical' | 'error' | 'warning' | 'notice' | 'info' | 'debug';
6 changes: 5 additions & 1 deletion packages/yii-dev-panel/src/Application/Component/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {ContentCut, GitHub, Refresh} from '@mui/icons-material';
import AdbIcon from '@mui/icons-material/Adb';
import InfoIcon from '@mui/icons-material/Info';
import {
CssBaseline,
IconButton,
@@ -179,9 +180,12 @@ export const Layout = React.memo(({children}: React.PropsWithChildren) => {
})}
</Box>
<div>
<IconButton size="large" onClick={handleMenu} onMouseOver={handleMenu} color="inherit">
<IconButton size="large" href={'/debug-server'} color="inherit">
<AdbIcon />
</IconButton>
<IconButton size="large" onClick={handleMenu} onMouseOver={handleMenu} color="inherit">
<InfoIcon />
</IconButton>
<Menu
keepMounted
open={Boolean(anchorEl)}
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
import {FilePresent} from '@mui/icons-material';
import {Alert, AlertTitle, Link} from '@mui/material';
import Box from '@mui/material/Box';
import {parseFilePathWithLineAnchor} from '@yiisoft/yii-dev-panel-sdk/Helper/filePathParser';
import {JsonRenderer} from '@yiisoft/yii-dev-panel/Module/Debug/Component/JsonRenderer';
import {LogEntry} from '@yiisoft/yii-dev-panel-sdk/Component/LogEntry';

type Level = 'error' | 'info' | 'debug';
type LogEntry = {
context: object;
level: Level;
line: string;
message: string;
time: number;
};
type LogPanelProps = {
data: LogEntry[];
};
@@ -22,18 +10,7 @@ export const LogPanel = ({data}: LogPanelProps) => {
{!data || data.length === 0 ? (
<>Nothing here</>
) : (
data.map((entry, index) => (
<Alert key={index} variant="outlined" severity="success" icon={false}>
<AlertTitle>{entry.message}</AlertTitle>
<Box>
<JsonRenderer value={entry.context} depth={2} />
<Link href={`/inspector/files?path=${parseFilePathWithLineAnchor(entry.line)}`}>
{entry.line}
<FilePresent fontSize="small" />
</Link>
</Box>
</Alert>
))
data.map((entry, index) => <LogEntry key={index} entry={entry} />)
)}
</>
);
29 changes: 29 additions & 0 deletions packages/yii-dev-panel/src/Module/DevServer/Context/Context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {createSlice} from '@reduxjs/toolkit';
import {useSelector} from 'react-redux';

export const framesSlice = createSlice({
name: 'store.frames2',
initialState: {
frames: {} as Record<string, string>,
},
reducers: {
addFrame: (state, action) => {
state.frames = {
...state.frames,
[action.payload]: action.payload,
};
},
updateFrame: (state, action) => {
state.frames = action.payload;
},
deleteFrame: (state, action) => {
const frames = Object.entries(state.frames).filter(([name, url]) => name != action.payload);
state.frames = Object.fromEntries(frames);
},
},
});

export const {addFrame, updateFrame, deleteFrame} = framesSlice.actions;

type State = {[framesSlice.name]: ReturnType<typeof framesSlice.getInitialState>};
export const useDevServerEntries = () => useSelector((state: State) => state[framesSlice.name].frames);
150 changes: 150 additions & 0 deletions packages/yii-dev-panel/src/Module/DevServer/Pages/Layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import DataObjectIcon from '@mui/icons-material/DataObject';
import TableRowsIcon from '@mui/icons-material/TableRows';
import {
Badge,
Button,
List,
ListItem,
ListItemIcon,
ListItemText,
Stack,
ToggleButton,
ToggleButtonGroup,
ToggleButtonProps,
Tooltip,
} from '@mui/material';

import Avatar from '@mui/material/Avatar';
import {JsonRenderer} from '@yiisoft/yii-dev-panel-sdk/Component/JsonRenderer';
import {LogEntry} from '@yiisoft/yii-dev-panel-sdk/Component/LogEntry';
import {formatDate} from '@yiisoft/yii-dev-panel-sdk/Helper/formatDate';
import {forwardRef, MouseEvent, useCallback, useState} from 'react';
import {useSelector} from '@yiisoft/yii-dev-panel/store';
import {useServerSentEvents} from '@yiisoft/yii-dev-panel-sdk/Component/useDevServerEvents';

export const BadgedToggleButton = forwardRef<HTMLButtonElement, ToggleButtonProps & {badgeContent: number}>(
(props, ref) => {
const {badgeContent, children, ...others} = props;

return (
<Badge badgeContent={String(badgeContent)} color="primary">
<ToggleButton {...others} ref={ref}>
{children}
</ToggleButton>
</Badge>
);
},
);

enum EventTypeEnum {
VAR_DUMPER = 27,
LOGS = 43,
}

type EventType = {
data: string;
time: Date;
type: EventTypeEnum;
};

function DebugEntryIcon({type}: {type: EventTypeEnum | undefined}) {
if (type === EventTypeEnum.VAR_DUMPER) {
return (
<Tooltip title={'VarDumper'}>
<DataObjectIcon />
</Tooltip>
);
}
if (type === EventTypeEnum.LOGS) {
return (
<Tooltip title={'Logger'}>
<TableRowsIcon />
</Tooltip>
);
}

return <Avatar alt={'Unknown'} />;
}

function DebugEntryContent({data, type}: {data: string; type: EventTypeEnum}) {
if (type === EventTypeEnum.VAR_DUMPER) {
return <JsonRenderer value={JSON.parse(data)} />;
}
if (type === EventTypeEnum.LOGS) {
return <LogEntry entry={JSON.parse(data)} />;
}
return <>{data}</>;
}

export const Layout = () => {
const [events, setEvents] = useState<EventType[]>([]);
const [eventsCounter, setEventsCounter] = useState<Record<EventTypeEnum, number>>({
[EventTypeEnum.VAR_DUMPER]: 0,
[EventTypeEnum.LOGS]: 0,
});
const backendUrl = useSelector((state) => state.application.baseUrl) as string;

const onUpdatesHandler = useCallback((m) => {
console.log('event', m);
const data = JSON.parse(m.data);
setEventsCounter((v) => ({...v, [data[0]]: v[data[0]] + 1}));
setEvents((v) => [...v, {data: data[1], time: new Date(), type: data[0] as EventTypeEnum}]);
}, []);

useServerSentEvents(backendUrl, onUpdatesHandler, true);

const [types, setTypes] = useState<EventTypeEnum[]>([EventTypeEnum.VAR_DUMPER, EventTypeEnum.LOGS]);

const handleFormat = (event: MouseEvent<HTMLElement>, types: EventTypeEnum[]) => {
setTypes(types);
};

const handleClear = (event: MouseEvent<HTMLElement>) => {
setEvents([]);
setEventsCounter({
[EventTypeEnum.VAR_DUMPER]: 0,
[EventTypeEnum.LOGS]: 0,
});
};

return (
<>
<h2>Debug Server Listener</h2>
<Stack direction={'row'} spacing={1}>
<Badge badgeContent={String(events.length)} color="primary">
<Button onClick={handleClear}>Clear</Button>
</Badge>
<ToggleButtonGroup size="small" value={types} onChange={handleFormat} color="primary">
<BadgedToggleButton badgeContent={eventsCounter[EventTypeEnum.LOGS]} value={EventTypeEnum.LOGS}>
<TableRowsIcon />
&nbsp;Logs
</BadgedToggleButton>
<BadgedToggleButton
badgeContent={eventsCounter[EventTypeEnum.VAR_DUMPER]}
value={EventTypeEnum.VAR_DUMPER}
>
<DataObjectIcon />
&nbsp;VarDumper
</BadgedToggleButton>
</ToggleButtonGroup>
</Stack>
<List>
{events.map((e) => {
if (!types.includes(e.type)) {
return null;
}
return (
<ListItem>
<ListItemIcon>
<DebugEntryIcon type={e.type} />
</ListItemIcon>
<ListItemText secondary={formatDate(e.time.getTime())}>
<DebugEntryContent type={e.type} data={e.data} />
</ListItemText>
</ListItem>
);
})}
</List>
</>
);
};
1 change: 1 addition & 0 deletions packages/yii-dev-panel/src/Module/DevServer/Pages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {Layout} from '@yiisoft/yii-dev-panel/Module/DevServer/Pages/Layout';
15 changes: 15 additions & 0 deletions packages/yii-dev-panel/src/Module/DevServer/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {framesSlice} from '@yiisoft/yii-dev-panel/Module/DevServer/Context/Context';
import {persistReducer} from 'redux-persist';
import storage from 'redux-persist/lib/storage';

const framesSliceConfig = {
key: framesSlice.name,
version: 1,
storage,
};

export const reducers = {
[framesSlice.name]: persistReducer(framesSliceConfig, framesSlice.reducer),
};

export const middlewares = [];
10 changes: 10 additions & 0 deletions packages/yii-dev-panel/src/Module/DevServer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {ModuleInterface} from '@yiisoft/yii-dev-panel-sdk/Types/Module.types';
import {middlewares, reducers} from '@yiisoft/yii-dev-panel/Module/DevServer/api';
import {routes} from '@yiisoft/yii-dev-panel/Module/DevServer/router';

export const DevServerModule: ModuleInterface = {
routes: routes,
reducers: reducers,
middlewares: middlewares,
standaloneModule: false,
};
9 changes: 9 additions & 0 deletions packages/yii-dev-panel/src/Module/DevServer/router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as Pages from '@yiisoft/yii-dev-panel/Module/DevServer/Pages';
import {RouteObject} from 'react-router-dom';

export const routes = [
{
path: '/debug-server',
element: <Pages.Layout />,
},
] satisfies RouteObject[];
11 changes: 10 additions & 1 deletion packages/yii-dev-panel/src/modules.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import {ApplicationModule} from '@yiisoft/yii-dev-panel/Application';
import {DebugModule} from '@yiisoft/yii-dev-panel/Module/Debug';
import {DevServerModule} from '@yiisoft/yii-dev-panel/Module/DevServer';
import {FramesModule} from '@yiisoft/yii-dev-panel/Module/Frames';
import {GiiModule} from '@yiisoft/yii-dev-panel/Module/Gii';
import {InspectorModule} from '@yiisoft/yii-dev-panel/Module/Inspector';
import {OpenApiModule} from '@yiisoft/yii-dev-panel/Module/OpenApi';

export const modules = [ApplicationModule, DebugModule, GiiModule, InspectorModule, OpenApiModule, FramesModule];
export const modules = [
ApplicationModule,
DebugModule,
GiiModule,
InspectorModule,
OpenApiModule,
FramesModule,
DevServerModule,
];
1 change: 1 addition & 0 deletions packages/yii-dev-panel/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// <reference types="vite/client" />
/// <reference types="@welldone-software/why-did-you-render" />
/// <reference types="vite-plugin-pwa/react" />
/// <reference types="vite-plugin-svgr/client" />