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));
Expand All @@ -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
Expand Up @@ -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) {
Expand Down
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) {
Expand All @@ -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) {
Expand Down
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,
Expand Down Expand Up @@ -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)}
Expand Down
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[];
};
Expand All @@ -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} />)
)}
</>
);
Expand Down
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';
Loading