visible/visible

View on GitHub
packages/@visi/web-client/src/components/report/report.tsx

Summary

Maintainability
C
1 day
Test Coverage
import classNames from 'classnames';
import React, { useState } from 'react';
import ReactGA from 'react-ga';

import {
  Difficulty,
  Impact,
  Outcome,
  ReportLargeFragment,
} from '../../generated/graphql';
import { useTranslation } from '../../utils/i18next';
import {
  Badge,
  BadgeVariant,
  CodeFrame,
  DiffCodeFrame,
  Image,
  Typography,
} from '../ui';
import { OutcomeIcon } from './outcome';
import { Reference } from './reference';
import { Status } from './status';

export interface ReportProps {
  original: string;
  url: string;
  report: ReportLargeFragment;
  diagnosisId: string;
  withEditor?: boolean;
  withKeywords?: boolean;
}

const mapImpact = (impact?: Impact | null): BadgeVariant => {
  switch (impact) {
    case Impact.Critical:
      return 'purple';
    case Impact.Serious:
      return 'red';
    case Impact.Minor:
      return 'yellow';
    default:
      return 'grey';
  }
};

const mapDifficulty = (difficulty?: Difficulty | null): BadgeVariant => {
  switch (difficulty) {
    case Difficulty.Difficult:
      return 'red';
    case Difficulty.Medium:
      return 'yellow';
    case Difficulty.Easy:
      return 'green';
    default:
      return 'grey';
  }
};

export const Report = ({ report, original, url, withEditor }: ReportProps) => {
  const { t } = useTranslation();
  const [open, setOpen] = useState(false);

  const impactVariant = mapImpact(report.impact);
  const difficultyVariant = mapDifficulty(report.difficulty);

  const detailsId = `${report.id}-details`;
  const wrapper = classNames('space-y-2');

  const content = classNames(
    'group',
    'flex',
    'items-center',
    'p-2',
    'pl-10',
    'space-x-8',
    'rounded-md',
    'cursor-pointer',
    'hover:bg-gray-200',
  );

  const handleClick = (e: React.MouseEvent<HTMLElement>) => {
    e.preventDefault();

    ReactGA.event({
      category: 'reports',
      action: open ? 'collapse' : 'expand',
      label: report.rule.name,
    });

    setOpen(!open);
  };

  return (
    <div className={wrapper}>
      <div className={content}>
        <div className="flex-1 relative">
          <OutcomeIcon outcome={report.outcome} />

          <button
            className="hover:text-primary-500 hover:underline w-full text-left"
            onClick={handleClick}
            title={
              open
                ? t('report.collapsed', 'Show details')
                : t('report.expand', 'Hide details')
            }
            aria-controls={detailsId}
            aria-expanded={open}
          >
            <Typography className="mb-1" variant="h4" fontSize="lg" lang="en">
              {report.rule.name}
            </Typography>
          </button>

          <Typography
            fontStyle={report.message == null ? 'italic' : 'normal'}
            color="wash"
            lang="en"
          >
            {report.message ?? 'No message'}
          </Typography>

          <button
            className="text-primary-500 hover:underline w-full text-left"
            onClick={handleClick}
            aria-controls={detailsId}
            aria-expanded={open}
          >
            <Typography fontSize="sm">
              {open
                ? t('report.expanded', 'Hide details')
                : t('report.collapsed', 'Show details')}
            </Typography>
          </button>
        </div>

        <div
          className={classNames('inline-flex', 'flex-col', 'space-y-3', 'w-40')}
        >
          {report.impact && (
            <Badge variant={impactVariant} className="flex-shrink-0">
              {t('report.impact', 'Impact: {{impact}}', {
                impact: t(`impact.${report.impact.toLowerCase()}`),
              })}
            </Badge>
          )}

          {report.difficulty && (
            <Badge variant={difficultyVariant} className="flex-shrink-0">
              {t('report.difficulty', 'Difficulty: {{difficulty}}', {
                difficulty: t(`difficulty.${report.difficulty.toLowerCase()}`),
              })}
            </Badge>
          )}
        </div>

        {report.screenshot && (
          <div className="flex-shrink-0 ml-5">
            <Image
              src={report.screenshot}
              alt={
                report.target ?? t('report.no-desc', 'no description provided')
              }
              className="bg-black"
              variant="shadow"
              width="100px"
              height="100px"
            />
          </div>
        )}
      </div>

      {open && (
        <div
          id={detailsId}
          className={classNames('space-y-2', { hidden: !open })}
        >
          <Status report={report} title={url} />
          {withEditor && report.diffHunk ? (
            <DiffCodeFrame
              hunk={report.diffHunk}
              filename={url}
              title={t('report.suggestion', 'Suggested change')}
            />
          ) : (
            <CodeFrame
              title={t('report.code-frame', 'Code frame')}
              value={original}
              filename={url}
              highlightColor={report.outcome === Outcome.Fail ? 'red' : 'green'}
              highlightStart={report.location?.startLine}
              highlightEnd={report.location?.endLine}
            />
          )}
          {report.rule.mapping?.[0] && (
            <Reference name={report.rule.mapping?.[0]} />
          )}
        </div>
      )}
    </div>
  );
};