@material-ui/icons#ChevronRight TypeScript Examples

The following examples show how to use @material-ui/icons#ChevronRight. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: poll.create.tsx    From anchor-web-app with Apache License 2.0 6 votes vote down vote up
function PollLink({
  to,
  title,
  description,
}: {
  to: string;
  title: ReactNode;
  description: ReactNode;
}) {
  return (
    <li>
      <Link to={to}>
        <HorizontalRuler />
        <div>
          <div>
            <h3>{title}</h3>
            <p>{description}</p>
          </div>
          <ChevronRight />
        </div>
      </Link>
    </li>
  );
}
Example #2
Source File: index.tsx    From anchor-web-app with Apache License 2.0 4 votes vote down vote up
function OverviewBase({ className }: OverviewProps) {
  const {
    target: { isNative },
  } = useDeploymentTarget();

  const { contractAddress } = useAnchorWebapp();

  const { data: ancPrice } = useAssetPriceInUstQuery('anc');

  const { data: { govRewards, lpRewards } = {} } = useBorrowAPYQuery();

  const { data: ancUstLpRewards } = useRewardsAncUstLpRewardsQuery();

  const navigate = useNavigate();

  const { data: { ancTokenInfo } = {} } = useAncTokenInfoQuery();
  const { data: { ancBalance: govANCBalance } = {} } = useAncBalanceQuery(
    contractAddress.anchorToken.gov,
  );
  const { data: { ancBalance: communityANCBalance } = {} } = useAncBalanceQuery(
    contractAddress.anchorToken.community,
  );
  const { data: { ancBalance: distributorANCBalance } = {} } =
    useAncBalanceQuery(contractAddress.anchorToken.distributor);
  // FIXME remain lp total staked
  const { data: { ancBalance: lpStakingANCBalance } = {} } = useAncBalanceQuery(
    contractAddress.anchorToken.staking,
  );
  const { data: { ancBalance: airdropANCBalance } = {} } = useAncBalanceQuery(
    'terra146ahqn6d3qgdvmj8cj96hh03dzmeedhsf0kxqm' as HumanAddr,
  );
  const { data: { ancBalance: investorTeamLockANCBalance } = {} } =
    useAncBalanceQuery(
      'terra1dp0taj85ruc299rkdvzp4z5pfg6z6swaed74e6' as HumanAddr,
    );
  const { data: { govState, govConfig } = {} } = useGovStateQuery();

  const { data: { lpStakingState } = {} } = useAncLpStakingStateQuery();

  const ancUstLpAprTooltip = useMemo(() => {
    let defaultTooltip = 'LP rewards APR';

    if (lpRewards && lpRewards.length > 0 && lpRewards[0].apy) {
      return `${formatRate(lpRewards[0].apy)}% (if compounded daily)`;
    }

    return defaultTooltip;
  }, [lpRewards]);

  const { totalStaked, totalStakedRate } = useMemo(() => {
    if (
      !ancTokenInfo ||
      !govANCBalance ||
      !communityANCBalance ||
      !distributorANCBalance ||
      !lpStakingANCBalance ||
      !airdropANCBalance ||
      !investorTeamLockANCBalance ||
      !govState ||
      !govConfig
    ) {
      return {
        totalStaked: big(0) as u<ANC<Big>>,
        totalStakedRate: big(0) as Rate<Big>,
      };
    }

    const totalStaked = big(govANCBalance.balance).minus(
      govState.total_deposit,
    ) as u<ANC<Big>>;

    const currentTotalSupply = big(ancTokenInfo.total_supply)
      .minus(communityANCBalance.balance)
      .minus(distributorANCBalance.balance)
      .minus(lpStakingANCBalance.balance)
      .minus(airdropANCBalance.balance)
      .minus(investorTeamLockANCBalance.balance);

    const totalStakedRate = big(totalStaked).div(
      currentTotalSupply,
    ) as Rate<Big>;

    return { totalStaked, totalStakedRate };
  }, [
    airdropANCBalance,
    ancTokenInfo,
    communityANCBalance,
    distributorANCBalance,
    govANCBalance,
    govConfig,
    govState,
    investorTeamLockANCBalance,
    lpStakingANCBalance,
  ]);

  const ancRewards = ancUstLpRewards?.userLPPendingToken?.pending_on_proxy;
  const astroRewards = ancUstLpRewards?.userLPPendingToken?.pending;

  const hasAstroRewards = astroRewards && !big(astroRewards).eq(0);
  const hasAncRewards = ancRewards && !big(ancRewards).eq(0);
  const hasRewards = hasAstroRewards || hasAncRewards;

  return (
    <div className={className}>
      <Section className="anc-price">
        <h2>
          <IconSpan>
            ANC PRICE{' '}
            <InfoTooltip>
              AMM price of ANC that is determined by the current pool ratio
            </InfoTooltip>
          </IconSpan>
        </h2>
        <div>
          <AnimateNumber format={formatUSTWithPostfixUnits}>
            {ancPrice || ('0' as UST)}
          </AnimateNumber>{' '}
          <Sub>UST</Sub>
        </div>
      </Section>
      <Section className="total-staked">
        <h2>
          <IconSpan>
            TOTAL STAKED{' '}
            <InfoTooltip>
              Total quantity of ANC tokens staked to the governance contract
            </InfoTooltip>
          </IconSpan>
        </h2>
        <div>
          <AnimateNumber format={formatUTokenDecimal2}>
            {totalStaked}
          </AnimateNumber>{' '}
          <Sub>
            ANC{' '}
            <span>
              (
              <AnimateNumber format={formatRate}>
                {totalStakedRate}
              </AnimateNumber>
              %)
            </span>
          </Sub>
        </div>
      </Section>
      <Section className="staking">
        <Circles backgroundColors={['#2C2C2C']}>
          <GifIcon
            src={anc160gif}
            style={{ fontSize: '2em', borderRadius: '50%' }}
          />
        </Circles>
        <h2>Anchor (ANC)</h2>
        <div className="staking-apy">
          <TooltipLabel
            title="Annualized ANC staking return based on the ANC distribution and staking ratio"
            placement="top"
          >
            APR
          </TooltipLabel>
          <span style={{ display: 'inline-block', minWidth: 80 }}>
            <AnimateNumber format={formatRate}>
              {govRewards && govRewards.length > 0
                ? govRewards[0].CurrentAPY
                : (0 as Rate<number>)}
            </AnimateNumber>{' '}
            %
          </span>
        </div>
        {isNative && (
          <div className="staking-buttons">
            <BorderButton component={Link} to={`/trade`}>
              Trade ANC
            </BorderButton>
            <Tooltip
              title="Stake ANC to participate in governance voting or to obtain governance rewards"
              placement="top"
            >
              <BorderButton
                component={Link}
                to={`/${ancGovernancePathname}/stake`}
              >
                Gov Stake
              </BorderButton>
            </Tooltip>
          </div>
        )}
      </Section>
      <Section
        className={isNative ? 'lp lp-action' : 'lp'}
        onClick={() => isNative && navigate(`/${ancUstLpPathname}/provide`)}
      >
        <Circles backgroundColors={['#ffffff', '#2C2C2C']}>
          <TokenIcon token="ust" style={{ fontSize: '1.1em' }} />
          <GifIcon
            src={anc160gif}
            style={{ fontSize: '2em', borderRadius: '50%' }}
          />
        </Circles>
        <h2>
          <IconSpan>ANC-UST LP {isNative && <ChevronRight />}</IconSpan>
        </h2>
        <div className="lp-labels">
          {hasRewards && (
            <div>
              <TooltipLabel title="Rewards" placement="top">
                Rewards
              </TooltipLabel>
              {hasAncRewards && (
                <p>{formatOutput(demicrofy(ancRewards))} ANC</p>
              )}
              {hasAstroRewards && (
                <p>{formatOutput(demicrofy(astroRewards))} ASTRO</p>
              )}
            </div>
          )}
          <div>
            <TooltipLabel title={ancUstLpAprTooltip} placement="top">
              APR
            </TooltipLabel>
            <p>
              <AnimateNumber format={formatRate}>
                {lpRewards && lpRewards.length > 0
                  ? lpRewards[0].apr
                  : (0 as Rate<number>)}
              </AnimateNumber>{' '}
              %
            </p>
          </div>
          <div>
            <TooltipLabel
              title="Total quantity of ANC-UST LP tokens staked"
              placement="top"
            >
              Total Staked
            </TooltipLabel>
            <p>
              <AnimateNumber format={formatUTokenDecimal2}>
                {lpStakingState?.total_bond_amount
                  ? lpStakingState.total_bond_amount
                  : (0 as u<Token<number>>)}
              </AnimateNumber>
            </p>
          </div>
        </div>
      </Section>
    </div>
  );
}
Example #3
Source File: MetricAssignmentResults.tsx    From abacus with GNU General Public License v2.0 4 votes vote down vote up
/**
 * Display results for a MetricAssignment
 */
export default function MetricAssignmentResults({
  strategy,
  metricAssignment,
  metric,
  analysesByStrategyDateAsc,
  experiment,
  recommendation,
  variationDiffKey,
}: {
  strategy: AnalysisStrategy
  metricAssignment: MetricAssignment
  metric: Metric
  analysesByStrategyDateAsc: Record<AnalysisStrategy, Analysis[]>
  experiment: ExperimentFull
  recommendation: Recommendations.Recommendation
  variationDiffKey: string
}): JSX.Element | null {
  const classes = useStyles()

  const [isShowObservedData, setIsShowObservedData] = useState<boolean>(false)
  const toggleIsShowObservedData = () => {
    setIsShowObservedData((isShowObservedData) => !isShowObservedData)
  }

  const isConversion = metric.parameterType === MetricParameterType.Conversion
  const estimateTransform: (estimate: number | null) => number | null = isConversion
    ? (estimate: number | null) => estimate && estimate * 100
    : identity
  const analyses = analysesByStrategyDateAsc[strategy]
  const latestAnalysis = _.last(analyses)
  const latestEstimates = latestAnalysis?.metricEstimates
  if (!latestAnalysis || !latestEstimates) {
    return <MissingAnalysisMessage />
  }

  const [_changeVariationId, baseVariationId] = variationDiffKey.split('_')

  const dates = analyses.map(({ analysisDatetime }) => analysisDatetime.toISOString())

  const plotlyDataVariationGraph: Array<Partial<PlotData>> = [
    ..._.flatMap(experiment.variations, (variation, index) => {
      return [
        {
          name: `${variation.name}: lower bound`,
          x: dates,
          y: analyses
            .map(
              ({ metricEstimates }) => metricEstimates && metricEstimates.variations[variation.variationId].bottom_95,
            )
            .map(estimateTransform),
          line: {
            color: Visualizations.variantColors[index],
          },
          mode: 'lines' as const,
          type: 'scatter' as const,
        },
        {
          name: `${variation.name}: upper bound`,
          x: dates,
          y: analyses
            .map(({ metricEstimates }) => metricEstimates && metricEstimates.variations[variation.variationId].top_95)
            .map(estimateTransform),
          line: {
            color: Visualizations.variantColors[index],
          },
          fill: 'tonexty' as const,
          fillcolor: Visualizations.variantColors[index],
          mode: 'lines' as const,
          type: 'scatter' as const,
        },
      ]
    }),
  ]

  const plotlyDataDifferenceGraph: Array<Partial<PlotData>> = [
    {
      name: `difference: 99% lower bound`,
      x: dates,
      y: analyses
        .map(({ metricEstimates }) => metricEstimates && metricEstimates.diffs[variationDiffKey].bottom_99)
        .map(estimateTransform),
      line: { width: 0 },
      marker: { color: '444' },
      mode: 'lines' as const,
      type: 'scatter' as const,
    },
    {
      name: `difference: 99% upper bound`,
      x: dates,
      y: analyses
        .map(({ metricEstimates }) => metricEstimates && metricEstimates.diffs[variationDiffKey].top_99)
        .map(estimateTransform),
      fill: 'tonexty',
      fillcolor: 'rgba(0,0,0,.2)',
      line: { width: 0 },
      marker: { color: '444' },
      mode: 'lines' as const,
      type: 'scatter' as const,
    },
    {
      name: `difference: 95% lower bound`,
      x: dates,
      y: analyses
        .map(({ metricEstimates }) => metricEstimates && metricEstimates.diffs[variationDiffKey].bottom_95)
        .map(estimateTransform),
      line: { width: 0 },
      marker: { color: '444' },
      mode: 'lines' as const,
      type: 'scatter' as const,
    },
    {
      name: `difference: 95% upper bound`,
      x: dates,
      y: analyses
        .map(({ metricEstimates }) => metricEstimates && metricEstimates.diffs[variationDiffKey].top_95)
        .map(estimateTransform),
      fill: 'tonexty',
      fillcolor: 'rgba(0,0,0,.2)',
      line: { width: 0 },
      marker: { color: '444' },
      mode: 'lines' as const,
      type: 'scatter' as const,
    },
    {
      name: `difference: 50% lower bound`,
      x: dates,
      y: analyses
        .map(({ metricEstimates }) => metricEstimates && metricEstimates.diffs[variationDiffKey].bottom_50)
        .map(estimateTransform),
      line: { width: 0 },
      marker: { color: '444' },
      mode: 'lines' as const,
      type: 'scatter' as const,
    },
    {
      name: `difference: 50% upper bound`,
      x: dates,
      y: analyses
        .map(({ metricEstimates }) => metricEstimates && metricEstimates.diffs[variationDiffKey].top_50)
        .map(estimateTransform),
      fill: 'tonexty',
      fillcolor: 'rgba(0,0,0,.2)',
      line: { width: 0 },
      marker: { color: '444' },
      mode: 'lines' as const,
      type: 'scatter' as const,
    },
    {
      name: 'ROPE: lower bound',
      x: dates,
      y: analyses.map((_) => -metricAssignment.minDifference).map(estimateTransform),
      line: {
        color: 'rgba(0,0,0,.4)',
        dash: 'dash',
      },
      mode: 'lines' as const,
      type: 'scatter' as const,
    },
    {
      name: 'ROPE: upper bound',
      x: dates,
      y: analyses.map((_) => metricAssignment.minDifference).map(estimateTransform),
      line: {
        color: 'rgba(0,0,0,.4)',
        dash: 'dash',
      },
      mode: 'lines' as const,
      type: 'scatter' as const,
    },
  ]

  return (
    <div className={clsx(classes.root, 'analysis-detail-panel')}>
      <Typography className={classes.dataTableHeader}>Summary</Typography>
      <TableContainer component={Paper}>
        <Table>
          <TableBody>
            <TableRow>
              <TableCell>
                <Typography variant='h5' gutterBottom className={classes.recommendation}>
                  <AnalysisDisplay {...{ experiment, analysis: recommendation }} />
                </Typography>
                {recommendation.decision === Recommendations.Decision.ManualAnalysisRequired && (
                  <Typography variant='body1' gutterBottom>
                    <strong> Different strategies are recommending conflicting variations! </strong>
                  </Typography>
                )}
                <Typography variant='body1'>
                  {getOverviewMessage(experiment, recommendation)}{' '}
                  <Link
                    href={`https://github.com/Automattic/experimentation-platform/wiki/Experimenter's-Guide#reading-the-data`}
                    target='_blank'
                  >
                    Learn more
                  </Link>
                </Typography>
              </TableCell>
            </TableRow>
            <TableRow>
              <TableCell>
                <Typography variant='body1' gutterBottom>
                  The absolute change in the {isConversion ? 'conversion rate' : 'ARPU'} of{' '}
                  <MetricValue
                    metricParameterType={metric.parameterType}
                    isDifference={true}
                    value={latestEstimates.diffs[variationDiffKey].bottom_95}
                    displayPositiveSign
                    displayUnit={false}
                  />{' '}
                  to{' '}
                  <MetricValue
                    metricParameterType={metric.parameterType}
                    isDifference={true}
                    value={latestEstimates.diffs[variationDiffKey].top_95}
                    displayPositiveSign
                  />{' '}
                  is {recommendation.statisticallySignificant ? '' : ' not '}
                  statistically different from zero because the interval
                  {recommendation.statisticallySignificant ? ' excludes ' : ' includes '}
                  zero.{' '}
                  {
                    explanationLine2[
                      recommendation.practicallySignificant as Recommendations.PracticalSignificanceStatus
                    ]
                  }
                  <MetricValue
                    metricParameterType={metric.parameterType}
                    isDifference={true}
                    value={-metricAssignment.minDifference}
                    displayPositiveSign
                    displayUnit={false}
                  />{' '}
                  to{' '}
                  <MetricValue
                    metricParameterType={metric.parameterType}
                    isDifference={true}
                    value={metricAssignment.minDifference}
                    displayPositiveSign
                  />
                  .
                </Typography>
                <strong>Last analyzed:</strong>{' '}
                <DatetimeText datetime={latestAnalysis.analysisDatetime} excludeTime={true} />.
              </TableCell>
            </TableRow>
            <TableRow>
              <TableCell>
                <strong>Metric description:</strong> {metric.description}
              </TableCell>
            </TableRow>
          </TableBody>
        </Table>
      </TableContainer>
      <Typography className={classes.dataTableHeader}>Analysis</Typography>
      <TableContainer component={Paper}>
        <Table className={classes.coolTable}>
          <TableHead>
            <TableRow>
              <TableCell>Variant</TableCell>
              <TableCell align='right'>
                {metric.parameterType === MetricParameterType.Revenue
                  ? 'Average revenue per user (ARPU) interval'
                  : 'Conversion rate interval'}
              </TableCell>
              <TableCell align='right'>Absolute change</TableCell>
              <TableCell align='right'>Relative change (lift)</TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {experiment.variations.map((variation) => (
              <React.Fragment key={variation.variationId}>
                <TableRow>
                  <TableCell
                    component='th'
                    scope='row'
                    variant='head'
                    valign='top'
                    className={clsx(classes.rowHeader, classes.headerCell, classes.credibleIntervalHeader)}
                  >
                    <span className={classes.monospace}>{variation.name}</span>
                  </TableCell>
                  <TableCell className={classes.monospace} align='right'>
                    <MetricValueInterval
                      intervalName={'the metric value'}
                      metricParameterType={metric.parameterType}
                      bottomValue={latestEstimates.variations[variation.variationId].bottom_95}
                      topValue={latestEstimates.variations[variation.variationId].top_95}
                      displayPositiveSign={false}
                    />
                  </TableCell>
                  <TableCell className={classes.monospace} align='right'>
                    {variation.isDefault ? (
                      'Baseline'
                    ) : (
                      <MetricValueInterval
                        intervalName={'the absolute change between variations'}
                        metricParameterType={metric.parameterType}
                        isDifference={true}
                        bottomValue={latestEstimates.diffs[`${variation.variationId}_${baseVariationId}`].bottom_95}
                        topValue={latestEstimates.diffs[`${variation.variationId}_${baseVariationId}`].top_95}
                      />
                    )}
                  </TableCell>
                  <TableCell className={classes.monospace} align='right'>
                    {variation.isDefault ? (
                      'Baseline'
                    ) : (
                      <MetricValueInterval
                        intervalName={'the relative change between variations'}
                        metricParameterType={MetricParameterType.Conversion}
                        bottomValue={Analyses.ratioToDifferenceRatio(
                          latestEstimates.ratios[`${variation.variationId}_${baseVariationId}`].bottom_95,
                        )}
                        topValue={Analyses.ratioToDifferenceRatio(
                          latestEstimates.ratios[`${variation.variationId}_${baseVariationId}`].top_95,
                        )}
                      />
                    )}
                  </TableCell>
                </TableRow>
              </React.Fragment>
            ))}
          </TableBody>
        </Table>
      </TableContainer>
      <Typography className={classes.analysisFinePrint}>
        95% Credible Intervals (CIs). <strong> Experimenter-set minimum practical difference: </strong>{' '}
        <MetricValue
          value={metricAssignment.minDifference}
          metricParameterType={metric.parameterType}
          isDifference={true}
        />
        .
      </Typography>
      {dates.length > 1 ? (
        <div className={classes.metricEstimatePlots}>
          <Plot
            layout={{
              ...Visualizations.plotlyLayoutDefault,
              title: isConversion
                ? `Conversion rate estimates by variation (%)`
                : `Revenue estimates by variation (USD)`,
            }}
            data={plotlyDataVariationGraph}
            className={classes.metricEstimatePlot}
          />
          <Plot
            layout={{
              ...Visualizations.plotlyLayoutDefault,
              title: isConversion
                ? `Conversion rate difference estimates (percentage points)`
                : `Revenue difference estimates (USD)`,
            }}
            data={plotlyDataDifferenceGraph}
            className={classes.metricEstimatePlot}
          />
        </div>
      ) : (
        <Typography variant='body1' className={classes.noPlotMessage}>
          Past values will be plotted once we have more than one day of results.
        </Typography>
      )}
      <Typography
        className={clsx(classes.dataTableHeader, classes.clickable)}
        onClick={toggleIsShowObservedData}
        role='button'
      >
        {isShowObservedData ? (
          <ExpandMore className={classes.expandCollapseIcon} />
        ) : (
          <ChevronRight className={classes.expandCollapseIcon} />
        )}
        &quot;Observed&quot; data
      </Typography>
      {isShowObservedData && (
        <>
          <TableContainer component={Paper}>
            <Table className={classes.coolTable}>
              <TableHead>
                <TableRow>
                  <TableCell>Variant</TableCell>
                  <TableCell align='right'>Users</TableCell>
                  <TableCell align='right'>
                    {metric.parameterType === MetricParameterType.Revenue ? 'Revenue' : 'Conversions'}
                  </TableCell>
                  <TableCell align='right'>
                    {metric.parameterType === MetricParameterType.Revenue
                      ? 'Average revenue per user (ARPU)'
                      : 'Conversion rate'}
                  </TableCell>
                </TableRow>
              </TableHead>
              <TableBody>
                {experiment.variations.map((variation) => (
                  <React.Fragment key={variation.variationId}>
                    <TableRow>
                      <TableCell
                        component='th'
                        scope='row'
                        variant='head'
                        valign='top'
                        className={clsx(classes.rowHeader, classes.headerCell, classes.credibleIntervalHeader)}
                      >
                        <span className={classes.monospace}>{variation.name}</span>
                      </TableCell>
                      <TableCell className={classes.monospace} align='right'>
                        {latestAnalysis.participantStats[`variation_${variation.variationId}`].toLocaleString()}
                      </TableCell>
                      <TableCell className={classes.monospace} align='right'>
                        <MetricValue
                          value={
                            latestAnalysis.participantStats[`variation_${variation.variationId}`] *
                            latestEstimates.variations[variation.variationId].mean
                          }
                          metricParameterType={
                            metric.parameterType === MetricParameterType.Conversion
                              ? MetricParameterType.Count
                              : metric.parameterType
                          }
                        />
                      </TableCell>
                      <TableCell className={classes.monospace} align='right'>
                        <MetricValue
                          value={latestEstimates.variations[variation.variationId].mean}
                          metricParameterType={metric.parameterType}
                        />
                      </TableCell>
                    </TableRow>
                  </React.Fragment>
                ))}
              </TableBody>
            </Table>
          </TableContainer>
          <Typography variant='caption' gutterBottom>
            <Link href='https://wp.me/PCYsg-Fqg/#observed-data-uses-posterior-means' target='_blank'>
              &quot;Observed&quot; data as produced from our model, not raw observed data.
            </Link>{' '}
            For illustrative purposes only.
          </Typography>
        </>
      )}
    </div>
  )
}
Example #4
Source File: index.tsx    From prism-frontend with MIT License 4 votes vote down vote up
function DateSelector({ availableDates = [], classes }: DateSelectorProps) {
  const { startDate: stateStartDate } = useSelector(dateRangeSelector);
  const { t, i18n } = useSafeTranslation();
  const [dateRange, setDateRange] = useState<DateRangeType[]>([
    {
      value: 0,
      label: '',
      month: '',
      isFirstDay: false,
    },
  ]);

  const [timelinePosition, setTimelinePosition] = useState<Point>({
    x: 0,
    y: 0,
  });
  const [pointerPosition, setPointerPosition] = useState<Point>({ x: 0, y: 0 });

  const dateRef = useRef(availableDates);

  const timeLine = useRef(null);
  const timeLineWidth = get(timeLine.current, 'offsetWidth', 0);

  const { updateHistory } = useUrlHistory();

  // Move the slider automatically so that the pointer always visible
  useEffect(() => {
    let x = 0;
    if (
      pointerPosition.x >=
      dateRange.length * TIMELINE_ITEM_WIDTH - timeLineWidth
    ) {
      // eslint-disable-next-line fp/no-mutation
      x = timeLineWidth - dateRange.length * TIMELINE_ITEM_WIDTH;
    } else if (pointerPosition.x > timeLineWidth) {
      // eslint-disable-next-line fp/no-mutation
      x = -pointerPosition.x + timeLineWidth / 2;
    }
    if (
      -timelinePosition.x > pointerPosition.x ||
      -timelinePosition.x + timeLineWidth < pointerPosition.x
    ) {
      setTimelinePosition({ x, y: 0 });
    }
  }, [dateRange.length, pointerPosition, timeLineWidth, timelinePosition.x]);

  // Create timeline range and set pointer position
  useEffect(() => {
    const locale = t('date_locale') ? t('date_locale') : 'en';
    const range = Array.from(
      moment()
        .range(
          moment(stateStartDate).startOf('year'),
          moment(stateStartDate).endOf('year'),
        )
        .by('days'),
    ).map(date => {
      date.locale(locale);
      return {
        value: date.valueOf(),
        label: date.format(MONTH_FIRST_DATE_FORMAT),
        month: date.format(MONTH_ONLY_DATE_FORMAT),
        isFirstDay: date.date() === date.startOf('month').date(),
      };
    });
    setDateRange(range);
    const dateIndex = findIndex(range, date => {
      return (
        date.label ===
        moment(stateStartDate).locale(locale).format(MONTH_FIRST_DATE_FORMAT)
      );
    });
    setPointerPosition({
      x: dateIndex * TIMELINE_ITEM_WIDTH,
      y: 0,
    });
  }, [stateStartDate, t, i18n]);

  function updateStartDate(date: Date) {
    const time = date.getTime();
    // This updates state because a useEffect in MapView updates the redux state
    // TODO this is convoluted coupling, we should update state here if feasible.
    updateHistory('date', moment(time).format(DEFAULT_DATE_FORMAT));
  }

  function setDatePosition(date: number | undefined, increment: number) {
    const dates = availableDates.map(d => {
      return d + USER_DATE_OFFSET;
    });
    const selectedIndex = findDateIndex(dates, date);
    if (dates[selectedIndex + increment]) {
      updateStartDate(new Date(dates[selectedIndex + increment]));
    }
  }

  // move pointer to closest date when change map layer
  useEffect(() => {
    if (!isEqual(dateRef.current, availableDates)) {
      setDatePosition(stateStartDate, 0);
      dateRef.current = availableDates;
    }
  });

  function incrementDate() {
    setDatePosition(stateStartDate, 1);
  }

  function decrementDate() {
    setDatePosition(stateStartDate, -1);
  }

  // Click on available date to move the pointer
  const clickDate = (index: number) => {
    const dates = availableDates.map(date => {
      return date + USER_DATE_OFFSET;
    });
    const selectedIndex = findDateIndex(dates, dateRange[index].value);
    if (selectedIndex >= 0 && dates[selectedIndex] !== stateStartDate) {
      setPointerPosition({ x: index * TIMELINE_ITEM_WIDTH, y: 0 });
      updateStartDate(new Date(dates[selectedIndex]));
    }
  };

  // Set timeline position after being dragged
  const onTimelineStop = (e: DraggableEvent, position: Point) => {
    setTimelinePosition(position);
  };

  // Set pointer position after being dragged
  const onPointerStop = (e: DraggableEvent, position: Point) => {
    const exactX = Math.round(position.x / TIMELINE_ITEM_WIDTH);
    if (exactX >= dateRange.length) {
      return;
    }
    const dates = availableDates.map(date => {
      return date + USER_DATE_OFFSET;
    });
    const selectedIndex = findDateIndex(dates, dateRange[exactX].value);
    if (selectedIndex >= 0 && dates[selectedIndex] !== stateStartDate) {
      setPointerPosition({ x: exactX * TIMELINE_ITEM_WIDTH, y: position.y });
      updateStartDate(new Date(dates[selectedIndex]));
    }
  };

  return (
    <div className={classes.container}>
      <Grid
        container
        alignItems="center"
        justify="center"
        className={classes.datePickerContainer}
      >
        <Grid item xs={12} sm={1} className={classes.datePickerGrid}>
          <Hidden smUp>
            <Button onClick={decrementDate}>
              <ChevronLeft />
            </Button>
          </Hidden>

          <DatePicker
            locale={t('date_locale')}
            dateFormat="PP"
            className={classes.datePickerInput}
            selected={moment(stateStartDate).toDate()}
            onChange={updateStartDate}
            maxDate={new Date()}
            todayButton={t('Today')}
            peekNextMonth
            showMonthDropdown
            showYearDropdown
            dropdownMode="select"
            customInput={<Input />}
            includeDates={availableDates.map(
              d => new Date(d + USER_DATE_OFFSET),
            )}
          />

          <Hidden smUp>
            <Button onClick={incrementDate}>
              <ChevronRight />
            </Button>
          </Hidden>
        </Grid>

        <Grid item xs={12} sm className={classes.slider}>
          <Hidden xsDown>
            <Button onClick={decrementDate}>
              <ChevronLeft />
            </Button>
          </Hidden>
          <Grid className={classes.dateContainer} ref={timeLine}>
            <Draggable
              axis="x"
              handle={`#${TIMELINE_ID}`}
              bounds={{
                top: 0,
                bottom: 0,
                right: 0,
                left: timeLineWidth - dateRange.length * TIMELINE_ITEM_WIDTH,
              }}
              position={timelinePosition}
              onStop={onTimelineStop}
            >
              <div className={classes.timeline} id={TIMELINE_ID}>
                <Grid
                  container
                  alignItems="stretch"
                  className={classes.dateLabelContainer}
                >
                  <TimelineItems
                    dateRange={dateRange}
                    availableDates={availableDates}
                    clickDate={clickDate}
                  />
                </Grid>
                <Draggable
                  axis="x"
                  handle={`#${POINTER_ID}`}
                  bounds={{
                    top: 0,
                    bottom: 0,
                    left: 0,
                    right: dateRange.length * TIMELINE_ITEM_WIDTH,
                  }}
                  grid={[TIMELINE_ITEM_WIDTH, 1]}
                  position={pointerPosition}
                  onStart={(e: DraggableEvent) => e.stopPropagation()}
                  onStop={onPointerStop}
                >
                  <div className={classes.pointer} id={POINTER_ID}>
                    <FontAwesomeIcon
                      icon={faCaretUp}
                      style={{ fontSize: 40 }}
                      color="white"
                    />
                  </div>
                </Draggable>
              </div>
            </Draggable>
          </Grid>
          <Hidden xsDown>
            <Button onClick={incrementDate}>
              <ChevronRight />
            </Button>
          </Hidden>
        </Grid>
      </Grid>
    </div>
  );
}
Example #5
Source File: Organization.tsx    From crossfeed with Creative Commons Zero v1.0 Universal 4 votes vote down vote up
Organization: React.FC = () => {
  const {
    apiGet,
    apiPut,
    apiPost,
    user,
    setFeedbackMessage
  } = useAuthContext();
  const { organizationId } = useParams<{ organizationId: string }>();
  const [organization, setOrganization] = useState<OrganizationType>();
  const [tags, setTags] = useState<AutocompleteType[]>([]);
  const [userRoles, setUserRoles] = useState<Role[]>([]);
  const [scanTasks, setScanTasks] = useState<ScanTask[]>([]);
  const [scans, setScans] = useState<Scan[]>([]);
  const [scanSchema, setScanSchema] = useState<ScanSchema>({});
  const [newUserValues, setNewUserValues] = useState<{
    firstName: string;
    lastName: string;
    email: string;
    organization?: OrganizationType;
    role: string;
  }>({
    firstName: '',
    lastName: '',
    email: '',
    role: ''
  });
  const classes = useStyles();
  const [tagValue, setTagValue] = React.useState<AutocompleteType | null>(null);
  const [inputValue, setInputValue] = React.useState('');
  const [dialog, setDialog] = React.useState<{
    open: boolean;
    type?: 'rootDomains' | 'ipBlocks' | 'tags';
    label?: string;
  }>({ open: false });

  const dateAccessor = (date?: string) => {
    return !date || new Date(date).getTime() === new Date(0).getTime()
      ? 'None'
      : `${formatDistanceToNow(parseISO(date))} ago`;
  };

  const userRoleColumns: Column<Role>[] = [
    {
      Header: 'Name',
      accessor: ({ user }) => user.fullName,
      width: 200,
      disableFilters: true,
      id: 'name'
    },
    {
      Header: 'Email',
      accessor: ({ user }) => user.email,
      width: 150,
      minWidth: 150,
      id: 'email',
      disableFilters: true
    },
    {
      Header: 'Role',
      accessor: ({ approved, role, user }) => {
        if (approved) {
          if (user.invitePending) {
            return 'Invite pending';
          } else if (role === 'admin') {
            return 'Administrator';
          } else {
            return 'Member';
          }
        }
        return 'Pending approval';
      },
      width: 50,
      minWidth: 50,
      id: 'approved',
      disableFilters: true
    },
    {
      Header: () => {
        return (
          <div style={{ justifyContent: 'flex-center' }}>
            <Button color="secondary" onClick={() => setDialog({ open: true })}>
              <ControlPoint style={{ marginRight: '10px' }}></ControlPoint>
              Add member
            </Button>
          </div>
        );
      },
      id: 'action',
      Cell: ({ row }: { row: { index: number } }) => {
        const isApproved =
          !organization?.userRoles[row.index] ||
          organization?.userRoles[row.index].approved;
        return (
          <>
            {isApproved ? (
              <Button
                onClick={() => {
                  removeUser(row.index);
                }}
                color="secondary"
              >
                <p>Remove</p>
              </Button>
            ) : (
              <Button
                onClick={() => {
                  approveUser(row.index);
                }}
                color="secondary"
              >
                <p>Approve</p>
              </Button>
            )}
          </>
        );
      },
      disableFilters: true
    }
  ];

  const scanColumns: Column<Scan>[] = [
    {
      Header: 'Name',
      accessor: 'name',
      width: 150,
      id: 'name',
      disableFilters: true
    },
    {
      Header: 'Description',
      accessor: ({ name }) => scanSchema[name] && scanSchema[name].description,
      width: 200,
      minWidth: 200,
      id: 'description',
      disableFilters: true
    },
    {
      Header: 'Mode',
      accessor: ({ name }) =>
        scanSchema[name] && scanSchema[name].isPassive ? 'Passive' : 'Active',
      width: 150,
      minWidth: 150,
      id: 'mode',
      disableFilters: true
    },
    {
      Header: 'Action',
      id: 'action',
      maxWidth: 100,
      Cell: ({ row }: { row: { index: number } }) => {
        if (!organization) return;
        const enabled = organization.granularScans.find(
          (scan) => scan.id === scans[row.index].id
        );
        return (
          <Button
            type="button"
            onClick={() => {
              updateScan(scans[row.index], !enabled);
            }}
          >
            {enabled ? 'Disable' : 'Enable'}
          </Button>
        );
      },
      disableFilters: true
    }
  ];

  const scanTaskColumns: Column<ScanTask>[] = [
    {
      Header: 'ID',
      accessor: 'id',
      disableFilters: true
    },
    {
      Header: 'Status',
      accessor: 'status',
      disableFilters: true
    },
    {
      Header: 'Type',
      accessor: 'type',
      disableFilters: true
    },
    {
      Header: 'Name',
      accessor: ({ scan }) => scan?.name,
      disableFilters: true
    },
    {
      Header: 'Created At',
      accessor: ({ createdAt }) => dateAccessor(createdAt),
      disableFilters: true,
      disableSortBy: true
    },
    {
      Header: 'Requested At',
      accessor: ({ requestedAt }) => dateAccessor(requestedAt),
      disableFilters: true,
      disableSortBy: true
    },
    {
      Header: 'Started At',
      accessor: ({ startedAt }) => dateAccessor(startedAt),
      disableFilters: true,
      disableSortBy: true
    },
    {
      Header: 'Finished At',
      accessor: ({ finishedAt }) => dateAccessor(finishedAt),
      disableFilters: true,
      disableSortBy: true
    },
    {
      Header: 'Output',
      accessor: 'output',
      disableFilters: true
    }
  ];

  const fetchOrganization = useCallback(async () => {
    try {
      const organization = await apiGet<OrganizationType>(
        `/organizations/${organizationId}`
      );
      organization.scanTasks.sort(
        (a, b) =>
          new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
      );
      setOrganization(organization);
      setUserRoles(organization.userRoles);
      setScanTasks(organization.scanTasks);
      const tags = await apiGet<OrganizationTag[]>(`/organizations/tags`);
      setTags(tags);
    } catch (e) {
      console.error(e);
    }
  }, [apiGet, setOrganization, organizationId]);

  const fetchScans = useCallback(async () => {
    try {
      const response = await apiGet<{
        scans: Scan[];
        schema: ScanSchema;
      }>('/granularScans/');
      let { scans } = response;
      const { schema } = response;

      if (user?.userType !== 'globalAdmin')
        scans = scans.filter(
          (scan) =>
            scan.name !== 'censysIpv4' && scan.name !== 'censysCertificates'
        );

      setScans(scans);
      setScanSchema(schema);
    } catch (e) {
      console.error(e);
    }
  }, [apiGet, user]);

  const approveUser = async (user: number) => {
    try {
      await apiPost(
        `/organizations/${organization?.id}/roles/${organization?.userRoles[user].id}/approve`,
        { body: {} }
      );
      const copy = userRoles.map((role, id) =>
        id === user ? { ...role, approved: true } : role
      );
      setUserRoles(copy);
    } catch (e) {
      console.error(e);
    }
  };

  const removeUser = async (user: number) => {
    try {
      await apiPost(
        `/organizations/${organization?.id}/roles/${userRoles[user].id}/remove`,
        { body: {} }
      );
      const copy = userRoles.filter((_, ind) => ind !== user);
      setUserRoles(copy);
    } catch (e) {
      console.error(e);
    }
  };

  const updateOrganization = async (body: any) => {
    try {
      const org = await apiPut('/organizations/' + organization?.id, {
        body: organization
      });
      setOrganization(org);
      setFeedbackMessage({
        message: 'Organization successfully updated',
        type: 'success'
      });
    } catch (e) {
      setFeedbackMessage({
        message:
          e.status === 422
            ? 'Error updating organization'
            : e.message ?? e.toString(),
        type: 'error'
      });
      console.error(e);
    }
  };

  const updateScan = async (scan: Scan, enabled: boolean) => {
    try {
      if (!organization) return;
      await apiPost(
        `/organizations/${organization?.id}/granularScans/${scan.id}/update`,
        {
          body: {
            enabled
          }
        }
      );
      setOrganization({
        ...organization,
        granularScans: enabled
          ? organization.granularScans.concat([scan])
          : organization.granularScans.filter(
              (granularScan) => granularScan.id !== scan.id
            )
      });
    } catch (e) {
      setFeedbackMessage({
        message:
          e.status === 422 ? 'Error updating scan' : e.message ?? e.toString(),
        type: 'error'
      });
      console.error(e);
    }
  };

  useEffect(() => {
    fetchOrganization();
  }, [fetchOrganization]);

  const onInviteUserSubmit = async () => {
    try {
      const body = {
        firstName: newUserValues.firstName,
        lastName: newUserValues.lastName,
        email: newUserValues.email,
        organization: organization?.id,
        organizationAdmin: newUserValues.role === 'admin'
      };
      const user: User = await apiPost('/users/', {
        body
      });
      const newRole = user.roles[user.roles.length - 1];
      newRole.user = user;
      if (userRoles.find((role) => role.user.id === user.id)) {
        setUserRoles(
          userRoles.map((role) => (role.user.id === user.id ? newRole : role))
        );
      } else {
        setUserRoles(userRoles.concat([newRole]));
      }
    } catch (e) {
      setFeedbackMessage({
        message:
          e.status === 422 ? 'Error inviting user' : e.message ?? e.toString(),
        type: 'error'
      });
      console.log(e);
    }
  };

  const onInviteUserTextChange: React.ChangeEventHandler<
    HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
  > = (e) => onInviteUserChange(e.target.name, e.target.value);

  const onInviteUserChange = (name: string, value: any) => {
    setNewUserValues((values) => ({
      ...values,
      [name]: value
    }));
  };
  const filter = createFilterOptions<AutocompleteType>();

  const ListInput = (props: {
    type: 'rootDomains' | 'ipBlocks' | 'tags';
    label: string;
  }) => {
    if (!organization) return null;
    const elements: (string | OrganizationTag)[] = organization[props.type];
    return (
      <div className={classes.headerRow}>
        <label>{props.label}</label>
        <span>
          {elements &&
            elements.map((value: string | OrganizationTag, index: number) => (
              <Chip
                className={classes.chip}
                key={index}
                label={typeof value === 'string' ? value : value.name}
                onDelete={() => {
                  organization[props.type].splice(index, 1);
                  setOrganization({ ...organization });
                }}
              ></Chip>
            ))}
          <Chip
            label="ADD"
            variant="outlined"
            color="secondary"
            onClick={() => {
              setDialog({
                open: true,
                type: props.type,
                label: props.label
              });
            }}
          />
        </span>
      </div>
    );
  };

  if (!organization) return null;

  const views = [
    <Paper className={classes.settingsWrapper} key={0}>
      <Dialog
        open={dialog.open}
        onClose={() => setDialog({ open: false })}
        aria-labelledby="form-dialog-title"
        maxWidth="xs"
        fullWidth
      >
        <DialogTitle id="form-dialog-title">
          Add {dialog.label && dialog.label.slice(0, -1)}
        </DialogTitle>
        <DialogContent>
          {dialog.type === 'tags' ? (
            <>
              <DialogContentText>
                Select an existing tag or add a new one.
              </DialogContentText>
              <Autocomplete
                value={tagValue}
                onChange={(event, newValue) => {
                  if (typeof newValue === 'string') {
                    setTagValue({
                      name: newValue
                    });
                  } else {
                    setTagValue(newValue);
                  }
                }}
                filterOptions={(options, params) => {
                  const filtered = filter(options, params);
                  // Suggest the creation of a new value
                  if (
                    params.inputValue !== '' &&
                    !filtered.find(
                      (tag) =>
                        tag.name?.toLowerCase() ===
                        params.inputValue.toLowerCase()
                    )
                  ) {
                    filtered.push({
                      name: params.inputValue,
                      title: `Add "${params.inputValue}"`
                    });
                  }
                  return filtered;
                }}
                selectOnFocus
                clearOnBlur
                handleHomeEndKeys
                options={tags}
                getOptionLabel={(option) => {
                  return option.name ?? '';
                }}
                renderOption={(option) => {
                  if (option.title) return option.title;
                  return option.name ?? '';
                }}
                fullWidth
                freeSolo
                renderInput={(params) => (
                  <TextField {...params} variant="outlined" />
                )}
              />
            </>
          ) : (
            <TextField
              autoFocus
              margin="dense"
              id="name"
              label={dialog.label && dialog.label.slice(0, -1)}
              type="text"
              fullWidth
              onChange={(e) => setInputValue(e.target.value)}
            />
          )}
        </DialogContent>
        <DialogActions>
          <Button variant="outlined" onClick={() => setDialog({ open: false })}>
            Cancel
          </Button>
          <Button
            variant="contained"
            color="primary"
            onClick={() => {
              if (dialog.type && dialog.type !== 'tags') {
                if (inputValue) {
                  organization[dialog.type].push(inputValue);
                  setOrganization({ ...organization });
                }
              } else {
                if (tagValue) {
                  if (!organization.tags) organization.tags = [];
                  organization.tags.push(tagValue as any);
                  setOrganization({ ...organization });
                }
              }
              setDialog({ open: false });
              setInputValue('');
              setTagValue(null);
            }}
          >
            Add
          </Button>
        </DialogActions>
      </Dialog>
      <TextField
        value={organization.name}
        disabled
        variant="filled"
        InputProps={{
          className: classes.orgName
        }}
      ></TextField>
      <ListInput label="Root Domains" type="rootDomains"></ListInput>
      <ListInput label="IP Blocks" type="ipBlocks"></ListInput>
      <ListInput label="Tags" type="tags"></ListInput>
      <div className={classes.headerRow}>
        <label>Passive Mode</label>
        <span>
          <SwitchInput
            checked={organization.isPassive}
            onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
              setOrganization({
                ...organization,
                isPassive: event.target.checked
              });
            }}
            color="primary"
          />
        </span>
      </div>
      <div className={classes.buttons}>
        <Link to={`/organizations`}>
          <Button
            variant="outlined"
            style={{ marginRight: '10px', color: '#565C65' }}
          >
            Cancel
          </Button>
        </Link>
        <Button
          variant="contained"
          onClick={updateOrganization}
          style={{ background: '#565C65', color: 'white' }}
        >
          Save
        </Button>
      </div>
    </Paper>,
    <React.Fragment key={1}>
      <Table<Role> columns={userRoleColumns} data={userRoles} />
      <Dialog
        open={dialog.open}
        onClose={() => setDialog({ open: false })}
        aria-labelledby="form-dialog-title"
        maxWidth="xs"
        fullWidth
      >
        <DialogTitle id="form-dialog-title">Add Member</DialogTitle>
        <DialogContent>
          <p style={{ color: '#3D4551' }}>
            Organization members can view Organization-specific vulnerabilities,
            domains, and notes. Organization administrators can additionally
            manage members and update the organization.
          </p>
          <TextField
            margin="dense"
            id="firstName"
            name="firstName"
            label="First Name"
            type="text"
            fullWidth
            value={newUserValues.firstName}
            onChange={onInviteUserTextChange}
            variant="filled"
            InputProps={{
              className: classes.textField
            }}
          />
          <TextField
            margin="dense"
            id="lastName"
            name="lastName"
            label="Last Name"
            type="text"
            fullWidth
            value={newUserValues.lastName}
            onChange={onInviteUserTextChange}
            variant="filled"
            InputProps={{
              className: classes.textField
            }}
          />
          <TextField
            margin="dense"
            id="email"
            name="email"
            label="Email"
            type="text"
            fullWidth
            value={newUserValues.email}
            onChange={onInviteUserTextChange}
            variant="filled"
            InputProps={{
              className: classes.textField
            }}
          />
          <br></br>
          <br></br>
          <FormLabel component="legend">Role</FormLabel>
          <RadioGroup
            aria-label="role"
            name="role"
            value={newUserValues.role}
            onChange={onInviteUserTextChange}
          >
            <FormControlLabel
              value="standard"
              control={<Radio color="primary" />}
              label="Standard"
            />
            <FormControlLabel
              value="admin"
              control={<Radio color="primary" />}
              label="Administrator"
            />
          </RadioGroup>
        </DialogContent>
        <DialogActions>
          <Button variant="outlined" onClick={() => setDialog({ open: false })}>
            Cancel
          </Button>
          <Button
            variant="contained"
            color="primary"
            onClick={async () => {
              onInviteUserSubmit();
              setDialog({ open: false });
            }}
          >
            Add
          </Button>
        </DialogActions>
      </Dialog>
    </React.Fragment>,
    <React.Fragment key={2}>
      <OrganizationList parent={organization}></OrganizationList>
    </React.Fragment>,
    <React.Fragment key={3}>
      <Table<Scan> columns={scanColumns} data={scans} fetchData={fetchScans} />
      <h2>Organization Scan History</h2>
      <Table<ScanTask> columns={scanTaskColumns} data={scanTasks} />
    </React.Fragment>
  ];

  let navItems = [
    {
      title: 'Settings',
      path: `/organizations/${organizationId}`,
      exact: true
    },
    {
      title: 'Members',
      path: `/organizations/${organizationId}/members`
    }
  ];

  if (!organization.parent) {
    navItems = navItems.concat([
      // { title: 'Teams', path: `/organizations/${organizationId}/teams` },
      { title: 'Scans', path: `/organizations/${organizationId}/scans` }
    ]);
  }

  return (
    <div>
      <div className={classes.header}>
        <h1 className={classes.headerLabel}>
          <Link to="/organizations">Organizations</Link>
          {organization.parent && (
            <>
              <ChevronRight></ChevronRight>
              <Link to={'/organizations/' + organization.parent.id}>
                {organization.parent.name}
              </Link>
            </>
          )}
          <ChevronRight
            style={{
              verticalAlign: 'middle',
              lineHeight: '100%',
              fontSize: '26px'
            }}
          ></ChevronRight>
          <span style={{ color: '#07648D' }}>{organization.name}</span>
        </h1>
        <Subnav
          items={navItems}
          styles={{
            background: '#F9F9F9'
          }}
        ></Subnav>
      </div>
      <div className={classes.root}>
        <Switch>
          <Route
            path="/organizations/:organizationId"
            exact
            render={() => views[0]}
          />
          <Route
            path="/organizations/:organizationId/members"
            render={() => views[1]}
          />
          <Route
            path="/organizations/:organizationId/teams"
            render={() => views[2]}
          />
          <Route
            path="/organizations/:organizationId/scans"
            render={() => views[3]}
          />
        </Switch>
      </div>
    </div>
  );
}