Loading backend @ c27fc035 Compare aac3fc30 to c27fc035 Original line number Diff line number Diff line Subproject commit aac3fc3002ca4ce278e76dc2baa6d6f8b5f1cc73 Subproject commit c27fc03543594163601281b728067593ffa4815d editor/src/routes/index.tsx +1 −1 Original line number Diff line number Diff line Loading @@ -55,7 +55,7 @@ const LandingPage = () => { }, []) return ( <Container makeFullHeight className={container}> <Container className={container}> <div className={container}> <InjectLogo className={logo} /> <div className={introduction}> Loading frontend/src/actionlog/ActionLog/ListRenderer.tsx +59 −26 Original line number Diff line number Diff line import type { NonIdealStateProps } from '@blueprintjs/core' import type { CalloutProps, NonIdealStateProps } from '@blueprintjs/core' import { ButtonGroup, Callout, Classes, Colors, Divider, Loading Loading @@ -77,26 +78,36 @@ const emptyState = css` justify-content: center; ` export const ListRenderer: FC<{ const calloutStyles = css` // subtract margin width: calc(100% - 2rem); margin: 1rem; ` interface ListRendererProps { actionLogs: ActionLog[] exerciseId: string teamId: string channel: Channel inInstructor: boolean getFullscreenLink: (actionLogId: string) => NavigateOptions scrollToLast?: boolean getFullscreenLink?: (actionLogId: string) => NavigateOptions scrollToBottom?: boolean noDataProps?: NonIdealStateProps getFileLink: (fileId: string) => NavigateOptions }> = ({ calloutProps?: CalloutProps } export const ListRenderer: FC<ListRendererProps> = ({ actionLogs, exerciseId, teamId, channel, inInstructor, getFullscreenLink, scrollToLast, scrollToBottom, noDataProps, getFileLink, calloutProps, }) => { const { t } = useTranslationFrontend() const actionLogsLengthPrev = useRef<number>(actionLogs.length) Loading @@ -116,19 +127,33 @@ export const ListRenderer: FC<{ const unreadLogsStartRef = useRef<HTMLDivElement | null>(null) const lastLogRef = useRef<HTMLDivElement | null>(null) // scroll to bottom when rendered, or when changing teams or channels useLayoutEffect(() => { const scrollOnce = () => { const ref = document.getElementById('scrollable') as HTMLDivElement const lastLog = lastLogRef.current if (!ref) { return } if (ref && lastLog) { if (ref.clientHeight < (lastLog.getBoundingClientRect().height ?? 0)) { lastLog.scrollIntoView({ block: 'start', behavior: 'instant' }) if (lastLogRef.current) { lastLogRef.current.scrollIntoView({ block: 'end', behavior: 'instant' }) } else { ref.scrollTo({ top: ref.scrollHeight, behavior: 'instant' }) } } }, [actionLogs.length]) const handleScroll = useCallback(() => { scrollOnce() // Allow markdown/images to finish layout before final scroll requestAnimationFrame(() => { requestAnimationFrame(scrollOnce) }) }, []) // scroll to bottom when rendered, or when changing teams or channels useLayoutEffect(() => { if (!scrollToBottom) { return } handleScroll() }, [actionLogs.length, handleScroll, scrollToBottom]) /* * Logs with timestampRead === undefined should be rendered as unread Loading @@ -147,17 +172,21 @@ export const ListRenderer: FC<{ useEffect(() => { if (actionLogs.length !== actionLogsLengthPrev.current) { if (firstUnreadId === undefined) setFirstUnreadId(getFirstUnreadId()) if (scrollToLast) { lastLogRef.current?.scrollIntoView({ behavior: 'smooth', inline: 'nearest', }) if (firstUnreadId === undefined) { setFirstUnreadId(getFirstUnreadId()) } if (scrollToBottom) { handleScroll() } } actionLogsLengthPrev.current = actionLogs.length }, [actionLogs.length, firstUnreadId, getFirstUnreadId, scrollToLast]) }, [ actionLogs.length, firstUnreadId, getFirstUnreadId, handleScroll, scrollToBottom, ]) return ( <div className={cx(view, { [instructorView]: inInstructor })} id='channel'> Loading @@ -170,7 +199,7 @@ export const ListRenderer: FC<{ <Divider /> </div> {actionLogs.length == 0 ? ( {actionLogs.length === 0 ? ( <NonIdealState icon='low-voltage-pole' title={t('actionLog.noInjectsTitle')} Loading @@ -180,6 +209,10 @@ export const ListRenderer: FC<{ /> ) : ( <div className={scrollable} id='scrollable'> {calloutProps && ( <Callout className={calloutStyles} {...calloutProps} /> )} {actionLogs.map((actionLog, index) => ( <Fragment key={actionLog.id}> {/* Loading @@ -201,7 +234,7 @@ export const ListRenderer: FC<{ exerciseId={exerciseId} teamId={teamId} actionLog={actionLog} fullscreenLink={getFullscreenLink(actionLog.id)} fullscreenLink={getFullscreenLink?.(actionLog.id)} inInstructor={inInstructor} /> </div> Loading frontend/src/actionlog/ActionLog/index.tsx +20 −4 Original line number Diff line number Diff line import type { CalloutProps } from '@blueprintjs/core' import { NonIdealState, type NonIdealStateProps } from '@blueprintjs/core' import { ChannelActionLogsQuery, useTypedQuery } from '@inject/graphql' import { useTranslationFrontend } from '@inject/locale' Loading @@ -11,14 +12,20 @@ interface ActionLogProps { exerciseId: string channelId: string inInstructor: boolean getFullscreenLink: (actionLogId: string) => NavigateOptions getFullscreenLink?: (actionLogId: string) => NavigateOptions noDataProps?: NonIdealStateProps scrollToLast?: boolean scrollToBottom?: boolean getFileLink: (fileId: string) => NavigateOptions teamLimit?: number newestFirst?: boolean refetchOnRender?: boolean calloutProps?: CalloutProps } /** * The component expects that the subscription is active for the given teamId * and that the data is updated in the cache even if this component is not rendered. * If this is not the case, use the `refetchOnRender` prop to refetch the data on mount. */ export const ActionLog: FC<ActionLogProps> = ({ inInstructor, Loading @@ -27,8 +34,12 @@ export const ActionLog: FC<ActionLogProps> = ({ channelId, getFullscreenLink, noDataProps, scrollToLast, scrollToBottom, getFileLink, teamLimit, newestFirst, refetchOnRender, calloutProps, }) => { const { t } = useTranslationFrontend() Loading @@ -37,7 +48,11 @@ export const ActionLog: FC<ActionLogProps> = ({ variables: { teamIds: [teamId], channelId, exerciseId, teamLimit, newestFirst, }, requestPolicy: refetchOnRender ? 'cache-and-network' : undefined, context: useMemo( () => ({ suspense: true, Loading @@ -64,13 +79,14 @@ export const ActionLog: FC<ActionLogProps> = ({ <ListRenderer getFileLink={getFileLink} actionLogs={data.teamActionLogs} scrollToLast={scrollToLast} scrollToBottom={scrollToBottom} channel={data.channel} exerciseId={exerciseId} getFullscreenLink={getFullscreenLink} inInstructor={inInstructor} teamId={teamId} noDataProps={noDataProps} calloutProps={calloutProps} /> ) } frontend/src/actionlog/InjectMessage/Content/SandboxLogContent.tsx 0 → 100644 +82 −0 Original line number Diff line number Diff line import { Classes, Colors } from '@blueprintjs/core' import { css, cx } from '@emotion/css' import type { ISandboxLogDetails, TSandboxLogDetails } from '@inject/graphql' import { useTranslationFrontend } from '@inject/locale' import type { FC } from 'react' const card = css` display: grid; gap: 0.75rem; padding: 0.75rem 1rem; border-radius: 8px; border: 1px solid ${Colors.LIGHT_GRAY3}; background: ${Colors.LIGHT_GRAY5}; .${Classes.DARK} & { background: ${Colors.DARK_GRAY4}; border-color: ${Colors.DARK_GRAY3}; } ` const grid = css` display: grid; gap: 0.5rem 1rem; ` const row = css` display: grid; grid-template-columns: 11rem 1fr; gap: 0.5rem 1rem; align-items: baseline; ` const label = cx( Classes.TEXT_SMALL, css` text-transform: uppercase; color: ${Colors.GRAY1}; .${Classes.DARK} & { color: ${Colors.GRAY5}; } ` ) const value = css` color: ${Colors.DARK_GRAY1}; .${Classes.DARK} & { color: ${Colors.LIGHT_GRAY5}; } ` interface SandboxLogContentProps { details: TSandboxLogDetails | ISandboxLogDetails } export const SandboxLogContent: FC<SandboxLogContentProps> = ({ details }) => { const { cmdSource, container, username, workingDirectory } = details const { t } = useTranslationFrontend() return ( <div className={card}> <div className={grid}> <div className={row}> <div className={label}>{t('sandboxLogContent.source')}</div> <div className={value}>{cmdSource}</div> </div> <div className={row}> <div className={label}>{t('sandboxLogContent.container')}</div> <div className={value}>{container}</div> </div> <div className={row}> <div className={label}>{t('sandboxLogContent.user')}</div> <div className={value}>{username}</div> </div> <div className={row}> <div className={label}>{t('sandboxLogContent.workingDirectory')}</div> <div className={value}> <code>{workingDirectory}</code> </div> </div> </div> </div> ) } Loading
backend @ c27fc035 Compare aac3fc30 to c27fc035 Original line number Diff line number Diff line Subproject commit aac3fc3002ca4ce278e76dc2baa6d6f8b5f1cc73 Subproject commit c27fc03543594163601281b728067593ffa4815d
editor/src/routes/index.tsx +1 −1 Original line number Diff line number Diff line Loading @@ -55,7 +55,7 @@ const LandingPage = () => { }, []) return ( <Container makeFullHeight className={container}> <Container className={container}> <div className={container}> <InjectLogo className={logo} /> <div className={introduction}> Loading
frontend/src/actionlog/ActionLog/ListRenderer.tsx +59 −26 Original line number Diff line number Diff line import type { NonIdealStateProps } from '@blueprintjs/core' import type { CalloutProps, NonIdealStateProps } from '@blueprintjs/core' import { ButtonGroup, Callout, Classes, Colors, Divider, Loading Loading @@ -77,26 +78,36 @@ const emptyState = css` justify-content: center; ` export const ListRenderer: FC<{ const calloutStyles = css` // subtract margin width: calc(100% - 2rem); margin: 1rem; ` interface ListRendererProps { actionLogs: ActionLog[] exerciseId: string teamId: string channel: Channel inInstructor: boolean getFullscreenLink: (actionLogId: string) => NavigateOptions scrollToLast?: boolean getFullscreenLink?: (actionLogId: string) => NavigateOptions scrollToBottom?: boolean noDataProps?: NonIdealStateProps getFileLink: (fileId: string) => NavigateOptions }> = ({ calloutProps?: CalloutProps } export const ListRenderer: FC<ListRendererProps> = ({ actionLogs, exerciseId, teamId, channel, inInstructor, getFullscreenLink, scrollToLast, scrollToBottom, noDataProps, getFileLink, calloutProps, }) => { const { t } = useTranslationFrontend() const actionLogsLengthPrev = useRef<number>(actionLogs.length) Loading @@ -116,19 +127,33 @@ export const ListRenderer: FC<{ const unreadLogsStartRef = useRef<HTMLDivElement | null>(null) const lastLogRef = useRef<HTMLDivElement | null>(null) // scroll to bottom when rendered, or when changing teams or channels useLayoutEffect(() => { const scrollOnce = () => { const ref = document.getElementById('scrollable') as HTMLDivElement const lastLog = lastLogRef.current if (!ref) { return } if (ref && lastLog) { if (ref.clientHeight < (lastLog.getBoundingClientRect().height ?? 0)) { lastLog.scrollIntoView({ block: 'start', behavior: 'instant' }) if (lastLogRef.current) { lastLogRef.current.scrollIntoView({ block: 'end', behavior: 'instant' }) } else { ref.scrollTo({ top: ref.scrollHeight, behavior: 'instant' }) } } }, [actionLogs.length]) const handleScroll = useCallback(() => { scrollOnce() // Allow markdown/images to finish layout before final scroll requestAnimationFrame(() => { requestAnimationFrame(scrollOnce) }) }, []) // scroll to bottom when rendered, or when changing teams or channels useLayoutEffect(() => { if (!scrollToBottom) { return } handleScroll() }, [actionLogs.length, handleScroll, scrollToBottom]) /* * Logs with timestampRead === undefined should be rendered as unread Loading @@ -147,17 +172,21 @@ export const ListRenderer: FC<{ useEffect(() => { if (actionLogs.length !== actionLogsLengthPrev.current) { if (firstUnreadId === undefined) setFirstUnreadId(getFirstUnreadId()) if (scrollToLast) { lastLogRef.current?.scrollIntoView({ behavior: 'smooth', inline: 'nearest', }) if (firstUnreadId === undefined) { setFirstUnreadId(getFirstUnreadId()) } if (scrollToBottom) { handleScroll() } } actionLogsLengthPrev.current = actionLogs.length }, [actionLogs.length, firstUnreadId, getFirstUnreadId, scrollToLast]) }, [ actionLogs.length, firstUnreadId, getFirstUnreadId, handleScroll, scrollToBottom, ]) return ( <div className={cx(view, { [instructorView]: inInstructor })} id='channel'> Loading @@ -170,7 +199,7 @@ export const ListRenderer: FC<{ <Divider /> </div> {actionLogs.length == 0 ? ( {actionLogs.length === 0 ? ( <NonIdealState icon='low-voltage-pole' title={t('actionLog.noInjectsTitle')} Loading @@ -180,6 +209,10 @@ export const ListRenderer: FC<{ /> ) : ( <div className={scrollable} id='scrollable'> {calloutProps && ( <Callout className={calloutStyles} {...calloutProps} /> )} {actionLogs.map((actionLog, index) => ( <Fragment key={actionLog.id}> {/* Loading @@ -201,7 +234,7 @@ export const ListRenderer: FC<{ exerciseId={exerciseId} teamId={teamId} actionLog={actionLog} fullscreenLink={getFullscreenLink(actionLog.id)} fullscreenLink={getFullscreenLink?.(actionLog.id)} inInstructor={inInstructor} /> </div> Loading
frontend/src/actionlog/ActionLog/index.tsx +20 −4 Original line number Diff line number Diff line import type { CalloutProps } from '@blueprintjs/core' import { NonIdealState, type NonIdealStateProps } from '@blueprintjs/core' import { ChannelActionLogsQuery, useTypedQuery } from '@inject/graphql' import { useTranslationFrontend } from '@inject/locale' Loading @@ -11,14 +12,20 @@ interface ActionLogProps { exerciseId: string channelId: string inInstructor: boolean getFullscreenLink: (actionLogId: string) => NavigateOptions getFullscreenLink?: (actionLogId: string) => NavigateOptions noDataProps?: NonIdealStateProps scrollToLast?: boolean scrollToBottom?: boolean getFileLink: (fileId: string) => NavigateOptions teamLimit?: number newestFirst?: boolean refetchOnRender?: boolean calloutProps?: CalloutProps } /** * The component expects that the subscription is active for the given teamId * and that the data is updated in the cache even if this component is not rendered. * If this is not the case, use the `refetchOnRender` prop to refetch the data on mount. */ export const ActionLog: FC<ActionLogProps> = ({ inInstructor, Loading @@ -27,8 +34,12 @@ export const ActionLog: FC<ActionLogProps> = ({ channelId, getFullscreenLink, noDataProps, scrollToLast, scrollToBottom, getFileLink, teamLimit, newestFirst, refetchOnRender, calloutProps, }) => { const { t } = useTranslationFrontend() Loading @@ -37,7 +48,11 @@ export const ActionLog: FC<ActionLogProps> = ({ variables: { teamIds: [teamId], channelId, exerciseId, teamLimit, newestFirst, }, requestPolicy: refetchOnRender ? 'cache-and-network' : undefined, context: useMemo( () => ({ suspense: true, Loading @@ -64,13 +79,14 @@ export const ActionLog: FC<ActionLogProps> = ({ <ListRenderer getFileLink={getFileLink} actionLogs={data.teamActionLogs} scrollToLast={scrollToLast} scrollToBottom={scrollToBottom} channel={data.channel} exerciseId={exerciseId} getFullscreenLink={getFullscreenLink} inInstructor={inInstructor} teamId={teamId} noDataProps={noDataProps} calloutProps={calloutProps} /> ) }
frontend/src/actionlog/InjectMessage/Content/SandboxLogContent.tsx 0 → 100644 +82 −0 Original line number Diff line number Diff line import { Classes, Colors } from '@blueprintjs/core' import { css, cx } from '@emotion/css' import type { ISandboxLogDetails, TSandboxLogDetails } from '@inject/graphql' import { useTranslationFrontend } from '@inject/locale' import type { FC } from 'react' const card = css` display: grid; gap: 0.75rem; padding: 0.75rem 1rem; border-radius: 8px; border: 1px solid ${Colors.LIGHT_GRAY3}; background: ${Colors.LIGHT_GRAY5}; .${Classes.DARK} & { background: ${Colors.DARK_GRAY4}; border-color: ${Colors.DARK_GRAY3}; } ` const grid = css` display: grid; gap: 0.5rem 1rem; ` const row = css` display: grid; grid-template-columns: 11rem 1fr; gap: 0.5rem 1rem; align-items: baseline; ` const label = cx( Classes.TEXT_SMALL, css` text-transform: uppercase; color: ${Colors.GRAY1}; .${Classes.DARK} & { color: ${Colors.GRAY5}; } ` ) const value = css` color: ${Colors.DARK_GRAY1}; .${Classes.DARK} & { color: ${Colors.LIGHT_GRAY5}; } ` interface SandboxLogContentProps { details: TSandboxLogDetails | ISandboxLogDetails } export const SandboxLogContent: FC<SandboxLogContentProps> = ({ details }) => { const { cmdSource, container, username, workingDirectory } = details const { t } = useTranslationFrontend() return ( <div className={card}> <div className={grid}> <div className={row}> <div className={label}>{t('sandboxLogContent.source')}</div> <div className={value}>{cmdSource}</div> </div> <div className={row}> <div className={label}>{t('sandboxLogContent.container')}</div> <div className={value}>{container}</div> </div> <div className={row}> <div className={label}>{t('sandboxLogContent.user')}</div> <div className={value}>{username}</div> </div> <div className={row}> <div className={label}>{t('sandboxLogContent.workingDirectory')}</div> <div className={value}> <code>{workingDirectory}</code> </div> </div> </div> </div> ) }