mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-15 01:16:22 +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 { ProjectStatsSchema } from 'openapi/models';
|
||||
import { HelpPopper } from './HelpPopper';
|
||||
import { StatusBox } from './StatusBox';
|
||||
|
||||
const StyledBox = styled(Box)(({ theme }) => ({
|
||||
@ -20,6 +21,7 @@ const StyledBox = styled(Box)(({ theme }) => ({
|
||||
}));
|
||||
|
||||
const StyledWidget = styled(Box)(({ theme }) => ({
|
||||
position: 'relative',
|
||||
padding: theme.spacing(3),
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
flex: 1,
|
||||
@ -73,7 +75,12 @@ export const ProjectStats = ({ stats }: IProjectStatsProps) => {
|
||||
projectActivityPastWindow -
|
||||
20
|
||||
}
|
||||
/>
|
||||
>
|
||||
<HelpPopper id="total-changes">
|
||||
Sum of all configuration and state modifications in the
|
||||
project.
|
||||
</HelpPopper>
|
||||
</StatusBox>
|
||||
</StyledWidget>
|
||||
<StyledWidget>
|
||||
<StatusBox
|
||||
@ -95,7 +102,13 @@ export const ProjectStats = ({ stats }: IProjectStatsProps) => {
|
||||
avgTimeToProdPastWindow
|
||||
)}
|
||||
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>
|
||||
<StatusBox
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { CallMade, SouthEast } from '@mui/icons-material';
|
||||
import { Box, Typography, styled } from '@mui/material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
@ -53,17 +53,19 @@ const resolveColor = (change: number) => {
|
||||
return 'warning.dark';
|
||||
};
|
||||
|
||||
export const StatusBox = ({
|
||||
export const StatusBox: FC<IStatusBoxProps> = ({
|
||||
title,
|
||||
boxText,
|
||||
change,
|
||||
percentage,
|
||||
}: IStatusBoxProps) => (
|
||||
children,
|
||||
}) => (
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(title)}
|
||||
show={<StyledTypographyHeader>{title}</StyledTypographyHeader>}
|
||||
/>
|
||||
{children}
|
||||
<Box
|
||||
sx={{
|
||||
...flexRow,
|
||||
|
@ -12,7 +12,7 @@ type UsePersistentGlobalState<T> = () => [
|
||||
* The state is also persisted to localStorage and restored on page load.
|
||||
* 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>(
|
||||
key: string,
|
||||
|
@ -16,6 +16,7 @@ type CustomEvents =
|
||||
| 'maintenance'
|
||||
| 'message_banner'
|
||||
| 'hidden_environment'
|
||||
| 'project_overview'
|
||||
| 'suggest_tags'
|
||||
| 'unknown_ui_error';
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user