Verified Commit def2b35e authored by Marek Veselý's avatar Marek Veselý
Browse files

feat: add a basic table to sandbox-logs

parent 5491cf86
Loading
Loading
Loading
Loading
+47 −26
Original line number Diff line number Diff line
@@ -24,31 +24,20 @@ export const TimeRangeSelector: FC<TimeRangeSelectorProps> = ({
    selectedTimeRangeInitialEnd,
  ])

  return (
    <RangeSlider
      min={0}
      max={selectedTimeRangeInitialEnd}
      value={sliderRange}
      onChange={setSliderRange}
      onRelease={([start, end]) =>
        setSelectedTimeRange({
          rangeStart: start,
          rangeEnd: end,
        })
  const labelRenderer = (
    value: number,
    opts?: {
      isHandleTooltip: boolean
    }
      stepSize={1000 * 60} // 1 minute steps
      labelStepSize={selectedTimeRangeInitialEnd / 5}
      labelRenderer={(value, opts) => (
        <div
          className={cx({
            [css`
              text-align: center;
            `]: true,
            [css`
              white-space: nowrap;
            `]: timeFormat !== 'absolute',
          })}
        >
  ) => {
    const children = (() => {
      if (!opts?.isHandleTooltip && value === 0) {
        return 'Exercise Started'
      }
      if (!opts?.isHandleTooltip && value === selectedTimeRangeInitialEnd) {
        return 'Duration Expired'
      }
      return (
        <Timestamp
          stringOnly
          formatTimestampProps={{
@@ -62,8 +51,40 @@ export const TimeRangeSelector: FC<TimeRangeSelectorProps> = ({
            `,
          }}
        />
      )
    })()

    return (
      <div
        className={cx({
          [css`
            text-align: center;
          `]: true,
          [css`
            white-space: nowrap;
          `]: timeFormat !== 'absolute' || !opts?.isHandleTooltip,
        })}
      >
        {children}
      </div>
      )}
    )
  }

  return (
    <RangeSlider
      min={0}
      max={selectedTimeRangeInitialEnd}
      value={sliderRange}
      onChange={setSliderRange}
      onRelease={([start, end]) =>
        setSelectedTimeRange({
          rangeStart: start,
          rangeEnd: end,
        })
      }
      stepSize={1000 * 60} // 1 minute steps
      labelStepSize={selectedTimeRangeInitialEnd / 5}
      labelRenderer={labelRenderer}
    />
  )
}
+111 −0
Original line number Diff line number Diff line
import { css } from '@emotion/css'
import type { Column, Row } from '@inject/shared'
import {
  SortingOrder,
  stringSortingFunction,
  Table,
  Timestamp,
  timestampSortingFunction,
} from '@inject/shared'
import type { FC } from 'react'
import type { SandboxLogChecked } from './types'

const centerChildren = css`
  display: flex;
  justify-content: center;
  align-items: center;
`

interface SandboxLogsTableProps {
  sandboxLogs: SandboxLogChecked[]
  allTeamsTab: boolean
}

export const SandboxLogsTable: FC<SandboxLogsTableProps> = ({
  sandboxLogs,
  allTeamsTab,
}) => {
  const columns: Column<SandboxLogChecked>[] = [
    {
      id: 'team-name',
      name: 'Team',
      display: allTeamsTab,
      renderValue: log => log.team.name,
      sortingFunction: (a, b) =>
        stringSortingFunction(a.team.name, b.team.name),
    },
    {
      id: 'timestamp',
      name: 'Time',
      renderValue: log => (
        <div className={centerChildren}>
          <Timestamp
            formatTimestampProps={{
              timestamp: log.timestamp,
              inExerciseTime: log.inExerciseTime,
              minimal: true,
            }}
          />
        </div>
      ),
      sortingFunction: (a, b) =>
        timestampSortingFunction(a.timestamp, b.timestamp),
      defaultSortingOrder: SortingOrder.DESC,
    },
    {
      id: 'command',
      name: 'Command',
      renderValue: log => log.details.cmd,
      sortingFunction: (a, b) =>
        stringSortingFunction(a.details.cmd, b.details.cmd),
    },
    {
      id: 'source',
      name: 'Source',
      renderValue: log => log.details.cmdSource,
      sortingFunction: (a, b) =>
        stringSortingFunction(a.details.cmdSource, b.details.cmdSource),
    },
    {
      id: 'directory',
      name: 'Directory',
      renderValue: log => log.details.workingDirectory,
      sortingFunction: (a, b) =>
        stringSortingFunction(
          a.details.workingDirectory,
          b.details.workingDirectory
        ),
    },
    {
      id: 'username',
      name: 'Username',
      renderValue: log => log.details.username,
      sortingFunction: (a, b) =>
        stringSortingFunction(a.details.username, b.details.username),
    },
    {
      id: 'container',
      name: 'Container',
      renderValue: log => log.details.container,
      sortingFunction: (a, b) =>
        stringSortingFunction(a.details.container, b.details.container),
    },
  ]

  const rows: Row<SandboxLogChecked>[] = sandboxLogs.map(log => ({
    id: log.id,
    value: log,
  }))

  return (
    <Table<SandboxLogChecked>
      columns={columns}
      noDataStateProps={{
        title: 'No logs to display',
        description: 'There are no executed commands matching the filters.',
      }}
      defaultSortByColumnId='timestamp'
      rows={rows}
    />
  )
}
+8 −0
Original line number Diff line number Diff line
import type {
  IAnalystSandboxLog,
  IAnalystSandboxLogDetails,
} from '@inject/graphql'

export type SandboxLogChecked = Omit<IAnalystSandboxLog, 'details'> & {
  details: IAnalystSandboxLogDetails
}
+8 −6
Original line number Diff line number Diff line
@@ -22,12 +22,6 @@ const RouteComponent = () => {

  useEffect(() => dispatch({ type: 'reset' }), [dispatch, exerciseId])

  const actionLogOptionsProps = useActionLogOptionsWithAutoProps({
    teamCount: teams.length,
    teamLimitDefault: 20,
    selectedTeamId: teams.length === 1 ? teams[0].id : null,
  })

  const {
    earliestStartTime,
    latestStartTime,
@@ -41,6 +35,14 @@ const RouteComponent = () => {
    includeAll: true,
  })
  const { selectedTeam } = teamSelectorProps
  const selectedTeamId = selectedTeam?.id || null

  const teamIds = selectedTeamId ? [selectedTeamId] : teams.map(t => t.id)
  const actionLogOptionsProps = useActionLogOptionsWithAutoProps({
    teamCount: teamIds.length,
    teamLimitDefault: 20,
    selectedTeamId,
  })

  return (
    <div
+125 −16
Original line number Diff line number Diff line
import { Divider } from '@blueprintjs/core'
import { css } from '@emotion/css'
import { useSubscribedTeams } from '@inject/frontend'
import type { TeamLimitOptions } from '@inject/frontend'
import { TeamLimitSelect, useSubscribedTeams } from '@inject/frontend'
import type { VariablesOf } from '@inject/graphql'
import {
  AnalystSandboxLogsQuery,
  AnalystSandboxLogsSubscription,
  useSubscription,
  useTypedQuery,
} from '@inject/graphql'
import { CenteredSpinner } from '@inject/shared'
import { createFileRoute } from '@tanstack/react-router'
import { useEffect, useRef, useState } from 'react'
import { TimeRangeSelector } from '../../../components/Plots/TimeScatterPlot/TimeRangeSelector'
import { useTimeRangeSelectorProps } from '../../../components/Plots/TimeScatterPlot/TimeRangeSelector/useTimeRangeSelectorProps'
import { SandboxLogsTable } from '../../../components/SandboxLogsTable'
import type { SandboxLogChecked } from '../../../components/SandboxLogsTable/types'
import { TeamSelector } from '../../../components/TeamSelector'
import { useTeamSelectorProps } from '../../../components/TeamSelector/useTeamSelectorProps'

// TODO: table
// TODO: sortable table
// TODO: select columns
// TODO: filtered table
// TODO: searchbar
// TODO: histogram
// TODO: time range selector like action-logs
// TODO: when all teams selected, show how many logs per team
// a) inside the histogram
// b) a separate bar chart

const wrapper = css`
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: auto;
`
// TODO: allow logs outside of time range (outside of exercise time)

const RouteComponent = () => {
  const { exerciseId } = SandboxLogsRoute.useParams()
@@ -30,12 +35,116 @@ const RouteComponent = () => {
    teams,
    includeAll: true,
  })
  const selectedTeamId = teamSelectorProps.selectedTeam?.id || null

  const teamLimitDefault = selectedTeamId ? 50 : 10
  const [teamLimit, setTeamLimit] = useState<TeamLimitOptions>(teamLimitDefault)
  const prevTeamIdRef = useRef<string | null>(selectedTeamId)
  // Reset team limit when switching from a specific team to "all teams"
  useEffect(() => {
    if (prevTeamIdRef.current !== null && selectedTeamId === null) {
      setTeamLimit(teamLimitDefault)
    }
    prevTeamIdRef.current = selectedTeamId
  }, [selectedTeamId, teamLimitDefault])

  // TODO: add first and last timestamp so that logs outside of the exercise can also be shown (e.g. sandbox logs from before the exercise started)
  // TODO: also in other places where time range selector is used
  const {
    earliestStartTime,
    selectedTimeRange,
    selectedTimeRangeInitialEnd,
    setSelectedTimeRange,
  } = useTimeRangeSelectorProps()

  // TODO: test if works when changing teams
  const teamIds = selectedTeamId ? [selectedTeamId] : teams.map(t => t.id)
  const [{ data, fetching }] = useTypedQuery({
    query: AnalystSandboxLogsQuery,
    variables: {
      teamIds,
      exerciseId,
      teamLimit,
    },
    pause: !teamIds.length,
    requestPolicy: 'cache-and-network',
  })
  useSubscription({
    document: AnalystSandboxLogsSubscription,
    variables: { teamIds } as VariablesOf<
      typeof AnalystSandboxLogsSubscription
    >,
    pause: !teamIds.length,
  })

  const sandboxLogsInRange = data
    ? (data.teamActionLogs as SandboxLogChecked[]).filter(actionLog => {
        const actionLogTime = new Date(actionLog.timestamp).getTime()
        return (
    <div className={wrapper}>
          actionLogTime >= earliestStartTime + selectedTimeRange.rangeStart &&
          actionLogTime <= earliestStartTime + selectedTimeRange.rangeEnd
        )
      })
    : []

  return (
    <main
      className={css`
        display: flex;
        flex-direction: column;
        height: 100%;
        overflow: auto;
        gap: 2rem;
      `}
    >
      <section
        className={css`
          display: flex;
          flex-direction: column;
        `}
      >
        <div
          className={css`
            display: flex;
            justify-content: space-between;
            gap: 1rem;
          `}
        >
          <TeamSelector {...teamSelectorProps} />
      <Divider />
          <TeamLimitSelect
            teamLimit={teamLimit}
            setTeamLimit={setTeamLimit}
            teamCount={teamIds.length}
          />
        </div>
        <div
          className={css`
            padding: 0.5rem 4rem 0 4rem;
          `}
        >
          <TimeRangeSelector
            earliestStartTime={earliestStartTime}
            selectedTimeRangeInitialEnd={selectedTimeRangeInitialEnd}
            setSelectedTimeRange={setSelectedTimeRange}
          />
        </div>
      </section>

      <section
        className={css`
          flex: 1;
          overflow: auto;
        `}
      >
        {(fetching || !data) && <CenteredSpinner />}
        {data && (
          <SandboxLogsTable
            sandboxLogs={sandboxLogsInRange}
            allTeamsTab={selectedTeamId === null}
          />
        )}
      </section>
    </main>
  )
}

Loading