hyper-tuner/hyper-tuner-cloud

View on GitHub
src/pages/Diagnose.tsx

Summary

Maintainability
D
2 days
Test Coverage
import { FileTextOutlined, GlobalOutlined } from '@ant-design/icons';
import { Badge, Divider, Grid, Layout, Progress, Space, Steps, Tabs, Typography } from 'antd';
import { useCallback, useEffect, useRef, useState } from 'react';
import PerfectScrollbar from 'react-perfect-scrollbar';
import { connect } from 'react-redux';
import { Link, generatePath, useMatch, useNavigate } from 'react-router-dom';
import Loader from '../components/Loader';
import CompositeCanvas from '../components/TriggerLogs/CompositeCanvas';
import ToothCanvas from '../components/TriggerLogs/ToothCanvas';
import { collapsedSidebarWidth, sidebarWidth } from '../components/Tune/SideBar';
import useServerStorage from '../hooks/useServerStorage';
import { removeFilenameSuffix } from '../pocketbase';
import { Routes } from '../routes';
import store from '../store';
import { AppState, ToothLogsState, TuneDataState, UIState } from '../types/state';
import { Colors } from '../utils/colors';
import { decompress } from '../utils/compression';
import { isAbortedRequest } from '../utils/error';
import TriggerLogsParser, {
  CompositeLogEntry,
  ToothLogEntry,
} from '../utils/logs/TriggerLogsParser';
import { formatBytes } from '../utils/numbers';

const { Content } = Layout;

const edgeUnknown = 'Unknown';
const badgeStyle = { backgroundColor: Colors.TEXT };

const mapStateToProps = (state: AppState) => ({
  ui: state.ui,
  status: state.status,
  config: state.config,
  loadedToothLogs: state.toothLogs,
  tuneData: state.tuneData,
});

const Diagnose = ({
  ui,
  loadedToothLogs,
  tuneData,
}: {
  ui: UIState;
  loadedToothLogs: ToothLogsState | null;
  tuneData: TuneDataState | null;
}) => {
  const { lg } = Grid.useBreakpoint();
  const { Sider } = Layout;
  const [progress, setProgress] = useState(0);
  const [fileSize, setFileSize] = useState<string>();
  const [step, setStep] = useState(0);
  const [edgeLocation, setEdgeLocation] = useState(edgeUnknown);
  const [fetchError, setFetchError] = useState<Error>();
  const contentRef = useRef<HTMLDivElement | null>(null);
  const [canvasWidth, setCanvasWidth] = useState(0);
  const [canvasHeight, setCanvasHeight] = useState(0);
  const routeMatch = useMatch(Routes.TUNE_DIAGNOSE_FILE);
  const { fetchLogFileWithProgress } = useServerStorage();
  const navigate = useNavigate();

  const calculateCanvasSize = useCallback(() => {
    setCanvasWidth(contentRef.current?.clientWidth || 0);
    setCanvasHeight(Math.round(window.innerHeight - 115));
  }, []);
  const siderProps = {
    width: sidebarWidth,
    collapsedWidth: collapsedSidebarWidth,
    collapsible: true,
    breakpoint: 'xl' as const,
    collapsed: ui.sidebarCollapsed,
    onCollapse: (collapsed: boolean) => {
      store.dispatch({ type: 'ui/sidebarCollapsed', payload: collapsed });
    },
  };

  useEffect(() => {
    const controller = new AbortController();
    const { signal } = controller;

    const loadData = async () => {
      const logFileName = routeMatch?.params.fileName;

      if (!logFileName) {
        return;
      }

      // user didn't upload any logs
      if ((tuneData?.toothLogFiles || []).length === 0) {
        navigate(Routes.HUB);

        return;
      }

      try {
        const raw = await fetchLogFileWithProgress(
          tuneData!.id,
          logFileName,
          (percent, total, edge) => {
            setProgress(percent);
            setFileSize(formatBytes(total));
            setEdgeLocation(edge || edgeUnknown);
          },
          signal,
        );

        setFileSize(formatBytes(raw.byteLength));
        setStep(1);

        const parser = new TriggerLogsParser(decompress(raw)).parse();

        let type = '';
        let result: CompositeLogEntry[] | ToothLogEntry[] = [];

        if (parser.isComposite()) {
          type = 'composite';
          result = parser.getCompositeLogs();
        }

        if (parser.isTooth()) {
          type = 'tooth';
          result = parser.getToothLogs();
        }

        store.dispatch({
          type: 'toothLogs/load',
          payload: {
            fileName: logFileName,
            logs: result,
            type,
          },
        });

        setStep(2);
      } catch (error) {
        if (isAbortedRequest(error as Error)) {
          return;
        }

        setFetchError(error as Error);
      }
    };

    // first visit, logs are not loaded yet
    if (!loadedToothLogs?.type && tuneData?.tuneId) {
      loadData();
    }

    // file changed, reload
    if (loadedToothLogs?.type && loadedToothLogs.fileName !== routeMatch?.params.fileName) {
      // setToothLogs(undefined);
      // setCompositeLogs(undefined);
      store.dispatch({ type: 'toothLogs/load', payload: {} });
      loadData();
    }

    // user navigated to logs root page
    if (!routeMatch?.params.fileName && tuneData && tuneData.toothLogFiles.length) {
      // either redirect to the first log or to the latest selected
      if (loadedToothLogs?.fileName) {
        navigate(
          generatePath(Routes.TUNE_DIAGNOSE_FILE, {
            tuneId: tuneData.tuneId,
            fileName: loadedToothLogs.fileName,
          }),
        );
      } else {
        const firstLogFile = tuneData.toothLogFiles[0];
        navigate(
          generatePath(Routes.TUNE_DIAGNOSE_FILE, {
            tuneId: tuneData.tuneId,
            fileName: firstLogFile,
          }),
        );
      }
    }

    calculateCanvasSize();

    window.addEventListener('resize', calculateCanvasSize);

    return () => {
      controller.abort();
      window.removeEventListener('resize', calculateCanvasSize);
    };
  }, [calculateCanvasSize, routeMatch?.params.fileName, ui.sidebarCollapsed, tuneData?.tuneId]);

  const Graph = () => {
    switch (loadedToothLogs?.type) {
      case 'composite':
        return (
          <CompositeCanvas
            data={loadedToothLogs.logs as CompositeLogEntry[]}
            width={canvasWidth}
            height={canvasHeight}
          />
        );
      case 'tooth':
        return (
          <ToothCanvas data={loadedToothLogs.logs} width={canvasWidth} height={canvasHeight} />
        );
      default:
        return null;
    }
  };

  return (
    <>
      <Sider {...siderProps} className="app-sidebar">
        {loadedToothLogs?.type ? (
          !ui.sidebarCollapsed && (
            <Tabs
              defaultActiveKey="files"
              style={{ marginLeft: 20 }}
              items={[
                {
                  label: (
                    <Badge
                      size="small"
                      style={badgeStyle}
                      count={tuneData?.toothLogFiles.length}
                      offset={[10, -3]}
                    >
                      <FileTextOutlined />
                      Files
                    </Badge>
                  ),
                  key: 'files',
                  children: (
                    <PerfectScrollbar options={{ suppressScrollX: true }}>
                      {tuneData?.toothLogFiles.map((fileName) => (
                        <Typography.Paragraph key={fileName} ellipsis>
                          <Link
                            to={generatePath(Routes.TUNE_DIAGNOSE_FILE, {
                              tuneId: tuneData.tuneId,
                              fileName,
                            })}
                            style={
                              routeMatch?.params.fileName === fileName ? {} : { color: 'inherit' }
                            }
                          >
                            {removeFilenameSuffix(fileName)}
                          </Link>
                        </Typography.Paragraph>
                      ))}
                    </PerfectScrollbar>
                  ),
                },
              ]}
            />
          )
        ) : (
          <Loader />
        )}
      </Sider>
      <Layout className="logs-container">
        <Content>
          <div ref={contentRef}>
            {loadedToothLogs?.type ? (
              <Graph />
            ) : (
              <Space direction="vertical" size="large">
                <Progress type="circle" percent={progress} className="logs-progress" />
                <Divider />
                <Steps
                  current={step}
                  direction={lg ? 'horizontal' : 'vertical'}
                  items={[
                    {
                      title: 'Downloading',
                      subTitle: fileSize,
                      description: fetchError ? (
                        fetchError.message
                      ) : (
                        <Space>
                          <GlobalOutlined />
                          {edgeLocation}
                        </Space>
                      ),
                    },
                    {
                      title: 'Decoding',
                      description: 'Parsing CSV',
                    },
                    {
                      title: 'Rendering',
                      description: 'Putting pixels on your screen',
                    },
                  ]}
                />
              </Space>
            )}
          </div>
        </Content>
      </Layout>
    </>
  );
};

export default connect(mapStateToProps)(Diagnose);