mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
chore: add event timeline to new in unleash (#8358)
https://linear.app/unleash/issue/2-2729/add-event-timeline-to-new-in-unleash Adds the new event timeline to the "New in Unleash" section. Unlike Signals & Actions, the Event timeline doesn’t have a dedicated page to link to, as it's a global component within the layout. To address this, we extend the "check it out" action in the New in Unleash component by supporting a callback instead of a link. When the user clicks "check it out" for this new item, the page smoothly scrolls to the top, ~~the timeline opens (if it's not already)~~, and a temporary highlight effect is triggered on the timeline header button. Also includes some scouting / slight UX adjustments. https://github.com/user-attachments/assets/fe49f21b-5986-46b2-8fc6-acb4daef9d08
This commit is contained in:
parent
ec1fe6278a
commit
52b7e235fd
11
frontend/src/assets/img/eventTimeline.svg
Normal file
11
frontend/src/assets/img/eventTimeline.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 11 KiB |
@ -1,4 +1,4 @@
|
|||||||
import type { ReactNode } from 'react';
|
import { useState, type ReactNode } from 'react';
|
||||||
import { EventTimelineContext } from './EventTimelineContext';
|
import { EventTimelineContext } from './EventTimelineContext';
|
||||||
import { useLocalStorageState } from 'hooks/useLocalStorageState';
|
import { useLocalStorageState } from 'hooks/useLocalStorageState';
|
||||||
import type { IEnvironment } from 'interfaces/environments';
|
import type { IEnvironment } from 'interfaces/environments';
|
||||||
@ -10,22 +10,28 @@ type TimeSpanOption = {
|
|||||||
markers: string[];
|
markers: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type EventTimelineState = {
|
type EventTimelinePersistentState = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
timeSpan: TimeSpanOption;
|
timeSpan: TimeSpanOption;
|
||||||
environment?: IEnvironment;
|
environment?: IEnvironment;
|
||||||
signalsSuggestionSeen?: boolean;
|
signalsSuggestionSeen?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type EventTimelineTemporaryState = {
|
||||||
|
highlighted: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type EventTimelineStateSetters = {
|
type EventTimelineStateSetters = {
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
setTimeSpan: (timeSpan: TimeSpanOption) => void;
|
setTimeSpan: (timeSpan: TimeSpanOption) => void;
|
||||||
setEnvironment: (environment: IEnvironment) => void;
|
setEnvironment: (environment: IEnvironment) => void;
|
||||||
setSignalsSuggestionSeen: (seen: boolean) => void;
|
setSignalsSuggestionSeen: (seen: boolean) => void;
|
||||||
|
setHighlighted: (highlighted: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IEventTimelineContext
|
export interface IEventTimelineContext
|
||||||
extends EventTimelineState,
|
extends EventTimelinePersistentState,
|
||||||
|
EventTimelineTemporaryState,
|
||||||
EventTimelineStateSetters {}
|
EventTimelineStateSetters {}
|
||||||
|
|
||||||
export const timeSpanOptions: TimeSpanOption[] = [
|
export const timeSpanOptions: TimeSpanOption[] = [
|
||||||
@ -77,7 +83,7 @@ export const timeSpanOptions: TimeSpanOption[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const defaultState: EventTimelineState = {
|
const defaultState: EventTimelinePersistentState = {
|
||||||
open: false,
|
open: false,
|
||||||
timeSpan: timeSpanOptions[0],
|
timeSpan: timeSpanOptions[0],
|
||||||
};
|
};
|
||||||
@ -89,20 +95,30 @@ interface IEventTimelineProviderProps {
|
|||||||
export const EventTimelineProvider = ({
|
export const EventTimelineProvider = ({
|
||||||
children,
|
children,
|
||||||
}: IEventTimelineProviderProps) => {
|
}: IEventTimelineProviderProps) => {
|
||||||
const [state, setState] = useLocalStorageState<EventTimelineState>(
|
const [state, setState] =
|
||||||
'event-timeline:v1',
|
useLocalStorageState<EventTimelinePersistentState>(
|
||||||
defaultState,
|
'event-timeline:v1',
|
||||||
);
|
defaultState,
|
||||||
|
);
|
||||||
|
const [highlighted, setHighlighted] = useState(false);
|
||||||
|
|
||||||
const setField = <K extends keyof EventTimelineState>(
|
const setField = <K extends keyof EventTimelinePersistentState>(
|
||||||
key: K,
|
key: K,
|
||||||
value: EventTimelineState[K],
|
value: EventTimelinePersistentState[K],
|
||||||
) => {
|
) => {
|
||||||
setState((prevState) => ({ ...prevState, [key]: value }));
|
setState((prevState) => ({ ...prevState, [key]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onSetHighlighted = (highlighted: boolean) => {
|
||||||
|
setHighlighted(highlighted);
|
||||||
|
if (highlighted) {
|
||||||
|
setTimeout(() => setHighlighted(false), 3000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const contextValue: IEventTimelineContext = {
|
const contextValue: IEventTimelineContext = {
|
||||||
...state,
|
...state,
|
||||||
|
highlighted,
|
||||||
setOpen: (open: boolean) => setField('open', open),
|
setOpen: (open: boolean) => setField('open', open),
|
||||||
setTimeSpan: (timeSpan: TimeSpanOption) =>
|
setTimeSpan: (timeSpan: TimeSpanOption) =>
|
||||||
setField('timeSpan', timeSpan),
|
setField('timeSpan', timeSpan),
|
||||||
@ -110,6 +126,7 @@ export const EventTimelineProvider = ({
|
|||||||
setField('environment', environment),
|
setField('environment', environment),
|
||||||
setSignalsSuggestionSeen: (seen: boolean) =>
|
setSignalsSuggestionSeen: (seen: boolean) =>
|
||||||
setField('signalsSuggestionSeen', seen),
|
setField('signalsSuggestionSeen', seen),
|
||||||
|
setHighlighted: onSetHighlighted,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
import { type FC, useEffect } from 'react';
|
import { type FC, useEffect } from 'react';
|
||||||
import { useLastViewedProject } from 'hooks/useLastViewedProject';
|
import { useLastViewedProject } from 'hooks/useLastViewedProject';
|
||||||
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
||||||
|
import { EventTimelineProvider } from 'component/events/EventTimeline/EventTimelineProvider';
|
||||||
|
|
||||||
const server = testServerSetup();
|
const server = testServerSetup();
|
||||||
|
|
||||||
@ -18,8 +19,29 @@ beforeEach(() => {
|
|||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const TestNavigationSidebar: FC<{
|
||||||
|
project?: string;
|
||||||
|
flags?: LastViewedFlag[];
|
||||||
|
}> = ({ project, flags }) => {
|
||||||
|
const { setLastViewed: setProject } = useLastViewedProject();
|
||||||
|
const { setLastViewed: setFlag } = useLastViewedFlags();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setProject(project);
|
||||||
|
flags?.forEach((flag) => {
|
||||||
|
setFlag(flag);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EventTimelineProvider>
|
||||||
|
<NavigationSidebar />
|
||||||
|
</EventTimelineProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
test('switch full mode and mini mode', () => {
|
test('switch full mode and mini mode', () => {
|
||||||
render(<NavigationSidebar />);
|
render(<TestNavigationSidebar />);
|
||||||
|
|
||||||
expect(screen.queryByText('Projects')).toBeInTheDocument();
|
expect(screen.queryByText('Projects')).toBeInTheDocument();
|
||||||
expect(screen.queryByText('Applications')).toBeInTheDocument();
|
expect(screen.queryByText('Applications')).toBeInTheDocument();
|
||||||
@ -42,7 +64,7 @@ test('switch full mode and mini mode', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('persist navigation mode and expansion selection in storage', async () => {
|
test('persist navigation mode and expansion selection in storage', async () => {
|
||||||
render(<NavigationSidebar />);
|
render(<TestNavigationSidebar />);
|
||||||
const { value } = createLocalStorage('navigation-mode:v1', {});
|
const { value } = createLocalStorage('navigation-mode:v1', {});
|
||||||
expect(value).toBe('full');
|
expect(value).toBe('full');
|
||||||
|
|
||||||
@ -70,7 +92,7 @@ test('persist navigation mode and expansion selection in storage', async () => {
|
|||||||
test('select active item', async () => {
|
test('select active item', async () => {
|
||||||
render(
|
render(
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={'/search'} element={<NavigationSidebar />} />
|
<Route path={'/search'} element={<TestNavigationSidebar />} />
|
||||||
</Routes>,
|
</Routes>,
|
||||||
{ route: '/search' },
|
{ route: '/search' },
|
||||||
);
|
);
|
||||||
@ -80,30 +102,13 @@ test('select active item', async () => {
|
|||||||
expect(links[1]).toHaveClass(classes.selected);
|
expect(links[1]).toHaveClass(classes.selected);
|
||||||
});
|
});
|
||||||
|
|
||||||
const SetupComponent: FC<{ project: string; flags: LastViewedFlag[] }> = ({
|
|
||||||
project,
|
|
||||||
flags,
|
|
||||||
}) => {
|
|
||||||
const { setLastViewed: setProject } = useLastViewedProject();
|
|
||||||
const { setLastViewed: setFlag } = useLastViewedFlags();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setProject(project);
|
|
||||||
flags.forEach((flag) => {
|
|
||||||
setFlag(flag);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return <NavigationSidebar />;
|
|
||||||
};
|
|
||||||
|
|
||||||
test('print recent projects and flags', async () => {
|
test('print recent projects and flags', async () => {
|
||||||
testServerRoute(server, `/api/admin/projects/projectA/overview`, {
|
testServerRoute(server, `/api/admin/projects/projectA/overview`, {
|
||||||
name: 'projectNameA',
|
name: 'projectNameA',
|
||||||
});
|
});
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<SetupComponent
|
<TestNavigationSidebar
|
||||||
project={'projectA'}
|
project={'projectA'}
|
||||||
flags={[{ featureId: 'featureA', projectId: 'projectB' }]}
|
flags={[{ featureId: 'featureA', projectId: 'projectB' }]}
|
||||||
/>,
|
/>,
|
||||||
|
@ -16,6 +16,10 @@ import type { NavigationMode } from 'component/layout/MainLayout/NavigationSideb
|
|||||||
import { NewInUnleashItem } from './NewInUnleashItem';
|
import { NewInUnleashItem } from './NewInUnleashItem';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
import { ReactComponent as SignalsPreview } from 'assets/img/signals.svg';
|
import { ReactComponent as SignalsPreview } from 'assets/img/signals.svg';
|
||||||
|
import LinearScaleIcon from '@mui/icons-material/LinearScale';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useEventTimelineContext } from 'component/events/EventTimeline/EventTimelineContext';
|
||||||
|
import { ReactComponent as EventTimelinePreview } from 'assets/img/eventTimeline.svg';
|
||||||
|
|
||||||
const StyledNewInUnleash = styled('div')(({ theme }) => ({
|
const StyledNewInUnleash = styled('div')(({ theme }) => ({
|
||||||
margin: theme.spacing(2, 0, 1, 0),
|
margin: theme.spacing(2, 0, 1, 0),
|
||||||
@ -67,11 +71,15 @@ const StyledSignalsIcon = styled(Signals)(({ theme }) => ({
|
|||||||
color: theme.palette.primary.main,
|
color: theme.palette.primary.main,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledLinearScaleIcon = styled(LinearScaleIcon)(({ theme }) => ({
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
}));
|
||||||
|
|
||||||
type NewItem = {
|
type NewItem = {
|
||||||
label: string;
|
label: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
link: string;
|
onCheckItOut: () => void;
|
||||||
docsLink: string;
|
docsLink: string;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
longDescription: ReactNode;
|
longDescription: ReactNode;
|
||||||
@ -89,13 +97,17 @@ export const NewInUnleash = ({
|
|||||||
onItemClick,
|
onItemClick,
|
||||||
onMiniModeClick,
|
onMiniModeClick,
|
||||||
}: INewInUnleashProps) => {
|
}: INewInUnleashProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
const { trackEvent } = usePlausibleTracker();
|
const { trackEvent } = usePlausibleTracker();
|
||||||
const [seenItems, setSeenItems] = useLocalStorageState(
|
const [seenItems, setSeenItems] = useLocalStorageState(
|
||||||
'new-in-unleash-seen:v1',
|
'new-in-unleash-seen:v1',
|
||||||
new Set(),
|
new Set(),
|
||||||
);
|
);
|
||||||
const { isEnterprise } = useUiConfig();
|
const { isOss, isEnterprise } = useUiConfig();
|
||||||
const signalsEnabled = useUiFlag('signals');
|
const signalsEnabled = useUiFlag('signals');
|
||||||
|
const eventTimelineEnabled = useUiFlag('eventTimeline');
|
||||||
|
|
||||||
|
const { setHighlighted } = useEventTimelineContext();
|
||||||
|
|
||||||
const items: NewItem[] = [
|
const items: NewItem[] = [
|
||||||
{
|
{
|
||||||
@ -103,7 +115,7 @@ export const NewInUnleash = ({
|
|||||||
summary: 'Listen to signals via Webhooks',
|
summary: 'Listen to signals via Webhooks',
|
||||||
icon: <StyledSignalsIcon />,
|
icon: <StyledSignalsIcon />,
|
||||||
preview: <SignalsPreview />,
|
preview: <SignalsPreview />,
|
||||||
link: '/integrations/signals',
|
onCheckItOut: () => navigate('/integrations/signals'),
|
||||||
docsLink: 'https://docs.getunleash.io/reference/signals',
|
docsLink: 'https://docs.getunleash.io/reference/signals',
|
||||||
show: isEnterprise() && signalsEnabled,
|
show: isEnterprise() && signalsEnabled,
|
||||||
longDescription: (
|
longDescription: (
|
||||||
@ -134,6 +146,35 @@ export const NewInUnleash = ({
|
|||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Event timeline',
|
||||||
|
summary: 'Keep track of recent events across all your projects',
|
||||||
|
icon: <StyledLinearScaleIcon />,
|
||||||
|
preview: <EventTimelinePreview />,
|
||||||
|
onCheckItOut: () => {
|
||||||
|
setHighlighted(true);
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
docsLink: 'https://docs.getunleash.io/reference/events',
|
||||||
|
show: !isOss() && eventTimelineEnabled,
|
||||||
|
longDescription: (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
Monitor recent events across all your projects in one
|
||||||
|
unified timeline.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
You can access the event timeline from the top menu to
|
||||||
|
get an overview of changes and quickly identify and
|
||||||
|
debug any issues.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const visibleItems = items.filter(
|
const visibleItems = items.filter(
|
||||||
@ -172,7 +213,7 @@ export const NewInUnleash = ({
|
|||||||
({
|
({
|
||||||
label,
|
label,
|
||||||
icon,
|
icon,
|
||||||
link,
|
onCheckItOut,
|
||||||
longDescription,
|
longDescription,
|
||||||
docsLink,
|
docsLink,
|
||||||
preview,
|
preview,
|
||||||
@ -197,7 +238,7 @@ export const NewInUnleash = ({
|
|||||||
}}
|
}}
|
||||||
label={label}
|
label={label}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
link={link}
|
onCheckItOut={onCheckItOut}
|
||||||
preview={preview}
|
preview={preview}
|
||||||
longDescription={longDescription}
|
longDescription={longDescription}
|
||||||
docsLink={docsLink}
|
docsLink={docsLink}
|
||||||
|
@ -36,7 +36,7 @@ interface INewInUnleashItemProps {
|
|||||||
onDismiss: () => void;
|
onDismiss: () => void;
|
||||||
label: string;
|
label: string;
|
||||||
longDescription: ReactNode;
|
longDescription: ReactNode;
|
||||||
link: string;
|
onCheckItOut: () => void;
|
||||||
docsLink: string;
|
docsLink: string;
|
||||||
preview?: ReactNode;
|
preview?: ReactNode;
|
||||||
summary: string;
|
summary: string;
|
||||||
@ -62,7 +62,7 @@ export const NewInUnleashItem = ({
|
|||||||
onDismiss,
|
onDismiss,
|
||||||
label,
|
label,
|
||||||
longDescription,
|
longDescription,
|
||||||
link,
|
onCheckItOut,
|
||||||
docsLink,
|
docsLink,
|
||||||
preview,
|
preview,
|
||||||
summary,
|
summary,
|
||||||
@ -87,7 +87,7 @@ export const NewInUnleashItem = ({
|
|||||||
onClose={handleTooltipClose}
|
onClose={handleTooltipClose}
|
||||||
title={label}
|
title={label}
|
||||||
longDescription={longDescription}
|
longDescription={longDescription}
|
||||||
link={link}
|
onCheckItOut={onCheckItOut}
|
||||||
docsLink={docsLink}
|
docsLink={docsLink}
|
||||||
preview={preview}
|
preview={preview}
|
||||||
>
|
>
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
ClickAwayListener,
|
ClickAwayListener,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { type Link as RouterLink, useNavigate } from 'react-router-dom';
|
import type { Link as RouterLink } from 'react-router-dom';
|
||||||
import OpenInNew from '@mui/icons-material/OpenInNew';
|
import OpenInNew from '@mui/icons-material/OpenInNew';
|
||||||
import { ReactComponent as UnleashLogo } from 'assets/img/logoWithWhiteText.svg';
|
import { ReactComponent as UnleashLogo } from 'assets/img/logoWithWhiteText.svg';
|
||||||
|
|
||||||
@ -22,6 +22,7 @@ const Header = styled(Box)(({ theme }) => ({
|
|||||||
|
|
||||||
const Body = styled(Box)(({ theme }) => ({
|
const Body = styled(Box)(({ theme }) => ({
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
|
lineHeight: 1.5,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledLink = styled(Link<typeof RouterLink | 'a'>)(({ theme }) => ({
|
const StyledLink = styled(Link<typeof RouterLink | 'a'>)(({ theme }) => ({
|
||||||
@ -57,17 +58,22 @@ const CenteredPreview = styled(Box)(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const LongDescription = styled(Box)(({ theme }) => ({
|
const LongDescription = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(1.5),
|
||||||
ul: {
|
ul: {
|
||||||
|
margin: 0,
|
||||||
paddingLeft: theme.spacing(2),
|
paddingLeft: theme.spacing(2),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const Title = styled(Typography)(({ theme }) => ({
|
const Title = styled(Typography)(({ theme }) => ({
|
||||||
padding: theme.spacing(1, 0, 2, 0),
|
padding: theme.spacing(1, 0, 2, 0),
|
||||||
|
lineHeight: 1.5,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const ReadMore = styled(Box)(({ theme }) => ({
|
const ReadMore = styled(Box)(({ theme }) => ({
|
||||||
padding: theme.spacing(2, 0, 4, 0),
|
padding: theme.spacing(3, 0),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const NewInUnleashTooltip: FC<{
|
export const NewInUnleashTooltip: FC<{
|
||||||
@ -75,7 +81,7 @@ export const NewInUnleashTooltip: FC<{
|
|||||||
title: string;
|
title: string;
|
||||||
longDescription: ReactNode;
|
longDescription: ReactNode;
|
||||||
docsLink: string;
|
docsLink: string;
|
||||||
link: string;
|
onCheckItOut: () => void;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
preview?: ReactNode;
|
preview?: ReactNode;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@ -83,72 +89,68 @@ export const NewInUnleashTooltip: FC<{
|
|||||||
children,
|
children,
|
||||||
title,
|
title,
|
||||||
longDescription,
|
longDescription,
|
||||||
link,
|
onCheckItOut,
|
||||||
docsLink,
|
docsLink,
|
||||||
preview,
|
preview,
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}) => (
|
||||||
const navigate = useNavigate();
|
<HtmlTooltip
|
||||||
|
disableFocusListener
|
||||||
return (
|
disableHoverListener
|
||||||
<HtmlTooltip
|
disableTouchListener
|
||||||
disableFocusListener
|
onClose={onClose}
|
||||||
disableHoverListener
|
open={open}
|
||||||
disableTouchListener
|
maxHeight={800}
|
||||||
onClose={onClose}
|
maxWidth={350}
|
||||||
open={open}
|
arrow
|
||||||
maxHeight={800}
|
tabIndex={0}
|
||||||
maxWidth={350}
|
placement='right-end'
|
||||||
arrow
|
title={
|
||||||
tabIndex={0}
|
<ClickAwayListener onClickAway={onClose}>
|
||||||
placement='right-end'
|
<Box>
|
||||||
title={
|
<Header>
|
||||||
<ClickAwayListener onClickAway={onClose}>
|
{preview ? (
|
||||||
<Box>
|
<BottomPreview>{preview}</BottomPreview>
|
||||||
<Header>
|
) : (
|
||||||
{preview ? (
|
<CenteredPreview>
|
||||||
<BottomPreview>{preview}</BottomPreview>
|
<UnleashLogo />
|
||||||
) : (
|
</CenteredPreview>
|
||||||
<CenteredPreview>
|
)}
|
||||||
<UnleashLogo />
|
</Header>
|
||||||
</CenteredPreview>
|
<Body>
|
||||||
)}
|
<Title>{title}</Title>
|
||||||
</Header>
|
<LongDescription>{longDescription}</LongDescription>
|
||||||
<Body>
|
<ReadMore>
|
||||||
<Title>{title}</Title>
|
<StyledLink
|
||||||
<LongDescription>{longDescription}</LongDescription>
|
component='a'
|
||||||
<ReadMore>
|
href={docsLink}
|
||||||
<StyledLink
|
underline='hover'
|
||||||
component='a'
|
rel='noopener noreferrer'
|
||||||
href={docsLink}
|
target='_blank'
|
||||||
underline='hover'
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
<StyledOpenInNew /> Read more in our
|
|
||||||
documentation
|
|
||||||
</StyledLink>
|
|
||||||
</ReadMore>
|
|
||||||
<Button
|
|
||||||
variant='contained'
|
|
||||||
color='primary'
|
|
||||||
type='submit'
|
|
||||||
size='small'
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
onClose();
|
|
||||||
navigate(link);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Check it out
|
<StyledOpenInNew /> Read more in our
|
||||||
</Button>
|
documentation
|
||||||
</Body>
|
</StyledLink>
|
||||||
</Box>
|
</ReadMore>
|
||||||
</ClickAwayListener>
|
<Button
|
||||||
}
|
variant='contained'
|
||||||
>
|
color='primary'
|
||||||
{children}
|
type='submit'
|
||||||
</HtmlTooltip>
|
size='small'
|
||||||
);
|
onClick={(event) => {
|
||||||
};
|
event.stopPropagation();
|
||||||
|
onClose();
|
||||||
|
onCheckItOut();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Check it out
|
||||||
|
</Button>
|
||||||
|
</Body>
|
||||||
|
</Box>
|
||||||
|
</ClickAwayListener>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</HtmlTooltip>
|
||||||
|
);
|
||||||
|
@ -33,9 +33,7 @@ import { useAdminRoutes } from 'component/admin/useAdminRoutes';
|
|||||||
import InviteLinkButton from './InviteLink/InviteLinkButton/InviteLinkButton';
|
import InviteLinkButton from './InviteLink/InviteLinkButton/InviteLinkButton';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
import { CommandBar } from 'component/commandBar/CommandBar';
|
import { CommandBar } from 'component/commandBar/CommandBar';
|
||||||
import LinearScaleIcon from '@mui/icons-material/LinearScale';
|
import { HeaderEventTimelineButton } from './HeaderEventTimelineButton';
|
||||||
import { useEventTimelineContext } from 'component/events/EventTimeline/EventTimelineContext';
|
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
|
||||||
|
|
||||||
const HeaderComponent = styled(AppBar)(({ theme }) => ({
|
const HeaderComponent = styled(AppBar)(({ theme }) => ({
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
@ -109,10 +107,6 @@ const Header = () => {
|
|||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
const [openDrawer, setOpenDrawer] = useState(false);
|
||||||
const toggleDrawer = () => setOpenDrawer((prev) => !prev);
|
const toggleDrawer = () => setOpenDrawer((prev) => !prev);
|
||||||
const celebatoryUnleash = useUiFlag('celebrateUnleash');
|
const celebatoryUnleash = useUiFlag('celebrateUnleash');
|
||||||
const eventTimeline = useUiFlag('eventTimeline') && !isOss();
|
|
||||||
const { open: showTimeline, setOpen: setShowTimeline } =
|
|
||||||
useEventTimelineContext();
|
|
||||||
const { trackEvent } = usePlausibleTracker();
|
|
||||||
|
|
||||||
const routes = getRoutes();
|
const routes = getRoutes();
|
||||||
const adminRoutes = useAdminRoutes();
|
const adminRoutes = useAdminRoutes();
|
||||||
@ -187,35 +181,7 @@ const Header = () => {
|
|||||||
<StyledNav>
|
<StyledNav>
|
||||||
<StyledUserContainer>
|
<StyledUserContainer>
|
||||||
<CommandBar />
|
<CommandBar />
|
||||||
<ConditionallyRender
|
<HeaderEventTimelineButton />
|
||||||
condition={eventTimeline}
|
|
||||||
show={
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
showTimeline
|
|
||||||
? 'Hide timeline'
|
|
||||||
: 'Show timeline'
|
|
||||||
}
|
|
||||||
arrow
|
|
||||||
>
|
|
||||||
<StyledIconButton
|
|
||||||
onClick={() => {
|
|
||||||
trackEvent('event-timeline', {
|
|
||||||
props: {
|
|
||||||
eventType: showTimeline
|
|
||||||
? 'close'
|
|
||||||
: 'open',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setShowTimeline(!showTimeline);
|
|
||||||
}}
|
|
||||||
size='large'
|
|
||||||
>
|
|
||||||
<LinearScaleIcon />
|
|
||||||
</StyledIconButton>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<InviteLinkButton />
|
<InviteLinkButton />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
|
@ -0,0 +1,64 @@
|
|||||||
|
import { alpha, IconButton, styled, Tooltip } from '@mui/material';
|
||||||
|
import LinearScaleIcon from '@mui/icons-material/LinearScale';
|
||||||
|
import { useEventTimelineContext } from 'component/events/EventTimeline/EventTimelineContext';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
|
const StyledHeaderEventTimelineButton = styled(IconButton, {
|
||||||
|
shouldForwardProp: (prop) => prop !== 'highlighted',
|
||||||
|
})<{
|
||||||
|
component?: 'a' | 'button';
|
||||||
|
href?: string;
|
||||||
|
target?: string;
|
||||||
|
highlighted?: boolean;
|
||||||
|
}>(({ theme, highlighted }) => ({
|
||||||
|
animation: highlighted ? 'pulse 1.5s infinite linear' : 'none',
|
||||||
|
zIndex: highlighted ? theme.zIndex.tooltip : 'auto',
|
||||||
|
'@keyframes pulse': {
|
||||||
|
'0%': {
|
||||||
|
boxShadow: `0 0 0 0px ${alpha(theme.palette.primary.main, 0.5)}`,
|
||||||
|
transform: 'scale(1)',
|
||||||
|
},
|
||||||
|
'50%': {
|
||||||
|
boxShadow: `0 0 0 15px ${alpha(theme.palette.primary.main, 0.2)}`,
|
||||||
|
transform: 'scale(1.1)',
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
boxShadow: `0 0 0 30px ${alpha(theme.palette.primary.main, 0)}`,
|
||||||
|
transform: 'scale(1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const HeaderEventTimelineButton = () => {
|
||||||
|
const { trackEvent } = usePlausibleTracker();
|
||||||
|
const { isOss } = useUiConfig();
|
||||||
|
const eventTimeline = useUiFlag('eventTimeline') && !isOss();
|
||||||
|
const {
|
||||||
|
open: showTimeline,
|
||||||
|
setOpen: setShowTimeline,
|
||||||
|
highlighted,
|
||||||
|
} = useEventTimelineContext();
|
||||||
|
|
||||||
|
if (!eventTimeline) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={showTimeline ? 'Hide timeline' : 'Show timeline'} arrow>
|
||||||
|
<StyledHeaderEventTimelineButton
|
||||||
|
highlighted={highlighted}
|
||||||
|
onClick={() => {
|
||||||
|
trackEvent('event-timeline', {
|
||||||
|
props: {
|
||||||
|
eventType: showTimeline ? 'close' : 'open',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setShowTimeline(!showTimeline);
|
||||||
|
}}
|
||||||
|
size='large'
|
||||||
|
>
|
||||||
|
<LinearScaleIcon />
|
||||||
|
</StyledHeaderEventTimelineButton>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
@ -36,8 +36,7 @@ import { Notifications } from 'component/common/Notifications/Notifications';
|
|||||||
import { useAdminRoutes } from 'component/admin/useAdminRoutes';
|
import { useAdminRoutes } from 'component/admin/useAdminRoutes';
|
||||||
import InviteLinkButton from './InviteLink/InviteLinkButton/InviteLinkButton';
|
import InviteLinkButton from './InviteLink/InviteLinkButton/InviteLinkButton';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
import LinearScaleIcon from '@mui/icons-material/LinearScale';
|
import { HeaderEventTimelineButton } from './HeaderEventTimelineButton';
|
||||||
import { useEventTimelineContext } from 'component/events/EventTimeline/EventTimelineContext';
|
|
||||||
|
|
||||||
const HeaderComponent = styled(AppBar)(({ theme }) => ({
|
const HeaderComponent = styled(AppBar)(({ theme }) => ({
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
@ -148,9 +147,6 @@ const OldHeader = () => {
|
|||||||
const onAdminClose = () => setAdminRef(null);
|
const onAdminClose = () => setAdminRef(null);
|
||||||
const onConfigureClose = () => setConfigRef(null);
|
const onConfigureClose = () => setConfigRef(null);
|
||||||
const celebatoryUnleash = useUiFlag('celebrateUnleash');
|
const celebatoryUnleash = useUiFlag('celebrateUnleash');
|
||||||
const eventTimeline = useUiFlag('eventTimeline') && !isOss();
|
|
||||||
const { open: showTimeline, setOpen: setShowTimeline } =
|
|
||||||
useEventTimelineContext();
|
|
||||||
|
|
||||||
const routes = getRoutes();
|
const routes = getRoutes();
|
||||||
const adminRoutes = useAdminRoutes();
|
const adminRoutes = useAdminRoutes();
|
||||||
@ -250,28 +246,7 @@ const OldHeader = () => {
|
|||||||
/>
|
/>
|
||||||
</StyledLinks>
|
</StyledLinks>
|
||||||
<StyledUserContainer>
|
<StyledUserContainer>
|
||||||
<ConditionallyRender
|
<HeaderEventTimelineButton />
|
||||||
condition={eventTimeline}
|
|
||||||
show={
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
showTimeline
|
|
||||||
? 'Hide timeline'
|
|
||||||
: 'Show timeline'
|
|
||||||
}
|
|
||||||
arrow
|
|
||||||
>
|
|
||||||
<StyledIconButton
|
|
||||||
onClick={() =>
|
|
||||||
setShowTimeline(!showTimeline)
|
|
||||||
}
|
|
||||||
size='large'
|
|
||||||
>
|
|
||||||
<LinearScaleIcon />
|
|
||||||
</StyledIconButton>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<InviteLinkButton />
|
<InviteLinkButton />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
|
Loading…
Reference in New Issue
Block a user