@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 |
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 |
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 |
/**
* 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} />
)}
"Observed" 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'>
"Observed" 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 |
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 |
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>
);
}