diff --git a/packages/yii-dev-panel-sdk/src/Component/LogEntry.tsx b/packages/yii-dev-panel-sdk/src/Component/LogEntry.tsx new file mode 100644 index 00000000..b39b3091 --- /dev/null +++ b/packages/yii-dev-panel-sdk/src/Component/LogEntry.tsx @@ -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 ( + + {entry.message} + + + {'line' in entry && ( + + {entry.line} + + + )} + + + ); +}; diff --git a/packages/yii-dev-panel-sdk/src/Component/ServerSentEventsObserver.ts b/packages/yii-dev-panel-sdk/src/Component/ServerSentEventsObserver.ts index af25f6f3..4b129bac 100644 --- a/packages/yii-dev-panel-sdk/src/Component/ServerSentEventsObserver.ts +++ b/packages/yii-dev-panel-sdk/src/Component/ServerSentEventsObserver.ts @@ -1,5 +1,5 @@ // 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) {} @@ -7,6 +7,15 @@ class ServerSentEvents { 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); diff --git a/packages/yii-dev-panel-sdk/src/Component/useDevServerEvents.ts b/packages/yii-dev-panel-sdk/src/Component/useDevServerEvents.ts new file mode 100644 index 00000000..0a91d723 --- /dev/null +++ b/packages/yii-dev-panel-sdk/src/Component/useDevServerEvents.ts @@ -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) => 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]); +}; diff --git a/packages/yii-dev-panel-sdk/src/Component/useServerSentEvents.ts b/packages/yii-dev-panel-sdk/src/Component/useServerSentEvents.ts index 4421ff37..a02b1218 100644 --- a/packages/yii-dev-panel-sdk/src/Component/useServerSentEvents.ts +++ b/packages/yii-dev-panel-sdk/src/Component/useServerSentEvents.ts @@ -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) { diff --git a/packages/yii-dev-panel-sdk/src/Helper/collorMapper.ts b/packages/yii-dev-panel-sdk/src/Helper/collorMapper.ts new file mode 100644 index 00000000..c63133b3 --- /dev/null +++ b/packages/yii-dev-panel-sdk/src/Helper/collorMapper.ts @@ -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'; +}; diff --git a/packages/yii-dev-panel-sdk/src/Helper/formatDate.ts b/packages/yii-dev-panel-sdk/src/Helper/formatDate.ts index eecaecd2..9532c4f3 100644 --- a/packages/yii-dev-panel-sdk/src/Helper/formatDate.ts +++ b/packages/yii-dev-panel-sdk/src/Helper/formatDate.ts @@ -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) { diff --git a/packages/yii-dev-panel-sdk/src/Types/logger.ts b/packages/yii-dev-panel-sdk/src/Types/logger.ts new file mode 100644 index 00000000..b45cb142 --- /dev/null +++ b/packages/yii-dev-panel-sdk/src/Types/logger.ts @@ -0,0 +1 @@ +export type PhpLoggerLevel = 'emergency' | 'alert' | 'critical' | 'error' | 'warning' | 'notice' | 'info' | 'debug'; diff --git a/packages/yii-dev-panel/src/Application/Component/Layout.tsx b/packages/yii-dev-panel/src/Application/Component/Layout.tsx index fe0a34ff..d0da1808 100644 --- a/packages/yii-dev-panel/src/Application/Component/Layout.tsx +++ b/packages/yii-dev-panel/src/Application/Component/Layout.tsx @@ -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) => { })}
- + + + + { {!data || data.length === 0 ? ( <>Nothing here ) : ( - data.map((entry, index) => ( - - {entry.message} - - - - {entry.line} - - - - - )) + data.map((entry, index) => ) )} ); diff --git a/packages/yii-dev-panel/src/Module/DevServer/Context/Context.tsx b/packages/yii-dev-panel/src/Module/DevServer/Context/Context.tsx new file mode 100644 index 00000000..d692682c --- /dev/null +++ b/packages/yii-dev-panel/src/Module/DevServer/Context/Context.tsx @@ -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, + }, + 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}; +export const useDevServerEntries = () => useSelector((state: State) => state[framesSlice.name].frames); diff --git a/packages/yii-dev-panel/src/Module/DevServer/Pages/Layout.tsx b/packages/yii-dev-panel/src/Module/DevServer/Pages/Layout.tsx new file mode 100644 index 00000000..8ffdc327 --- /dev/null +++ b/packages/yii-dev-panel/src/Module/DevServer/Pages/Layout.tsx @@ -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( + (props, ref) => { + const {badgeContent, children, ...others} = props; + + return ( + + + {children} + + + ); + }, +); + +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 ( + + + + ); + } + if (type === EventTypeEnum.LOGS) { + return ( + + + + ); + } + + return ; +} + +function DebugEntryContent({data, type}: {data: string; type: EventTypeEnum}) { + if (type === EventTypeEnum.VAR_DUMPER) { + return ; + } + if (type === EventTypeEnum.LOGS) { + return ; + } + return <>{data}; +} + +export const Layout = () => { + const [events, setEvents] = useState([]); + const [eventsCounter, setEventsCounter] = useState>({ + [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.VAR_DUMPER, EventTypeEnum.LOGS]); + + const handleFormat = (event: MouseEvent, types: EventTypeEnum[]) => { + setTypes(types); + }; + + const handleClear = (event: MouseEvent) => { + setEvents([]); + setEventsCounter({ + [EventTypeEnum.VAR_DUMPER]: 0, + [EventTypeEnum.LOGS]: 0, + }); + }; + + return ( + <> +

Debug Server Listener

+ + + + + + + +  Logs + + + +  VarDumper + + + + + {events.map((e) => { + if (!types.includes(e.type)) { + return null; + } + return ( + + + + + + + + + ); + })} + + + ); +}; diff --git a/packages/yii-dev-panel/src/Module/DevServer/Pages/index.ts b/packages/yii-dev-panel/src/Module/DevServer/Pages/index.ts new file mode 100644 index 00000000..df601b93 --- /dev/null +++ b/packages/yii-dev-panel/src/Module/DevServer/Pages/index.ts @@ -0,0 +1 @@ +export {Layout} from '@yiisoft/yii-dev-panel/Module/DevServer/Pages/Layout'; diff --git a/packages/yii-dev-panel/src/Module/DevServer/api.ts b/packages/yii-dev-panel/src/Module/DevServer/api.ts new file mode 100644 index 00000000..800dea9e --- /dev/null +++ b/packages/yii-dev-panel/src/Module/DevServer/api.ts @@ -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 = []; diff --git a/packages/yii-dev-panel/src/Module/DevServer/index.ts b/packages/yii-dev-panel/src/Module/DevServer/index.ts new file mode 100644 index 00000000..f6dbff76 --- /dev/null +++ b/packages/yii-dev-panel/src/Module/DevServer/index.ts @@ -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, +}; diff --git a/packages/yii-dev-panel/src/Module/DevServer/router.tsx b/packages/yii-dev-panel/src/Module/DevServer/router.tsx new file mode 100644 index 00000000..5a62e735 --- /dev/null +++ b/packages/yii-dev-panel/src/Module/DevServer/router.tsx @@ -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: , + }, +] satisfies RouteObject[]; diff --git a/packages/yii-dev-panel/src/modules.ts b/packages/yii-dev-panel/src/modules.ts index 39214928..de734242 100644 --- a/packages/yii-dev-panel/src/modules.ts +++ b/packages/yii-dev-panel/src/modules.ts @@ -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, +]; diff --git a/packages/yii-dev-panel/src/vite-env.d.ts b/packages/yii-dev-panel/src/vite-env.d.ts index f01a5584..8dc3a67c 100644 --- a/packages/yii-dev-panel/src/vite-env.d.ts +++ b/packages/yii-dev-panel/src/vite-env.d.ts @@ -1,3 +1,4 @@ /// /// /// +///