@material-ui/core#AccordionSummary TypeScript Examples

The following examples show how to use @material-ui/core#AccordionSummary. 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: CollapsibleAlert.tsx    From abacus with GNU General Public License v2.0 6 votes vote down vote up
export default function CollapsibleAlert({
  id,
  severity,
  summary,
  className,
  children,
}: {
  id: string
  severity: Color
  summary?: React.ReactNode
  className?: string
  children?: React.ReactNode
}): JSX.Element {
  const classes = useStyles()
  const severityClassMap = new Map([
    ['success', 'MuiAlert-standardSuccess'],
    ['info', 'MuiAlert-standardInfo'],
    ['warning', 'MuiAlert-standardWarning'],
    ['error', 'MuiAlert-standardError'],
  ])

  return (
    <Alert severity={severity} className={className}>
      <Accordion className={clsx(severityClassMap.get(severity), classes.accordionRoot)}>
        <AccordionSummary
          classes={{ root: classes.accordionSummary }}
          expandIcon={<ExpandMoreIcon />}
          aria-controls={`${id}-content`}
          id={id}
        >
          {summary}
        </AccordionSummary>
        <AccordionDetails>
          <div>{children}</div>
        </AccordionDetails>
      </Accordion>
    </Alert>
  )
}
Example #2
Source File: ErrorMessage.tsx    From TidGi-Desktop with Mozilla Public License 2.0 6 votes vote down vote up
export function WikiErrorMessages(props: IWikiErrorMessagesProps): JSX.Element {
  const { t } = useTranslation();
  const wikiLogs = usePromiseValue(async () => await window.service.wiki.getWikiLogs(props.activeWorkspace.wikiFolderLocation));
  if (wikiLogs !== undefined) {
    return (
      <WikiErrorMessagesContainer>
        <Accordion>
          <AccordionSummary>
            <Typography align="left" variant="h5">
              {t('Error.WikiRuntimeError')} {t('ClickForDetails')}
            </Typography>
          </AccordionSummary>
          <AccordionDetails>
            <Typography align="left" variant="body2">
              {t('Error.WikiRuntimeErrorDescription')}
            </Typography>
            <Button variant="outlined" onClick={async () => await window.service.native.open(wikiLogs.filePath, true)}>
              {t('Preference.OpenLogFolder')}
            </Button>

            <div>
              <pre>
                <code>{wikiLogs.content}</code>
              </pre>
            </div>
          </AccordionDetails>
        </Accordion>
      </WikiErrorMessagesContainer>
    );
  }
  return <div />;
}
Example #3
Source File: ServerListItemGroup.tsx    From shadowsocks-electron with GNU General Public License v3.0 6 votes vote down vote up
StyledAccordionSummary = withStyles((theme: Theme) => (
  createStyles({
    root: {
      minHeight: '36px',
      backgroundColor: theme.palette.type === "dark" ? '#525252' : 'rgba(255, 255, 255, 1)',
      '&.Mui-expanded': {
        minHeight: '36px',
      },
      '& .MuiAccordionSummary-content': {
        margin: '8px 0'
      },
      '& .MuiIconButton-root': {
        padding: '8px 12px',
      },
    }
  })
))(AccordionSummary)
Example #4
Source File: MyAccordion.tsx    From clearflask with Apache License 2.0 6 votes vote down vote up
export default function MyAccordion(props: {
  name?: React.ReactNode;
} & React.ComponentProps<typeof Accordion>) {
  const classes = useStyles();
  const { children, name, ...AccordionProps } = props;
  return (
    <Accordion
      TransitionProps={{
        appear: true,
      }}
      classes={{
        root: classes.accordion,
      }}
      elevation={0}
      {...AccordionProps}
    >
      <AccordionSummary expandIcon={<ExpandMoreIcon />}>
        <Typography>
          {name}
        </Typography>
      </AccordionSummary>
      <AccordionDetails>
        <Typography className={classes.children}>
          {children}
        </Typography>
      </AccordionDetails>
    </Accordion>
  );
}
Example #5
Source File: with-subheader.tsx    From react-component-library with BSD 3-Clause "New" or "Revised" License 6 votes vote down vote up
accordion = (
    <Accordion>
        <AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls="panel1a-content" id="panel1a-header">
            <Typography>Expansion Panel 1</Typography>
        </AccordionSummary>
        <AccordionDetails>
            <Typography>
                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet
                blandit leo lobortis eget.
            </Typography>
        </AccordionDetails>
    </Accordion>
)
Example #6
Source File: StatefulSetsAccordions.tsx    From backstage with Apache License 2.0 6 votes vote down vote up
StatefulSetAccordion = ({
  statefulset,
  ownedPods,
  matchingHpa,
}: StatefulSetAccordionProps) => {
  const podNamesWithErrors = useContext(PodNamesWithErrorsContext);

  const podsWithErrors = ownedPods.filter(p =>
    podNamesWithErrors.has(p.metadata?.name ?? ''),
  );

  return (
    <Accordion TransitionProps={{ unmountOnExit: true }}>
      <AccordionSummary expandIcon={<ExpandMoreIcon />}>
        <StatefulSetSummary
          statefulset={statefulset}
          numberOfCurrentPods={ownedPods.length}
          numberOfPodsWithErrors={podsWithErrors.length}
          hpa={matchingHpa}
        />
      </AccordionSummary>
      <AccordionDetails>
        <PodsTable
          pods={ownedPods}
          extraColumns={[READY_COLUMNS, RESOURCE_COLUMNS]}
        />
      </AccordionDetails>
    </Accordion>
  );
}
Example #7
Source File: ServicesAccordions.tsx    From backstage with Apache License 2.0 6 votes vote down vote up
ServiceAccordion = ({ service }: ServiceAccordionProps) => {
  return (
    <Accordion TransitionProps={{ unmountOnExit: true }}>
      <AccordionSummary expandIcon={<ExpandMoreIcon />}>
        <ServiceSummary service={service} />
      </AccordionSummary>
      <AccordionDetails>
        <ServiceCard service={service} />
      </AccordionDetails>
    </Accordion>
  );
}
Example #8
Source File: JobsAccordions.tsx    From backstage with Apache License 2.0 6 votes vote down vote up
JobAccordion = ({ job, ownedPods }: JobAccordionProps) => {
  return (
    <Accordion TransitionProps={{ unmountOnExit: true }}>
      <AccordionSummary expandIcon={<ExpandMoreIcon />}>
        <JobSummary job={job} />
      </AccordionSummary>
      <AccordionDetails>
        <PodsTable pods={ownedPods} />
      </AccordionDetails>
    </Accordion>
  );
}
Example #9
Source File: IngressesAccordions.tsx    From backstage with Apache License 2.0 6 votes vote down vote up
IngressAccordion = ({ ingress }: IngressAccordionProps) => {
  return (
    <Accordion TransitionProps={{ unmountOnExit: true }}>
      <AccordionSummary expandIcon={<ExpandMoreIcon />}>
        <IngressSummary ingress={ingress} />
      </AccordionSummary>
      <AccordionDetails>
        <IngressCard ingress={ingress} />
      </AccordionDetails>
    </Accordion>
  );
}
Example #10
Source File: DeploymentsAccordions.tsx    From backstage with Apache License 2.0 6 votes vote down vote up
DeploymentAccordion = ({
  deployment,
  ownedPods,
  matchingHpa,
}: DeploymentAccordionProps) => {
  const podNamesWithErrors = useContext(PodNamesWithErrorsContext);

  const podsWithErrors = ownedPods.filter(p =>
    podNamesWithErrors.has(p.metadata?.name ?? ''),
  );

  return (
    <Accordion TransitionProps={{ unmountOnExit: true }}>
      <AccordionSummary expandIcon={<ExpandMoreIcon />}>
        <DeploymentSummary
          deployment={deployment}
          numberOfCurrentPods={ownedPods.length}
          numberOfPodsWithErrors={podsWithErrors.length}
          hpa={matchingHpa}
        />
      </AccordionSummary>
      <AccordionDetails>
        <PodsTable
          pods={ownedPods}
          extraColumns={[READY_COLUMNS, RESOURCE_COLUMNS]}
        />
      </AccordionDetails>
    </Accordion>
  );
}
Example #11
Source File: DefaultCustomResource.tsx    From backstage with Apache License 2.0 6 votes vote down vote up
DefaultCustomResourceAccordion = ({
  customResource,
  customResourceName,
  defaultExpanded,
}: DefaultCustomResourceAccordionProps) => {
  return (
    <Accordion
      defaultExpanded={defaultExpanded}
      TransitionProps={{ unmountOnExit: true }}
    >
      <AccordionSummary expandIcon={<ExpandMoreIcon />}>
        <DefaultCustomResourceSummary
          customResource={customResource}
          customResourceName={customResourceName}
        />
      </AccordionSummary>
      <AccordionDetails>
        {customResource.hasOwnProperty('status') && (
          <StructuredMetadataTable metadata={customResource.status} />
        )}
      </AccordionDetails>
    </Accordion>
  );
}
Example #12
Source File: CronJobsAccordions.tsx    From backstage with Apache License 2.0 6 votes vote down vote up
CronJobAccordion = ({ cronJob, ownedJobs }: CronJobAccordionProps) => {
  return (
    <Accordion TransitionProps={{ unmountOnExit: true }}>
      <AccordionSummary expandIcon={<ExpandMoreIcon />}>
        <CronJobSummary cronJob={cronJob} />
      </AccordionSummary>
      <AccordionDetails>
        <JobsAccordions jobs={ownedJobs.reverse()} />
      </AccordionDetails>
    </Accordion>
  );
}
Example #13
Source File: ActionOutput.tsx    From backstage with Apache License 2.0 6 votes vote down vote up
ActionOutput = ({ url, name, className }: ActionOutputProps) => {
  const classes = useStyles();

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

  return (
    <Accordion TransitionProps={{ unmountOnExit: true }} className={className}>
      <AccordionSummary
        expandIcon={<ExpandMoreIcon />}
        aria-controls={`panel-${name}-content`}
        id={`panel-${name}-header`}
        IconButtonProps={{
          className: classes.button,
        }}
      >
        <Typography variant="button">{name}</Typography>
      </AccordionSummary>
      <AccordionDetails className={classes.accordionDetails}>
        Nothing here...
      </AccordionDetails>
    </Accordion>
  );
}
Example #14
Source File: InfoPanel.tsx    From backstage-plugin-opsgenie with MIT License 6 votes vote down vote up
InfoPanel = (props: Props) => {
  const classes = useStyles(props);
  const { title, message, children } = props;

  return (
    <Accordion className={classes.panel}>
      <AccordionSummary
        expandIcon={<ExpandMoreIconStyled />}
        className={classes.summary}
      >
        <ErrorOutlineStyled />
        <Typography className={classes.summaryText} variant="subtitle1">
          {title}
        </Typography>
      </AccordionSummary>
      {(message || children) && (
        <AccordionDetails>
          <Grid container>
            {message && (
              <Grid item xs={12}>
                <Typography className={classes.message} variant="body1">
                  {message}
                </Typography>
              </Grid>
            )}
            {children && (
              <Grid item xs={12} className={classes.details}>
                {children}
              </Grid>
            )}
          </Grid>
        </AccordionDetails>
      )}
    </Accordion>
  );
}
Example #15
Source File: Rollout.tsx    From backstage with Apache License 2.0 5 votes vote down vote up
RolloutAccordion = ({
  rollout,
  ownedPods,
  matchingHpa,
  defaultExpanded,
}: RolloutAccordionProps) => {
  const podNamesWithErrors = useContext(PodNamesWithErrorsContext);

  const podsWithErrors = ownedPods.filter(p =>
    podNamesWithErrors.has(p.metadata?.name ?? ''),
  );

  const currentStepIndex = rollout.status?.currentStepIndex ?? 0;
  const abortedMessage = findAbortedMessage(rollout);

  return (
    <Accordion
      defaultExpanded={defaultExpanded}
      TransitionProps={{ unmountOnExit: true }}
    >
      <AccordionSummary expandIcon={<ExpandMoreIcon />}>
        <RolloutSummary
          rollout={rollout}
          numberOfCurrentPods={ownedPods.length}
          numberOfPodsWithErrors={podsWithErrors.length}
          hpa={matchingHpa}
        />
      </AccordionSummary>
      <AccordionDetails>
        <div style={{ width: '100%' }}>
          <div>
            <Typography variant="h6">Rollout status</Typography>
          </div>
          <div style={{ margin: '1rem' }}>
            {abortedMessage && (
              <>
                {AbortedTitle}
                <Typography variant="subtitle2">{abortedMessage}</Typography>
              </>
            )}
            <StepsProgress
              aborted={abortedMessage !== undefined}
              steps={rollout.spec?.strategy?.canary?.steps ?? []}
              currentStepIndex={currentStepIndex}
            />
          </div>
          <div>
            <PodsTable
              pods={ownedPods}
              extraColumns={[READY_COLUMNS, RESOURCE_COLUMNS]}
            />
          </div>
        </div>
      </AccordionDetails>
    </Accordion>
  );
}
Example #16
Source File: ComponentAccordion.tsx    From backstage with Apache License 2.0 5 votes vote down vote up
ComponentAccordion = (props: {
  title: string;
  expanded?: boolean;
  Content: () => JSX.Element;
  Actions?: () => JSX.Element;
  Settings?: () => JSX.Element;
  ContextProvider?: (props: any) => JSX.Element;
}) => {
  const {
    title,
    expanded = false,
    Content,
    Actions,
    Settings,
    ContextProvider,
    ...childProps
  } = props;

  const classes = useStyles();
  const [settingsIsExpanded, setSettingsIsExpanded] = React.useState(false);
  const [isExpanded, setIsExpanded] = React.useState(expanded);

  const handleOpenSettings = (e: any) => {
    e.stopPropagation();
    setSettingsIsExpanded(prevState => !prevState);
  };

  const innerContent = (
    <>
      {Settings && (
        <SettingsModal
          open={settingsIsExpanded}
          close={() => setSettingsIsExpanded(false)}
          componentName={title}
        >
          <Settings />
        </SettingsModal>
      )}
      <Accordion
        expanded={isExpanded}
        onChange={(_e: any, expandedValue: boolean) =>
          setIsExpanded(expandedValue)
        }
      >
        <AccordionSummary expandIcon={<ExpandMoreIcon />}>
          {Settings && (
            <IconButton
              onClick={handleOpenSettings}
              className={classes.settingsIconButton}
            >
              <SettingsIcon />
            </IconButton>
          )}
          <Typography>{title}</Typography>
        </AccordionSummary>
        <AccordionDetails>
          <div className={classes.contentContainer}>
            <Content />
            {Actions && <Actions />}
          </div>
        </AccordionDetails>
      </Accordion>
    </>
  );

  return ContextProvider ? (
    <ContextProvider {...childProps}>{innerContent}</ContextProvider>
  ) : (
    innerContent
  );
}
Example #17
Source File: Cluster.tsx    From backstage with Apache License 2.0 5 votes vote down vote up
Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => {
  const groupedResponses = groupResponses(clusterObjects.resources);
  const podNameToMetrics = clusterObjects.podMetrics
    .flat()
    .reduce((accum, next) => {
      const name = next.pod.metadata?.name;
      if (name !== undefined) {
        accum.set(name, next);
      }
      return accum;
    }, new Map<string, ClientPodStatus>());
  return (
    <ClusterContext.Provider value={clusterObjects.cluster}>
      <GroupedResponsesContext.Provider value={groupedResponses}>
        <PodNamesWithMetricsContext.Provider value={podNameToMetrics}>
          <PodNamesWithErrorsContext.Provider value={podsWithErrors}>
            <Accordion TransitionProps={{ unmountOnExit: true }}>
              <AccordionSummary expandIcon={<ExpandMoreIcon />}>
                <ClusterSummary
                  clusterName={clusterObjects.cluster.name}
                  totalNumberOfPods={groupedResponses.pods.length}
                  numberOfPodsWithErrors={podsWithErrors.size}
                />
              </AccordionSummary>
              <AccordionDetails>
                <Grid container direction="column">
                  <Grid item>
                    <CustomResources />
                  </Grid>
                  <Grid item>
                    <DeploymentsAccordions />
                  </Grid>
                  <Grid item>
                    <StatefulSetsAccordions />
                  </Grid>
                  <Grid item>
                    <IngressesAccordions />
                  </Grid>
                  <Grid item>
                    <ServicesAccordions />
                  </Grid>
                  <Grid item>
                    <CronJobsAccordions />
                  </Grid>
                </Grid>
              </AccordionDetails>
            </Accordion>
          </PodNamesWithErrorsContext.Provider>
        </PodNamesWithMetricsContext.Provider>
      </GroupedResponsesContext.Provider>
    </ClusterContext.Provider>
  );
}
Example #18
Source File: WorkflowRunDetails.tsx    From backstage with Apache License 2.0 5 votes vote down vote up
JobListItem = ({
  job,
  className,
  entity,
}: {
  job: Job;
  className: string;
  entity: Entity;
}) => {
  const classes = useStyles();
  return (
    <Accordion TransitionProps={{ unmountOnExit: true }} className={className}>
      <AccordionSummary
        expandIcon={<ExpandMoreIcon />}
        aria-controls={`panel-${name}-content`}
        id={`panel-${name}-header`}
        IconButtonProps={{
          className: classes.button,
        }}
      >
        <Typography variant="button">
          {job.name} ({getElapsedTime(job.started_at, job.completed_at)})
        </Typography>
      </AccordionSummary>
      <AccordionDetails className={classes.accordionDetails}>
        <TableContainer>
          <Table>
            {job.steps.map(step => (
              <StepView key={step.number} step={step} />
            ))}
          </Table>
        </TableContainer>
      </AccordionDetails>
      {job.status === 'queued' || job.status === 'in_progress' ? (
        <WorkflowRunLogs runId={job.id} inProgress entity={entity} />
      ) : (
        <WorkflowRunLogs runId={job.id} inProgress={false} entity={entity} />
      )}
    </Accordion>
  );
}
Example #19
Source File: ActionOutput.tsx    From backstage with Apache License 2.0 5 votes vote down vote up
ActionOutput = ({
  url,
  name,
  className,
  action,
}: {
  url: string;
  name: string;
  className?: string;
  action: BuildStepAction;
}) => {
  const classes = useStyles();

  const [messages, setMessages] = useState([]);
  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(actionOutput => {
        if (typeof actionOutput !== 'undefined') {
          setMessages(
            actionOutput.map(({ message }: { message: string }) => message),
          );
        }
      });
  }, [url]);

  const timeElapsed = durationHumanized(action.start_time, action.end_time);

  return (
    <Accordion TransitionProps={{ unmountOnExit: true }} className={className}>
      <AccordionSummary
        expandIcon={<ExpandMoreIcon />}
        aria-controls={`panel-${name}-content`}
        id={`panel-${name}-header`}
        IconButtonProps={{
          className: classes.button,
        }}
      >
        <Typography variant="button">
          {name} ({timeElapsed})
        </Typography>
      </AccordionSummary>
      <AccordionDetails className={classes.accordionDetails}>
        {messages.length === 0 ? (
          'Nothing here...'
        ) : (
          <div style={{ height: '20vh', width: '100%' }}>
            <LogViewer text={messages.join('\n')} />
          </div>
        )}
      </AccordionDetails>
    </Accordion>
  );
}
Example #20
Source File: index.tsx    From TidGi-Desktop with Mozilla Public License 2.0 5 votes vote down vote up
AdvancedSettingsAccordionSummary = styled(AccordionSummary)`
  margin-top: 10px;
`
Example #21
Source File: TransactionContainer.tsx    From parity-bridges-ui with GNU General Public License v3.0 5 votes vote down vote up
TransactionContainer = ({ transaction, expanded, selected = false }: Props) => {
  const classes = useStyles();
  const {
    payloadHex,
    transactionDisplayPayload,
    sourceChain,
    targetChain,
    sourceAccount,
    companionAccount,
    senderName,
    transferAmount,
    type,
    status,
    steps,
    receiverAddress
  } = transaction;
  const [accordionExpanded, setAccordionExpanded] = useState(expanded);

  const onChange = useCallback(() => setAccordionExpanded(!accordionExpanded), [accordionExpanded]);

  useEffect(() => {
    if (status === TransactionStatusEnum.COMPLETED) {
      setAccordionExpanded(false);
    }
  }, [status]);

  return (
    <Accordion
      expanded={accordionExpanded}
      onChange={onChange}
      className={cx(classes.accordion, selected ? classes.selectedBorder : '')}
    >
      <AccordionSummary expandIcon={<ExpandMoreIcon />}>
        <div className={classes.header}>
          <TransactionHeader
            type={type}
            status={status}
            sourceChain={sourceChain}
            targetChain={targetChain}
            transferAmount={transferAmount}
          />
        </div>
      </AccordionSummary>
      <AccordionDetails>
        <Box component="div" width="100%">
          <TransactionAccounts
            senderName={senderName}
            sourceAccount={sourceAccount ? sourceAccount : undefined}
            senderCompanionAccount={companionAccount ? companionAccount : undefined}
            receiverAddress={receiverAddress ? receiverAddress : undefined}
            type={type}
          />

          <TransactionSwitchTab payloadHex={payloadHex} transactionDisplayPayload={transactionDisplayPayload}>
            <TransactionReceipt steps={steps} />
          </TransactionSwitchTab>
        </Box>
      </AccordionDetails>
    </Accordion>
  );
}
Example #22
Source File: index.tsx    From prism-frontend with MIT License 5 votes vote down vote up
function MenuItemMobile({
  expanded,
  selectAccordion,
  classes,
  title,
  icon,
  layersCategories,
}: MenuItemMobileProps) {
  const { t } = useSafeTranslation();
  const handleChange = (panel: string) => (
    event: React.ChangeEvent<{}>,
    newExpanded: boolean,
  ) => {
    selectAccordion(newExpanded ? panel : '');
  };

  return (
    <Accordion
      key={title}
      square
      elevation={0}
      expanded={expanded === title}
      onChange={handleChange(title)}
    >
      <AccordionSummary
        expandIcon={<FontAwesomeIcon icon={faCaretDown} />}
        IconButtonProps={{ color: 'inherit', size: 'small' }}
        aria-controls={title}
        id={title}
      >
        <img className={classes.icon} src={`/images/${icon}`} alt={title} />
        <Typography variant="body2">{t(title)}</Typography>
      </AccordionSummary>
      <AccordionDetails>
        <Grid container direction="column">
          {layersCategories.map(({ title: categoryTitle, layers, tables }) => (
            <MenuSwitch
              key={categoryTitle}
              title={categoryTitle}
              layers={layers}
              tables={tables}
            />
          ))}
        </Grid>
      </AccordionDetails>
    </Accordion>
  );
}
Example #23
Source File: FAQItem.tsx    From homebase-app with MIT License 5 votes vote down vote up
AccordionHeader = styled(AccordionSummary)({
  minHeight: 40,
  padding: "20px 40px",
  background: "rgb(47, 52, 56)",
})
Example #24
Source File: Summary.tsx    From UsTaxes with GNU Affero General Public License v3.0 4 votes vote down vote up
F1040Summary = ({ summary }: F1040SummaryProps): ReactElement => (
  <>
    {(() => {
      if (summary.amountOwed !== undefined && summary.amountOwed > 0) {
        return (
          <div>
            <Typography variant="body2" color="textSecondary">
              Amount Owed: <Currency value={-summary.amountOwed} />
            </Typography>
          </div>
        )
      }
      if (summary.refundAmount !== undefined && summary.refundAmount > 0) {
        return (
          <div>
            <Typography variant="body2" color="textSecondary">
              Refund Amount: <Currency value={summary.refundAmount} />
            </Typography>
          </div>
        )
      }
    })()}

    <h3>Credits</h3>
    <Grid container>
      <Grid item zeroMinWidth>
        <List>
          {summary.credits.map((credit) => (
            <BinaryStateListItem key={credit.name} active={credit.allowed}>
              <Typography variant="body2" color="textPrimary">
                {credit.name}
              </Typography>
              {(() => {
                if (credit.value !== undefined) {
                  return (
                    <Typography variant="body2" color="textSecondary">
                      Credit: <Currency value={credit.value} />
                    </Typography>
                  )
                }
                return <></>
              })()}
            </BinaryStateListItem>
          ))}
        </List>
      </Grid>
    </Grid>

    {(() => {
      if (summary.worksheets.length > 0) {
        return (
          <Grid container>
            <Grid item zeroMinWidth>
              <h3>Worksheets</h3>
              <List>
                {summary.worksheets.map((worksheet, idx) => (
                  <Accordion key={idx}>
                    <AccordionSummary expandIcon={<ExpandMore />}>
                      {worksheet.name}
                    </AccordionSummary>
                    <AccordionDetails>
                      <TableContainer>
                        <Table size="small">
                          <TableBody>
                            {worksheet.lines.map((line) => (
                              <TableRow key={line.line}>
                                <TableCell component="th">
                                  Line {line.line}
                                </TableCell>
                                <TableCell>
                                  {typeof line.value === 'number' ? (
                                    <Currency
                                      value={displayRound(line.value) ?? 0}
                                    />
                                  ) : (
                                    line.value
                                  )}
                                </TableCell>
                              </TableRow>
                            ))}
                          </TableBody>
                        </Table>
                      </TableContainer>
                    </AccordionDetails>
                  </Accordion>
                ))}
              </List>
            </Grid>
          </Grid>
        )
      }
      return <></>
    })()}
  </>
)
Example #25
Source File: ExperimentResults.tsx    From abacus with GNU General Public License v2.0 4 votes vote down vote up
/**
 * Render the latest analyses for the experiment for each metric assignment as a single condensed table, using only
 * the experiment's default analysis strategy.
 */
export default function ExperimentResults({
  analyses,
  experiment,
  metrics,
}: {
  analyses: Analysis[]
  experiment: ExperimentFull
  metrics: Metric[]
  debugMode?: boolean
}): JSX.Element {
  const classes = useStyles()
  const theme = useTheme()

  const availableAnalysisStrategies = [
    AnalysisStrategy.IttPure,
    AnalysisStrategy.MittNoCrossovers,
    AnalysisStrategy.MittNoSpammers,
    AnalysisStrategy.MittNoSpammersNoCrossovers,
  ]
  if (experiment.exposureEvents) {
    availableAnalysisStrategies.push(AnalysisStrategy.PpNaive)
  }
  const [strategy, setStrategy] = useState<AnalysisStrategy>(() => Experiments.getDefaultAnalysisStrategy(experiment))
  const onStrategyChange = (event: React.ChangeEvent<{ value: unknown }>) => {
    setStrategy(event.target.value as AnalysisStrategy)
  }

  const baseVariationId = experiment.variations.find((v) => v.isDefault)?.variationId
  const changeVariationId = experiment.variations.find((v) => !v.isDefault)?.variationId
  // istanbul ignore next; Shouldn't occur.
  if (!baseVariationId || !changeVariationId) {
    throw new Error('Missing base or change variations.')
  }
  const variationDiffKey = `${changeVariationId}_${baseVariationId}`

  const indexedMetrics = indexMetrics(metrics)
  const analysesByMetricAssignmentId = _.groupBy(analyses, 'metricAssignmentId')
  const allMetricAssignmentAnalysesData: MetricAssignmentAnalysesData[] = MetricAssignments.sort(
    experiment.metricAssignments,
  ).map((metricAssignment) => {
    const metricAssignmentAnalyses = analysesByMetricAssignmentId[metricAssignment.metricAssignmentId] || []
    return {
      metricAssignment,
      metric: indexedMetrics[metricAssignment.metricId],
      analysesByStrategyDateAsc: _.groupBy(
        _.orderBy(metricAssignmentAnalyses, ['analysisDatetime'], ['asc']),
        'analysisStrategy',
      ) as Record<AnalysisStrategy, Analysis[]>,
    }
  })

  const metricAssignmentSummaryData = allMetricAssignmentAnalysesData.map(
    ({ metricAssignment, metric, analysesByStrategyDateAsc }) => ({
      experiment,
      strategy,
      metricAssignment,
      metric,
      analysesByStrategyDateAsc,
      recommendation: Recommendations.getAggregateMetricAssignmentRecommendation(
        Object.values(analysesByStrategyDateAsc)
          .map(_.last.bind(null))
          .filter((x) => x)
          .map((analysis) =>
            Recommendations.getMetricAssignmentRecommendation(
              experiment,
              metric,
              analysis as Analysis,
              variationDiffKey,
            ),
          ),
        strategy,
      ),
    }),
  )

  // ### Result Summary Visualizations

  const primaryMetricAssignmentAnalysesData = allMetricAssignmentAnalysesData.find(
    ({ metricAssignment: { isPrimary } }) => isPrimary,
  ) as MetricAssignmentAnalysesData
  const primaryAnalyses = primaryMetricAssignmentAnalysesData.analysesByStrategyDateAsc[strategy] || []
  const dates = primaryAnalyses.map(({ analysisDatetime }) => analysisDatetime.toISOString())

  const plotlyDataParticipantGraph: Array<Partial<PlotData>> = [
    ..._.flatMap(experiment.variations, (variation, index) => {
      const variationKey = `variation_${variation.variationId}`
      return [
        {
          name: `${variation.name}`,
          x: dates,
          y: primaryAnalyses.map(({ participantStats: { [variationKey]: variationCount } }) => variationCount),
          line: {
            color: Visualizations.variantColors[index],
          },
          mode: 'lines+markers' as const,
          type: 'scatter' as const,
        },
      ]
    }),
  ]

  // ### Top Level Stats

  const primaryMetricLatestAnalysesByStrategy = _.mapValues(
    primaryMetricAssignmentAnalysesData.analysesByStrategyDateAsc,
    _.last.bind(null),
  )
  const latestPrimaryMetricAnalysis = primaryMetricLatestAnalysesByStrategy[strategy]
  // istanbul ignore next; trivial
  const totalParticipants = latestPrimaryMetricAnalysis?.participantStats['total'] ?? 0
  const primaryMetricRecommendation = Recommendations.getAggregateMetricAssignmentRecommendation(
    Object.values(primaryMetricLatestAnalysesByStrategy)
      .filter((x) => x)
      .map((analysis) =>
        Recommendations.getMetricAssignmentRecommendation(
          experiment,
          primaryMetricAssignmentAnalysesData.metric,
          analysis as Analysis,
          variationDiffKey,
        ),
      ),
    strategy,
  )
  // We check if there are any analyses at all to show as we want to show what we can to the Experimenter:
  const hasAnalyses = allMetricAssignmentAnalysesData.some(
    (x) => Object.values(x.analysesByStrategyDateAsc).filter((y) => y).length > 0,
  )

  const experimentParticipantStats = Analyses.getExperimentParticipantStats(
    experiment,
    primaryMetricLatestAnalysesByStrategy,
  )
  const experimentHealthIndicators = [
    ...Analyses.getExperimentParticipantHealthIndicators(experimentParticipantStats),
    ...Analyses.getExperimentHealthIndicators(experiment),
  ]

  const maxIndicationSeverity = experimentHealthIndicators
    .map(({ indication: { severity } }) => severity)
    .sort(
      (severityA, severityB) =>
        Analyses.healthIndicationSeverityOrder.indexOf(severityB) -
        Analyses.healthIndicationSeverityOrder.indexOf(severityA),
    )[0]

  const maxIndicationSeverityMessage = {
    [Analyses.HealthIndicationSeverity.Ok]: 'No issues detected',
    [Analyses.HealthIndicationSeverity.Warning]: 'Potential issues',
    [Analyses.HealthIndicationSeverity.Error]: 'Serious issues',
  }

  // ### Metric Assignments Table

  const tableColumns = [
    {
      title: 'Metric (attribution window)',
      render: ({ metric, metricAssignment }: { metric: Metric; metricAssignment: MetricAssignment }) => (
        <>
          <span className={classes.metricAssignmentNameLine}>
            <Tooltip title={metric.description}>
              <span>{metric.name}</span>
            </Tooltip>
            &nbsp;({AttributionWindowSecondsToHuman[metricAssignment.attributionWindowSeconds]})
          </span>
          {metricAssignment.isPrimary && (
            <>
              <br />
              <Attribute name='primary' />
            </>
          )}
        </>
      ),
      cellStyle: {
        fontFamily: theme.custom.fonts.monospace,
        fontWeight: 600,
        minWidth: 450,
      },
    },
    {
      title: 'Absolute change',
      render: ({
        metric,
        strategy,
        analysesByStrategyDateAsc,
        recommendation,
      }: {
        metric: Metric
        strategy: AnalysisStrategy
        analysesByStrategyDateAsc: Record<AnalysisStrategy, Analysis[]>
        recommendation: Recommendations.Recommendation
      }) => {
        const latestEstimates = _.last(analysesByStrategyDateAsc[strategy])?.metricEstimates
        if (
          !latestEstimates ||
          recommendation.decision === Recommendations.Decision.ManualAnalysisRequired ||
          recommendation.decision === Recommendations.Decision.MissingAnalysis
        ) {
          return null
        }

        return (
          <MetricValueInterval
            intervalName={'the absolute change between variations'}
            isDifference={true}
            metricParameterType={metric.parameterType}
            bottomValue={latestEstimates.diffs[variationDiffKey].bottom_95}
            topValue={latestEstimates.diffs[variationDiffKey].top_95}
            displayTooltipHint={false}
          />
        )
      },
      cellStyle: {
        fontFamily: theme.custom.fonts.monospace,
      },
    },
    {
      title: 'Relative change (lift)',
      render: ({
        strategy,
        analysesByStrategyDateAsc,
        recommendation,
      }: {
        metric: Metric
        strategy: AnalysisStrategy
        analysesByStrategyDateAsc: Record<AnalysisStrategy, Analysis[]>
        recommendation: Recommendations.Recommendation
      }) => {
        const latestEstimates = _.last(analysesByStrategyDateAsc[strategy])?.metricEstimates
        if (
          !latestEstimates?.ratios[variationDiffKey]?.top_95 ||
          recommendation.decision === Recommendations.Decision.ManualAnalysisRequired ||
          recommendation.decision === Recommendations.Decision.MissingAnalysis
        ) {
          return null
        }

        return (
          <MetricValueInterval
            intervalName={'the relative change between variations'}
            metricParameterType={MetricParameterType.Conversion}
            bottomValue={Analyses.ratioToDifferenceRatio(latestEstimates.ratios[variationDiffKey].bottom_95)}
            topValue={Analyses.ratioToDifferenceRatio(latestEstimates.ratios[variationDiffKey].top_95)}
            displayTooltipHint={false}
          />
        )
      },
      cellStyle: {
        fontFamily: theme.custom.fonts.monospace,
      },
    },
    {
      title: 'Analysis',
      render: ({
        experiment,
        recommendation,
      }: {
        experiment: ExperimentFull
        recommendation: Recommendations.Recommendation
      }) => {
        return <AnalysisDisplay {...{ experiment, analysis: recommendation }} />
      },
      cellStyle: {
        fontFamily: theme.custom.fonts.monospace,
      },
    },
  ]

  const DetailPanel = [
    ({
      strategy,
      analysesByStrategyDateAsc,
      metricAssignment,
      metric,
      recommendation,
    }: {
      strategy: AnalysisStrategy
      analysesByStrategyDateAsc: Record<AnalysisStrategy, Analysis[]>
      metricAssignment: MetricAssignment
      metric: Metric
      recommendation: Recommendations.Recommendation
    }) => {
      let disabled = recommendation.decision === Recommendations.Decision.ManualAnalysisRequired
      // istanbul ignore next; debug only
      disabled = disabled && !isDebugMode()
      return {
        render: () => (
          <MetricAssignmentResults
            {...{
              strategy,
              analysesByStrategyDateAsc,
              metricAssignment,
              metric,
              experiment,
              recommendation,
              variationDiffKey,
            }}
          />
        ),
        disabled,
      }
    },
  ]

  return (
    <div className='analysis-latest-results'>
      <div className={classes.root}>
        {hasAnalyses ? (
          <>
            {experiment.variations.length > 2 && (
              <>
                <Alert severity='error'>
                  <strong>A/B/n analysis is an ALPHA quality feature.</strong>
                  <br />
                  <br />
                  <strong>What&apos;s not working:</strong>
                  <ul>
                    <li> Metric Assignment Table (Absolute/Relative Change, Analysis.) </li>
                    <li> Summary text. </li>
                    <li> Recommendations. </li>
                    <li> Difference charts. </li>
                    <li> The health report. </li>
                  </ul>
                  <strong>What&apos;s working:</strong>
                  <ul>
                    <li> Participation stats and charts. </li>
                    <li> Analysis tables (when you open a metric assignment.)</li>
                    <li> Variation value charts. </li>
                    <li> Observed data. </li>
                  </ul>
                </Alert>
                <br />
              </>
            )}
            <div className={classes.summary}>
              <Paper className={classes.participantsPlotPaper}>
                <Typography variant='h3' gutterBottom>
                  Participants by Variation
                </Typography>
                <Plot
                  layout={{
                    ...Visualizations.plotlyLayoutDefault,
                    margin: {
                      l: theme.spacing(4),
                      r: theme.spacing(2),
                      t: 0,
                      b: theme.spacing(6),
                    },
                  }}
                  data={plotlyDataParticipantGraph}
                  className={classes.participantsPlot}
                />
              </Paper>
              <div className={classes.summaryColumn}>
                <Paper className={classes.summaryStatsPaper}>
                  {latestPrimaryMetricAnalysis && (
                    <>
                      <div className={classes.summaryStatsPart}>
                        <Typography variant='h3' className={classes.summaryStatsStat} color='primary'>
                          {totalParticipants.toLocaleString('en', { useGrouping: true })}
                        </Typography>
                        <Typography variant='subtitle1'>
                          <strong>analyzed participants</strong> as at{' '}
                          {formatIsoDate(latestPrimaryMetricAnalysis.analysisDatetime)}
                        </Typography>
                      </div>
                      <div className={classes.summaryStatsPart}>
                        <Typography variant='h3' className={classes.summaryStatsStat} color='primary'>
                          <DeploymentRecommendation {...{ experiment, analysis: primaryMetricRecommendation }} />
                        </Typography>
                        <Typography variant='subtitle1'>
                          <strong>primary metric</strong> recommendation
                        </Typography>
                      </div>
                    </>
                  )}
                </Paper>
                <Paper
                  className={clsx(
                    classes.summaryHealthPaper,
                    classes[indicationSeverityClassSymbol(maxIndicationSeverity)],
                  )}
                  component='a'
                  // @ts-ignore: Component extensions aren't appearing in types.
                  href='#health-report'
                >
                  <div className={classes.summaryStats}>
                    <Typography variant='h3' className={clsx(classes.summaryStatsStat)} color='primary'>
                      {maxIndicationSeverityMessage[maxIndicationSeverity]}
                    </Typography>
                    <Typography variant='subtitle1'>
                      see <strong>health report</strong>
                    </Typography>
                  </div>
                </Paper>
              </div>
            </div>
            <Typography variant='h3' className={classes.tableTitle}>
              Metric Assignment Results
            </Typography>
            <MaterialTable
              columns={tableColumns}
              data={metricAssignmentSummaryData}
              options={createStaticTableOptions(metricAssignmentSummaryData.length)}
              onRowClick={(_event, rowData, togglePanel) => {
                const { recommendation } = rowData as {
                  recommendation: Recommendations.Recommendation
                }
                let disabled = recommendation.decision === Recommendations.Decision.ManualAnalysisRequired
                // istanbul ignore next; debug only
                disabled = disabled && !isDebugMode()

                // istanbul ignore else; trivial
                if (togglePanel && !disabled) {
                  togglePanel()
                }
              }}
              detailPanel={DetailPanel}
            />
            <Typography variant='h3' className={classes.tableTitle}>
              Health Report
            </Typography>
            <Paper id='health-report'>
              <HealthIndicatorTable indicators={experimentHealthIndicators} />
            </Paper>
          </>
        ) : (
          <Paper className={classes.noAnalysesPaper}>
            <Typography variant='h3' gutterBottom>
              {' '}
              No Results{' '}
            </Typography>
            <Typography variant='body1'>No results are available at the moment, this can be due to:</Typography>
            <ul>
              <Typography component='li'>
                <strong> An experiment being new. </strong> ExPlat can take 24-48 hours for results to process and
                become available. Updates are usually released at 06:00 UTC daily.
              </Typography>
              <Typography component='li'>
                <strong> No assignments occuring. </strong> Check the &quot;Early Monitoring&quot; section below to
                ensure that assignments are occuring.
              </Typography>
            </ul>
          </Paper>
        )}

        <div className={classes.accordions}>
          {hasAnalyses && (
            <Accordion>
              <AccordionSummary expandIcon={<ExpandMoreIcon />}>
                <Typography variant='h5'>Advanced - Choose an Analysis Strategy</Typography>
              </AccordionSummary>
              <AccordionDetails className={classes.accordionDetails}>
                <Typography variant='body1'>
                  Choosing a different analysis strategy is useful for checking the effect of different modelling
                  decisions on the results:
                </Typography>

                <ul>
                  <Typography variant='body1' component='li'>
                    <strong>All participants:</strong> All the participants are analysed based on their initial
                    variation assignment. Pure intention-to-treat.
                  </Typography>
                  <Typography variant='body1' component='li'>
                    <strong>Without crossovers:</strong> Same as all participants, but excluding participants that were
                    assigned to multiple experiment variations before or on the analysis date (aka crossovers). Modified
                    intention-to-treat.
                  </Typography>
                  <Typography variant='body1' component='li'>
                    <strong>Without spammers:</strong> Same as all participants, but excluding participants that were
                    flagged as spammers on the analysis date. Modified intention-to-treat.
                  </Typography>
                  <Typography variant='body1' component='li'>
                    <strong>Without crossovers and spammers:</strong> Same as all participants, but excluding both
                    spammers and crossovers. Modified intention-to-treat.
                  </Typography>
                  <Typography variant='body1' component='li'>
                    <strong>Exposed without crossovers and spammers:</strong> Only participants that triggered one of
                    the experiment&apos;s exposure events, excluding both spammers and crossovers. This analysis
                    strategy is only available if the experiment has exposure events, while the other four strategies
                    are used for every experiment. Naive per-protocol.
                  </Typography>
                </ul>

                <FormControl>
                  <InputLabel htmlFor='strategy-selector' id='strategy-selector-label'>
                    Analysis Strategy:
                  </InputLabel>
                  <Select
                    id='strategy-selector'
                    labelId='strategy-selector-label'
                    value={strategy}
                    onChange={onStrategyChange}
                  >
                    {availableAnalysisStrategies.map((strat) => (
                      <MenuItem key={strat} value={strat}>
                        {Analyses.AnalysisStrategyToHuman[strat]}
                        {strat === Experiments.getDefaultAnalysisStrategy(experiment) && ' (recommended)'}
                      </MenuItem>
                    ))}
                  </Select>
                  <FormHelperText>Updates the page data.</FormHelperText>
                </FormControl>
              </AccordionDetails>
            </Accordion>
          )}
          <Accordion>
            <AccordionSummary expandIcon={<ExpandMoreIcon />}>
              <Typography variant='h5'>Early Monitoring - Live Assignment Event Flow</Typography>
            </AccordionSummary>
            <AccordionDetails className={classes.accordionDetails}>
              <Typography variant='body1'>
                For early monitoring, you can run this query in Hue to retrieve unfiltered assignment counts from the
                unprocessed tracks queue.
              </Typography>

              <Typography variant='body1'>
                This query should only be used to monitor event flow. The best way to use it is to run it multiple times
                and ensure that counts go up and are roughly distributed as expected. Counts may also go down as events
                are moved to prod_events every day.
              </Typography>
              <pre className={classes.pre}>
                <code>
                  {/* (Using a javasript string automatically replaces special characters with html entities.) */}
                  {`with tracks_counts as (
  select
    cast(json_extract(eventprops, '$.experiment_variation_id') as bigint) as experiment_variation_id,
    count(distinct userid) as unique_users
  from kafka_staging.etl_events
  where
    eventname = 'wpcom_experiment_variation_assigned' and
    eventprops like '%"experiment_id":"${experiment.experimentId}"%'
  group by cast(json_extract(eventprops, '$.experiment_variation_id') as bigint)
)

select
  experiment_variations.name as variation_name,
  unique_users
from tracks_counts
inner join wpcom.experiment_variations using (experiment_variation_id)`}
                </code>
              </pre>
            </AccordionDetails>
          </Accordion>
        </div>
      </div>
    </div>
  )
}
Example #26
Source File: Conversation.tsx    From prompts-ai with MIT License 4 votes vote down vote up
export default function Conversation(props: Props) {
    const styles = useStyles();
    const dispatch = useDispatch();
    const prompt = useSelector(selectPrompt);
    const globalCompletionParameters = useSelector(selectCompletionParameters);
    const conversation = useSelector((state: RootState) => {
        const workspace = state.editor.present.workspaces.find(w => w.id === state.editor.present.currentWorkspaceId)!;
        return workspace.conversations.find(c => c.id === props.id)!;
    });

    const hasStarted = conversation.parts.some(c => c.submitted);

    useEffect(() => {
        conversationBottom.current!.scrollTop = conversationBottom.current!.scrollHeight;
    });

    const conversationBottom = createRef<HTMLDivElement>();

    return <Card className={styles.card}>
        <CardContent>
            <Grid container alignItems={'center'} justify={'space-between'}>
                <Grid item><Typography>
                    {!hasStarted && (
                        "New Conversation"
                    )}
                    {hasStarted && (
                        <Box>
                            <Typography component={'span'}>Conversation #{props.ind}</Typography><br/>
                            <Typography variant={'caption'} component={'span'}>The prompt and parameters are locked.</Typography>
                        </Box>
                    )}
                </Typography></Grid>
                <Grid item>
                    {hasStarted && (
                        <IconButton onClick={() => {
                            dispatch(deleteConversation(props.id));
                        }}>
                            <Delete />
                        </IconButton>
                    )}
                </Grid>
            </Grid>
            <Box mt={1} className={styles.conversationBox}>
                <Paper className={styles.conversationBox} ref={conversationBottom}>
                    <Box ml={1} mt={1}>
                        {hasStarted && (<>
                            <Typography component={'span'} className={styles.promptedText}>{conversation.initialPrompt}</Typography>
                                {conversation.parts.map(part => (<>
                                    {part.source === ConversationPartSource.gpt && <Typography component={'span'} className={styles.generatedText}>{part.text}</Typography>}
                                    {part.source === ConversationPartSource.user && <Typography component={'span'} className={styles.promptedText}>{part.text}</Typography>}
                                </>))}
                        </>)}
                        {!hasStarted && (<>
                            <Typography component={'span'} className={styles.promptedText}>{prompt}</Typography>
                            <Typography component={'span'} className={styles.promptedText}>{conversation.restartSequence}</Typography>
                        </>)}
                        <div />
                    </Box>
                </Paper>
            </Box>
            <Box mt={2} className={styles.responseInput}>
                <Input conversationId={props.id} afterSend={() => {
                    conversationBottom.current!.scrollTop = conversationBottom.current!.scrollHeight;
                }}/>
            </Box>
            <Box mt={1}>
                <Accordion>
                    <AccordionSummary
                        expandIcon={<ExpandMoreIcon />}
                        aria-controls="panel1a-content"
                        id="panel1a-header"
                    >
                        <Typography>Parameters</Typography>
                    </AccordionSummary>
                    <AccordionDetails>
                        <Typography>
                            <Grid container spacing={1}>
                                <Grid item>
                                    <TextField
                                        value={conversation.restartSequence.split('\n').join('\\n')}
                                        onChange={(event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>) => {
                                            dispatch(updateConversationRestartSequence({
                                                conversationId: props.id,
                                                restartSequence: event.currentTarget.value.split('\\n').join('\n')
                                            }));
                                        }}
                                        className={styles.settingField}
                                        label={'Before User Input'}
                                        variant={'outlined'}
                                    />
                                </Grid>
                                <Grid item>
                                    <TextField
                                        value={conversation.startSequence.split('\n').join('\\n')}
                                        onChange={(event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>) => {
                                            dispatch(updateConversationStartSequence({
                                                conversationId: props.id,
                                                startSequence: event.currentTarget.value.split('\\n').join('\n')
                                            }));
                                        }}
                                        className={styles.settingField}
                                        label={'Before GPT-3 Completion'}
                                        variant={'outlined'}
                                    />
                                </Grid>
                            </Grid>
                            <Box mt={1}>
                                {conversation.completionParams === undefined && (
                                    <CompletionParameters parameters={globalCompletionParameters} />
                                )}
                                {conversation.completionParams !== undefined && (
                                    <CompletionParameters parameters={conversation.completionParams} />
                                )}
                            </Box>
                        </Typography>
                    </AccordionDetails>
                </Accordion>
            </Box>

        </CardContent>
        <CardActions>
        </CardActions>
    </Card>;
}
Example #27
Source File: DomainDetails.tsx    From crossfeed with Creative Commons Zero v1.0 Universal 4 votes vote down vote up
DomainDetails: React.FC<Props> = (props) => {
  const { domainId } = props;
  const { getDomain } = useDomainApi(false);
  const { user } = useAuthContext();
  const [domain, setDomain] = useState<Domain>();
  const classes = useStyles();
  const history = useHistory();

  const fetchDomain = useCallback(async () => {
    try {
      setDomain(undefined);
      const result = await getDomain(domainId);
      setDomain(result);
    } catch (e) {
      console.error(e);
    }
  }, [domainId, getDomain]);

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

  const webInfo = useMemo(() => {
    if (!domain) {
      return [];
    }
    const categoriesToProducts: Record<string, Set<string>> = {};
    for (const service of domain.services) {
      for (const product of service.products) {
        const version = product.version ? ` ${product.version}` : '';
        const value = product.name + version;
        const name =
          product.tags && product.tags.length > 0 ? product.tags[0] : 'Misc';
        if (!categoriesToProducts[name]) {
          categoriesToProducts[name] = new Set();
        }
        categoriesToProducts[name].add(value);
      }
    }
    return Object.entries(categoriesToProducts).reduce(
      (acc, [name, value]) => [
        ...acc,
        {
          label: name,
          value: Array.from(value).join(', ')
        }
      ],
      [] as any
    );
  }, [domain]);

  const overviewInfo = useMemo(() => {
    if (!domain) {
      return [];
    }
    const ret = [];
    if (domain.ip) {
      ret.push({
        label: 'IP',
        value: domain.ip
      });
    }
    ret.push({
      label: 'First Seen',
      value: `${differenceInCalendarDays(
        Date.now(),
        parseISO(domain.createdAt)
      )} days ago`
    });
    ret.push({
      label: 'Last Seen',
      value: `${differenceInCalendarDays(
        Date.now(),
        parseISO(domain.updatedAt)
      )} days ago`
    });
    if (domain.country) {
      ret.push({
        label: 'Country',
        value: domain.country
      });
    }
    if (domain.cloudHosted) {
      ret.push({
        label: 'Cloud Hosted',
        value: 'Yes'
      });
    }
    ret.push({
      label: 'Organization',
      value: domain.organization.name
    });
    return ret;
  }, [domain]);

  const [hiddenRows, setHiddenRows] = React.useState<{
    [key: string]: boolean;
  }>({});

  const formatBytes = (bytes: number, decimals = 2): string => {
    if (bytes === 0) return '0 Bytes';

    const k = 1024;
    const dm = decimals < 0 ? 0 : decimals;
    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];

    const i = Math.floor(Math.log(bytes) / Math.log(k));

    return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
  };

  const generateWebpageList = (tree: any, prefix = '') => {
    return (
      <List
        className={`${classes.listRoot}${prefix ? ' ' + classes.nested : ''}`}
      >
        {Object.keys(tree).map((key) => {
          const isWebpage =
            'url' in tree[key] && typeof tree[key]['url'] === 'string';
          if (!isWebpage) {
            const newPrefix = prefix + '/' + key;
            return (
              <>
                <ListItem
                  button
                  onClick={() => {
                    setHiddenRows((hiddenRows: any) => {
                      hiddenRows[newPrefix] =
                        newPrefix in hiddenRows ? !hiddenRows[newPrefix] : true;
                      return { ...hiddenRows };
                    });
                  }}
                  key={newPrefix}
                >
                  <ListItemText primary={(prefix ? '' : '/') + key + '/'} />
                  {hiddenRows[newPrefix] ? <ExpandLess /> : <ExpandMore />}
                </ListItem>
                <Collapse
                  in={!hiddenRows[newPrefix]}
                  timeout="auto"
                  unmountOnExit
                >
                  {generateWebpageList(tree[key], newPrefix)}
                </Collapse>
              </>
            );
          }
          const page = tree[key] as Webpage;
          const parsed = new URL(page.url);
          const split = parsed.pathname
            .replace(/\/$/, '') // Remove trailing slash
            .split('/');
          return (
            <ListItem
              button
              divider={true}
              key={page.url}
              onClick={() => window.open(page.url, '_blank')}
            >
              <ListItemText
                primary={(prefix ? '' : '/') + split.pop()}
                secondary={
                  page.status + ' • ' + formatBytes(page.responseSize ?? 0, 1)
                }
              ></ListItemText>
            </ListItem>
          );
        })}
      </List>
    );
  };

  if (!domain) {
    return null;
  }

  const url =
    (domain.services.find((service) => service.port === 443)
      ? 'https://'
      : 'http://') + domain.name;

  const { webpages = [] } = domain;
  webpages.sort((a, b) => (a.url > b.url ? 1 : -1));
  const webpageTree = generateWebpageTree(webpages);
  const webpageList = generateWebpageList(webpageTree);

  return (
    <Paper classes={{ root: classes.root }}>
      <div className={classes.title}>
        <h4>
          <Link to={`/inventory/domain/${domain.id}`}>{domain.name}</Link>
        </h4>

        <a href={url} target="_blank" rel="noopener noreferrer">
          <LinkOffIcon />
        </a>
      </div>
      <div className={classes.inner}>
        {overviewInfo.length > 0 && (
          <div className={classes.section}>
            <h4 className={classes.subtitle}>Overview</h4>
            <DefinitionList items={overviewInfo} />
          </div>
        )}
        {webInfo.length > 0 && (
          <div className={classes.section}>
            <h4 className={classes.subtitle}>Known Products</h4>
            <DefinitionList items={webInfo} />
          </div>
        )}

        {domain.vulnerabilities.length > 0 && (
          <div className={classes.section}>
            <h4 className={classes.subtitle}>Vulnerabilities</h4>
            <Accordion className={classes.accordionHeaderRow} disabled>
              <AccordionSummary>
                <Typography className={classes.accordionHeading}>
                  Title
                </Typography>
                <Typography className={classes.vulnDescription}>
                  Serverity
                </Typography>
                <Typography className={classes.vulnDescription}>
                  State
                </Typography>
                <Typography className={classes.vulnDescription}>
                  Created
                </Typography>
              </AccordionSummary>
            </Accordion>
            {domain.vulnerabilities.map((vuln) => (
              <Accordion
                className={classes.accordion}
                key={vuln.id}
                onClick={(event) => {
                  event.stopPropagation();
                  history.push('/inventory/vulnerability/' + vuln.id);
                }}
              >
                <AccordionSummary>
                  <Typography className={classes.accordionHeading}>
                    {vuln.title}
                  </Typography>
                  <Typography className={classes.vulnDescription}>
                    {vuln.severity}
                  </Typography>
                  <Typography className={classes.vulnDescription}>
                    {vuln.state}
                  </Typography>
                  <Typography className={classes.vulnDescription}>
                    {vuln.createdAt
                      ? `${differenceInCalendarDays(
                          Date.now(),
                          parseISO(vuln.createdAt)
                        )} days ago`
                      : ''}
                  </Typography>
                </AccordionSummary>
              </Accordion>
            ))}
          </div>
        )}
        {domain.services.length > 0 && (
          <div className={classes.section}>
            <h4 className={classes.subtitle}>Ports</h4>
            <Accordion className={classes.accordionHeaderRow} disabled>
              <AccordionSummary expandIcon={<ExpandMore />}>
                <Typography className={classes.accordionHeading}>
                  Port
                </Typography>
                <Typography className={classes.accordionHeading}>
                  Products
                </Typography>
                <Typography className={classes.lastSeen}>Last Seen</Typography>
              </AccordionSummary>
            </Accordion>
            {domain.services.map((service) => {
              const products = service.products
                .map(
                  (product) =>
                    product.name +
                    (product.version ? ` ${product.version}` : '')
                )
                .join(', ');
              return (
                <Accordion className={classes.accordion} key={service.id}>
                  <AccordionSummary expandIcon={<ExpandMore />}>
                    <Typography className={classes.accordionHeading}>
                      {service.port}
                    </Typography>
                    <Typography className={classes.accordionHeading}>
                      {products}
                    </Typography>
                    <Typography className={classes.lastSeen}>
                      {service.lastSeen
                        ? `${differenceInCalendarDays(
                            Date.now(),
                            parseISO(service.lastSeen)
                          )} days ago`
                        : ''}
                    </Typography>
                  </AccordionSummary>
                  {service.products.length > 0 && (
                    <AccordionDetails>
                      <DefinitionList
                        items={[
                          {
                            label: 'Products',
                            value: products
                          },
                          {
                            label: 'Banner',
                            value:
                              (user?.userType === 'globalView' ||
                                user?.userType === 'globalAdmin') &&
                              service.banner
                                ? service.banner
                                : 'None'
                          }
                        ]}
                      />
                    </AccordionDetails>
                  )}
                </Accordion>
              );
            })}
          </div>
        )}
        {domain.webpages?.length > 0 && (
          <div className={classes.section}>
            <h4 className={classes.subtitle}>Site Map</h4>
            {webpageList}
          </div>
        )}
      </div>
    </Paper>
  );
}
Example #28
Source File: stage-chart.tsx    From backstage with Apache License 2.0 4 votes vote down vote up
export function StageChart(props: StageChartProps) {
  const { stage, ...chartOptions } = props;
  const {
    chartTypes,
    defaultCollapsed = 0,
    defaultHidden = 0,
    zeroYAxis = false,
  } = chartOptions;

  const { zoomFilterValues } = useZoom();
  const { zoomProps, getZoomArea } = useZoomArea();

  const ticks = useMemo(
    () => pickElements(stage.values, 8).map(val => val.__epoch),
    [stage.values],
  );
  const domainY = useMemo(
    () => [zeroYAxis ? 0 : 'auto', 'auto'] as YAxisProps['domain'],
    [zeroYAxis],
  );
  const statuses = useMemo(
    () => statusTypes.filter(status => stage.statusSet.has(status)),
    [stage.statusSet],
  );
  const legendPayload = useMemo(
    (): LegendProps['payload'] =>
      statuses.map(status => ({
        value: capitalize(status),
        type: 'line',
        id: status,
        color: statusColorMap[status],
      })),
    [statuses],
  );

  const subStages = useMemo(
    () =>
      new Map<string, ChartableStage>(
        [...stage.stages.entries()].filter(
          ([_name, subStage]) => subStage.combinedAnalysis.max > defaultHidden,
        ),
      ),
    [stage.stages, defaultHidden],
  );

  const zoomFilteredValues = useMemo(
    () => zoomFilterValues(stage.values),
    [stage.values, zoomFilterValues],
  );

  return stage.combinedAnalysis.max < defaultHidden ? null : (
    <Accordion
      defaultExpanded={stage.combinedAnalysis.max > defaultCollapsed}
      TransitionProps={transitionProps}
    >
      <AccordionSummary expandIcon={<ExpandMoreIcon />}>
        <Typography>
          {stage.name} (med {formatDuration(stage.combinedAnalysis.med)}, avg{' '}
          {formatDuration(stage.combinedAnalysis.avg)})
        </Typography>
      </AccordionSummary>
      <AccordionDetails>
        {stage.values.length === 0 ? (
          <Alert severity="info">No data</Alert>
        ) : (
          <Grid container direction="column">
            <Grid item style={noUserSelect}>
              <ResponsiveContainer width="100%" height={140}>
                <ComposedChart data={zoomFilteredValues} {...zoomProps}>
                  <defs>
                    <linearGradient id="colorDur" x1="0" y1="0" x2="0" y2="1">
                      {fireColors.map(([percent, color]) => (
                        <stop
                          key={percent}
                          offset={percent}
                          stopColor={color}
                          stopOpacity={0.8}
                        />
                      ))}
                    </linearGradient>
                  </defs>
                  {statuses.length > 1 && <Legend payload={legendPayload} />}
                  <CartesianGrid strokeDasharray="3 3" />
                  <XAxis
                    dataKey="__epoch"
                    type="category"
                    ticks={ticks}
                    tickFormatter={tickFormatterX}
                  />
                  <YAxis
                    yAxisId={1}
                    tickFormatter={tickFormatterY}
                    type="number"
                    tickCount={5}
                    name="Duration"
                    domain={domainY}
                  />
                  <YAxis
                    yAxisId={2}
                    orientation="right"
                    type="number"
                    tickCount={5}
                    name="Count"
                  />
                  <Tooltip
                    formatter={tooltipValueFormatter}
                    labelFormatter={labelFormatter}
                  />
                  {statuses.reverse().map(status => (
                    <Fragment key={status}>
                      {!chartTypes[status].includes('duration') ? null : (
                        <>
                          <Area
                            isAnimationActive={false}
                            yAxisId={1}
                            type="monotone"
                            dataKey={status}
                            stackId={status}
                            stroke={
                              statuses.length > 1
                                ? statusColorMap[status]
                                : colorStroke
                            }
                            fillOpacity={statuses.length > 1 ? 0.5 : 1}
                            fill={
                              statuses.length > 1
                                ? statusColorMap[status]
                                : 'url(#colorDur)'
                            }
                            connectNulls
                          />
                          <Line
                            isAnimationActive={false}
                            yAxisId={1}
                            type="monotone"
                            dataKey={`${status} avg`}
                            stroke={
                              statuses.length > 1
                                ? statusColorMap[status]
                                : colorStrokeAvg
                            }
                            opacity={0.8}
                            strokeWidth={2}
                            dot={false}
                            connectNulls
                          />
                        </>
                      )}
                      {!chartTypes[status].includes('count') ? null : (
                        <Bar
                          isAnimationActive={false}
                          yAxisId={2}
                          type="monotone"
                          dataKey={`${status} count`}
                          stackId="1"
                          stroke={statusColorMap[status] ?? ''}
                          fillOpacity={0.5}
                          fill={statusColorMap[status] ?? ''}
                        />
                      )}
                    </Fragment>
                  ))}
                  {getZoomArea({ yAxisId: 1 })}
                </ComposedChart>
              </ResponsiveContainer>
            </Grid>
            {subStages.size === 0 ? null : (
              <Grid item>
                <Accordion
                  defaultExpanded={false}
                  TransitionProps={transitionProps}
                >
                  <AccordionSummary expandIcon={<ExpandMoreIcon />}>
                    <Typography>Sub stages ({subStages.size})</Typography>
                  </AccordionSummary>
                  <AccordionDetails>
                    <div style={fullWidth}>
                      {[...subStages.values()].map(subStage => (
                        <StageChart
                          key={subStage.name}
                          {...chartOptions}
                          stage={subStage}
                        />
                      ))}
                    </div>
                  </AccordionDetails>
                </Accordion>
              </Grid>
            )}
          </Grid>
        )}
      </AccordionDetails>
    </Accordion>
  );
}
Example #29
Source File: SearchType.Accordion.tsx    From backstage with Apache License 2.0 4 votes vote down vote up
SearchTypeAccordion = (props: SearchTypeAccordionProps) => {
  const classes = useStyles();
  const { setPageCursor, setTypes, types } = useSearch();
  const [expanded, setExpanded] = useState(true);
  const { defaultValue, name, types: givenTypes } = props;

  const toggleExpanded = () => setExpanded(prevState => !prevState);
  const handleClick = (type: string) => {
    return () => {
      setTypes(type !== '' ? [type] : []);
      setPageCursor(undefined);
      setExpanded(false);
    };
  };

  // Handle any provided defaultValue
  useEffect(() => {
    if (defaultValue) {
      setTypes([defaultValue]);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const definedTypes = [
    {
      value: '',
      name: 'All',
      icon: <AllIcon />,
    },
    ...givenTypes,
  ];
  const selected = types[0] || '';

  return (
    <Card className={classes.card}>
      <CardHeader title={name} titleTypographyProps={{ variant: 'overline' }} />
      <CardContent className={classes.cardContent}>
        <Accordion
          className={classes.accordion}
          expanded={expanded}
          onChange={toggleExpanded}
        >
          <AccordionSummary
            classes={{
              root: classes.accordionSummary,
              content: classes.accordionSummaryContent,
            }}
            expandIcon={<ExpandMoreIcon className={classes.icon} />}
            IconButtonProps={{ size: 'small' }}
          >
            {expanded
              ? 'Collapse'
              : definedTypes.filter(t => t.value === selected)[0]!.name}
          </AccordionSummary>
          <AccordionDetails classes={{ root: classes.accordionDetails }}>
            <List
              className={classes.list}
              component="nav"
              aria-label="filter by type"
              disablePadding
              dense
            >
              {definedTypes.map(type => (
                <Fragment key={type.value}>
                  <Divider />
                  <ListItem
                    selected={
                      types[0] === type.value ||
                      (types.length === 0 && type.value === '')
                    }
                    onClick={handleClick(type.value)}
                    button
                  >
                    <ListItemIcon>
                      {cloneElement(type.icon, {
                        className: classes.listItemIcon,
                      })}
                    </ListItemIcon>
                    <ListItemText primary={type.name} />
                  </ListItem>
                </Fragment>
              ))}
            </List>
          </AccordionDetails>
        </Accordion>
      </CardContent>
    </Card>
  );
}