mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02:00
Project stats feedback (#3131)
## About the changes  https://linear.app/unleash/issue/1-694/widgets-explanation-plausible-buttons
This commit is contained in:
parent
eac5fca44c
commit
c0ec6f20b2
@ -0,0 +1,76 @@
|
|||||||
|
import { useState, VFC } from 'react';
|
||||||
|
import { Box, Paper, Button, styled } from '@mui/material';
|
||||||
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||||
|
|
||||||
|
interface IFeedbackProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledBox = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
marginTop: theme.spacing(0.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const Feedback: VFC<IFeedbackProps> = ({ id }) => {
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const { value: selectedValue, setValue: setSelectedValue } =
|
||||||
|
createLocalStorage<{ value?: 'yes' | 'no' }>(
|
||||||
|
`ProjectOverviewFeedback:v1:${id}`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
const [selected, setSelected] = useState<'yes' | 'no' | undefined>(
|
||||||
|
selectedValue.value
|
||||||
|
);
|
||||||
|
const { trackEvent } = usePlausibleTracker();
|
||||||
|
|
||||||
|
if (!uiConfig?.flags?.T) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTrackFeedback = (value: 'yes' | 'no') => {
|
||||||
|
setSelected(value);
|
||||||
|
setSelectedValue({ value });
|
||||||
|
trackEvent('project_overview', {
|
||||||
|
props: {
|
||||||
|
eventType: id,
|
||||||
|
wasHelpful: value === 'yes',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
background: theme => theme.palette.neutral.light,
|
||||||
|
padding: theme => theme.spacing(1.5, 2),
|
||||||
|
marginTop: theme => theme.spacing(1.5),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Was this information useful to you?
|
||||||
|
<StyledBox>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant={selected === 'yes' ? 'contained' : 'outlined'}
|
||||||
|
sx={{ padding: 0 }}
|
||||||
|
onClick={() => onTrackFeedback('yes')}
|
||||||
|
disabled={Boolean(selected)}
|
||||||
|
>
|
||||||
|
Yes
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant={selected === 'no' ? 'contained' : 'outlined'}
|
||||||
|
sx={{ padding: 0 }}
|
||||||
|
onClick={() => onTrackFeedback('no')}
|
||||||
|
disabled={Boolean(selected)}
|
||||||
|
>
|
||||||
|
No
|
||||||
|
</Button>
|
||||||
|
</StyledBox>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,75 @@
|
|||||||
|
import { FC, useState } from 'react';
|
||||||
|
import { Close, HelpOutline } from '@mui/icons-material';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
IconButton,
|
||||||
|
Popper,
|
||||||
|
Paper,
|
||||||
|
ClickAwayListener,
|
||||||
|
styled,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { Feedback } from './Feedback';
|
||||||
|
|
||||||
|
interface IHelpPopperProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledPaper = styled(Paper)(({ theme }) => ({
|
||||||
|
padding: theme.spacing(3, 3),
|
||||||
|
maxWidth: '350px',
|
||||||
|
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||||
|
border: `1px solid ${theme.palette.neutral.border}`,
|
||||||
|
fontSize: theme.typography.body2.fontSize,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const HelpPopper: FC<IHelpPopperProps> = ({ children, id }) => {
|
||||||
|
const [anchor, setAnchorEl] = useState<null | Element>(null);
|
||||||
|
|
||||||
|
const onOpen = (event: React.FormEvent<HTMLButtonElement>) =>
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
|
||||||
|
const onClose = () => setAnchorEl(null);
|
||||||
|
|
||||||
|
const open = Boolean(anchor);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: theme => theme.spacing(0.5),
|
||||||
|
right: theme => theme.spacing(0.5),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton onClick={onOpen} aria-describedby={id} size="small">
|
||||||
|
<HelpOutline
|
||||||
|
sx={{ fontSize: theme => theme.typography.body1.fontSize }}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<Popper
|
||||||
|
id={id}
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchor}
|
||||||
|
sx={theme => ({ zIndex: theme.zIndex.tooltip })}
|
||||||
|
>
|
||||||
|
<ClickAwayListener onClickAway={onClose}>
|
||||||
|
<StyledPaper elevation={3}>
|
||||||
|
<IconButton
|
||||||
|
onClick={onClose}
|
||||||
|
sx={{ position: 'absolute', right: 4, top: 4 }}
|
||||||
|
>
|
||||||
|
<Close
|
||||||
|
sx={{
|
||||||
|
fontSize: theme =>
|
||||||
|
theme.typography.body1.fontSize,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
{children}
|
||||||
|
<Feedback id={id} />
|
||||||
|
</StyledPaper>
|
||||||
|
</ClickAwayListener>
|
||||||
|
</Popper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,6 @@
|
|||||||
import { Box, styled, Typography } from '@mui/material';
|
import { Box, styled, Typography } from '@mui/material';
|
||||||
import { ProjectStatsSchema } from 'openapi/models';
|
import { ProjectStatsSchema } from 'openapi/models';
|
||||||
|
import { HelpPopper } from './HelpPopper';
|
||||||
import { StatusBox } from './StatusBox';
|
import { StatusBox } from './StatusBox';
|
||||||
|
|
||||||
const StyledBox = styled(Box)(({ theme }) => ({
|
const StyledBox = styled(Box)(({ theme }) => ({
|
||||||
@ -20,6 +21,7 @@ const StyledBox = styled(Box)(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledWidget = styled(Box)(({ theme }) => ({
|
const StyledWidget = styled(Box)(({ theme }) => ({
|
||||||
|
position: 'relative',
|
||||||
padding: theme.spacing(3),
|
padding: theme.spacing(3),
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@ -73,7 +75,12 @@ export const ProjectStats = ({ stats }: IProjectStatsProps) => {
|
|||||||
projectActivityPastWindow -
|
projectActivityPastWindow -
|
||||||
20
|
20
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
|
<HelpPopper id="total-changes">
|
||||||
|
Sum of all configuration and state modifications in the
|
||||||
|
project.
|
||||||
|
</HelpPopper>
|
||||||
|
</StatusBox>
|
||||||
</StyledWidget>
|
</StyledWidget>
|
||||||
<StyledWidget>
|
<StyledWidget>
|
||||||
<StatusBox
|
<StatusBox
|
||||||
@ -95,7 +102,13 @@ export const ProjectStats = ({ stats }: IProjectStatsProps) => {
|
|||||||
avgTimeToProdPastWindow
|
avgTimeToProdPastWindow
|
||||||
)}
|
)}
|
||||||
percentage
|
percentage
|
||||||
/>
|
>
|
||||||
|
<HelpPopper id="avg-time-to-prod">
|
||||||
|
How long did it take on average from a feature toggle
|
||||||
|
was created until it was enabled in an environment of
|
||||||
|
type production.
|
||||||
|
</HelpPopper>
|
||||||
|
</StatusBox>
|
||||||
</StyledWidget>
|
</StyledWidget>
|
||||||
<StyledWidget>
|
<StyledWidget>
|
||||||
<StatusBox
|
<StatusBox
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { FC, ReactNode } from 'react';
|
||||||
import { CallMade, SouthEast } from '@mui/icons-material';
|
import { CallMade, SouthEast } from '@mui/icons-material';
|
||||||
import { Box, Typography, styled } from '@mui/material';
|
import { Box, Typography, styled } from '@mui/material';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
@ -53,17 +53,19 @@ const resolveColor = (change: number) => {
|
|||||||
return 'warning.dark';
|
return 'warning.dark';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StatusBox = ({
|
export const StatusBox: FC<IStatusBoxProps> = ({
|
||||||
title,
|
title,
|
||||||
boxText,
|
boxText,
|
||||||
change,
|
change,
|
||||||
percentage,
|
percentage,
|
||||||
}: IStatusBoxProps) => (
|
children,
|
||||||
|
}) => (
|
||||||
<>
|
<>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={Boolean(title)}
|
condition={Boolean(title)}
|
||||||
show={<StyledTypographyHeader>{title}</StyledTypographyHeader>}
|
show={<StyledTypographyHeader>{title}</StyledTypographyHeader>}
|
||||||
/>
|
/>
|
||||||
|
{children}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
...flexRow,
|
...flexRow,
|
||||||
|
@ -12,7 +12,7 @@ type UsePersistentGlobalState<T> = () => [
|
|||||||
* The state is also persisted to localStorage and restored on page load.
|
* The state is also persisted to localStorage and restored on page load.
|
||||||
* The localStorage state is not synced between tabs.
|
* The localStorage state is not synced between tabs.
|
||||||
*
|
*
|
||||||
* @deprecated `hooks/useLocalStorage` -- we don't need `react-hooks-global-state`
|
* @deprecated `utils/createLocalStorage` -- we don't need `react-hooks-global-state`
|
||||||
*/
|
*/
|
||||||
export const createPersistentGlobalStateHook = <T extends object>(
|
export const createPersistentGlobalStateHook = <T extends object>(
|
||||||
key: string,
|
key: string,
|
||||||
|
@ -16,6 +16,7 @@ type CustomEvents =
|
|||||||
| 'maintenance'
|
| 'maintenance'
|
||||||
| 'message_banner'
|
| 'message_banner'
|
||||||
| 'hidden_environment'
|
| 'hidden_environment'
|
||||||
|
| 'project_overview'
|
||||||
| 'suggest_tags'
|
| 'suggest_tags'
|
||||||
| 'unknown_ui_error';
|
| 'unknown_ui_error';
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user