rebase dev
|
|
@ -24,11 +24,20 @@ function DashboardList() {
|
|||
show={lenth === 0}
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<Icon name="no-dashboard" size={80} color="figmaColors-accent-secondary" />
|
||||
<div className="text-center text-gray-600 my-4">
|
||||
{dashboardsSearch !== ''
|
||||
? 'No matching results'
|
||||
: "You haven't created any dashboards yet"}
|
||||
<div className="text-center my-4">
|
||||
{dashboardsSearch !== '' ? (
|
||||
'No matching results'
|
||||
) : (
|
||||
<div>
|
||||
<div>Create your first Dashboard</div>
|
||||
<div className="text-sm color-gray-medium font-normal">
|
||||
A dashboard lets you visualize trends and insights of data captured by OpenReplay.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="my-2 bg-active-blue rounded flex items-center justify-center px-80 py-20">
|
||||
<Icon name="grid-1x2" size={40} color="figmaColors-accent-secondary" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,43 +3,57 @@ import { Button, PageTitle, Icon } from 'UI';
|
|||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { withSiteId } from 'App/routes';
|
||||
|
||||
import Select from 'Shared/Select';
|
||||
import DashboardList from './DashboardList';
|
||||
import DashboardSearch from './DashboardSearch';
|
||||
import { sort } from 'App/duck/sessions';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
|
||||
function DashboardsView({ history, siteId }: { history: any, siteId: string }) {
|
||||
const { dashboardStore } = useStore();
|
||||
function DashboardsView({ history, siteId }: { history: any; siteId: string }) {
|
||||
const { dashboardStore } = useStore();
|
||||
const sort = useObserver(() => dashboardStore.sort);
|
||||
|
||||
const onAddDashboardClick = () => {
|
||||
dashboardStore.initDashboard();
|
||||
dashboardStore
|
||||
.save(dashboardStore.dashboardInstance)
|
||||
.then(async (syncedDashboard) => {
|
||||
dashboardStore.selectDashboardById(syncedDashboard.dashboardId);
|
||||
history.push(withSiteId(`/dashboard/${syncedDashboard.dashboardId}`, siteId))
|
||||
})
|
||||
}
|
||||
const onAddDashboardClick = () => {
|
||||
dashboardStore.initDashboard();
|
||||
dashboardStore.save(dashboardStore.dashboardInstance).then(async (syncedDashboard) => {
|
||||
dashboardStore.selectDashboardById(syncedDashboard.dashboardId);
|
||||
history.push(withSiteId(`/dashboard/${syncedDashboard.dashboardId}`, siteId));
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '1300px', margin: 'auto'}} className="bg-white rounded py-4 border">
|
||||
<div className="flex items-center mb-4 justify-between px-6">
|
||||
<div className="flex items-baseline mr-3">
|
||||
<PageTitle title="Dashboards" />
|
||||
</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<Button variant="primary" onClick={onAddDashboardClick}>Create Dashboard</Button>
|
||||
<div className="ml-4 w-1/4" style={{ minWidth: 300 }}>
|
||||
<DashboardSearch />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-base text-disabled-text flex items-center px-6">
|
||||
<Icon name="info-circle-fill" className="mr-2" size={16} />
|
||||
A Dashboard is a collection of Metrics that can be shared across teams.
|
||||
</div>
|
||||
<DashboardList />
|
||||
return (
|
||||
<div style={{ maxWidth: '1300px', margin: 'auto' }} className="bg-white rounded py-4 border">
|
||||
<div className="flex items-center mb-4 justify-between px-6">
|
||||
<div className="flex items-baseline mr-3">
|
||||
<PageTitle title="Dashboards" />
|
||||
</div>
|
||||
);
|
||||
<div className="ml-auto flex items-center">
|
||||
<Button variant="primary" onClick={onAddDashboardClick}>
|
||||
New Dashboard
|
||||
</Button>
|
||||
<div className="mx-2">
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'Newest', value: 'desc' },
|
||||
{ label: 'Oldest', value: 'asc' },
|
||||
]}
|
||||
defaultValue={sort.by}
|
||||
plain
|
||||
onChange={({ value }) => dashboardStore.updateKey('sort', { by: value.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/4" style={{ minWidth: 300 }}>
|
||||
<DashboardSearch />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="text-base text-disabled-text flex items-center px-6">
|
||||
<Icon name="info-circle-fill" className="mr-2" size={16} />A dashboard is a custom
|
||||
visualization using your OpenReplay data.
|
||||
</div> */}
|
||||
<DashboardList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withPageTitle('Dashboards - OpenReplay')(DashboardsView);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import { toJS } from 'mobx';
|
||||
import { useStore } from 'App/mstore';
|
||||
import WidgetWrapper from '../WidgetWrapper';
|
||||
import { NoContent, Loader, Icon } from 'UI';
|
||||
|
|
@ -37,24 +38,20 @@ function DashboardWidgetGrid(props: Props) {
|
|||
<NoContent
|
||||
show={list.length === 0}
|
||||
icon="no-metrics-chart"
|
||||
title={
|
||||
<span className="text-2xl capitalize-first text-figmaColors-text-primary">
|
||||
Build your dashboard
|
||||
</span>
|
||||
}
|
||||
subtext={
|
||||
<div className="w-4/5 m-auto mt-4">
|
||||
<AddMetricContainer siteId={siteId} />
|
||||
</div>
|
||||
}
|
||||
title={<EmptyDashboardGrid />}
|
||||
// subtext={
|
||||
// <div className="w-4/5 m-auto mt-4">
|
||||
// <AddMetricContainer siteId={siteId} />
|
||||
// </div>
|
||||
// }
|
||||
>
|
||||
<div className="grid gap-4 grid-cols-4 items-start pb-10" id={props.id}>
|
||||
{smallWidgets.length > 0 ? (
|
||||
<>
|
||||
<div className="font-semibold text-xl py-4 flex items-center gap-2 col-span-4">
|
||||
<Icon name="grid-horizontal" size={26} />
|
||||
Web Vitals
|
||||
</div>
|
||||
<div className="grid gap-4 grid-cols-4 items-start pb-10" id={props.id}>{smallWidgets.length > 0 ? (
|
||||
<>
|
||||
<div className="font-semibold text-xl py-4 flex items-center gap-2col-span-4">
|
||||
<Icon name="grid-horizontal" size={26} />
|
||||
Web Vitals
|
||||
</div>
|
||||
|
||||
{smallWidgets &&
|
||||
smallWidgets.map((item: any, index: any) => (
|
||||
<React.Fragment key={item.widgetId}>
|
||||
|
|
@ -63,23 +60,24 @@ function DashboardWidgetGrid(props: Props) {
|
|||
widget={item}
|
||||
moveListItem={(dragIndex: any, hoverIndex: any) =>
|
||||
dashboard.swapWidgetPosition(dragIndex, hoverIndex)
|
||||
}
|
||||
dashboardId={dashboardId}
|
||||
|
||||
}dashboardId={dashboardId}
|
||||
siteId={siteId}
|
||||
isWidget={true}
|
||||
grid="vitals"
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{smallWidgets.length > 0 && regularWidgets.length > 0 ? (
|
||||
<div className="font-semibold text-xl py-4 flex items-center gap-2 col-span-4">
|
||||
<Icon name="grid-horizontal" size={26} />
|
||||
All Metrics
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{smallWidgets.length > 0 && regularWidgets.length > 0 ? (
|
||||
<div className="font-semibold text-xl py-4 flex items-center gap-2col-span-4">
|
||||
<Icon name="grid-horizontal" size={26} />
|
||||
All Metrics
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{regularWidgets &&
|
||||
regularWidgets.map((item: any, index: any) => (
|
||||
|
|
@ -97,8 +95,7 @@ function DashboardWidgetGrid(props: Props) {
|
|||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
<div className="col-span-2" id="no-print">
|
||||
<div className="col-span-2"id="no-print">
|
||||
<AddMetricContainer siteId={siteId} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -108,3 +105,122 @@ function DashboardWidgetGrid(props: Props) {
|
|||
}
|
||||
|
||||
export default DashboardWidgetGrid;
|
||||
|
||||
interface MetricType {
|
||||
title: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
}
|
||||
const METRIC_TYPES: MetricType[] = [
|
||||
{
|
||||
title: 'Add From Library',
|
||||
icon: 'grid',
|
||||
description: 'Select a pre existing card from card library',
|
||||
slug: 'library',
|
||||
},
|
||||
{
|
||||
title: 'Timeseries',
|
||||
icon: 'graph-up',
|
||||
description: 'Trend of sessions count in over the time.',
|
||||
slug: 'timeseries',
|
||||
},
|
||||
{
|
||||
title: 'Table',
|
||||
icon: 'list-alt',
|
||||
description: 'See list of Users, Sessions, Errors, Issues, etc.,',
|
||||
slug: 'table'
|
||||
},
|
||||
{
|
||||
title: 'Funnel',
|
||||
icon: 'funnel',
|
||||
description: 'Uncover the issues impacting user journeys.',
|
||||
slug: 'funnel'
|
||||
},
|
||||
{
|
||||
title: 'Errors Tracking',
|
||||
icon: 'exclamation-circle',
|
||||
description: 'Discover user journeys between 2 points.',
|
||||
slug: 'errors'
|
||||
},
|
||||
{
|
||||
title: 'Performance Monitoring',
|
||||
icon: 'speedometer2',
|
||||
description: 'Retention graph of users / features over a period of time.',
|
||||
slug: 'performance'
|
||||
},
|
||||
{
|
||||
title: 'Resource Monitoring',
|
||||
icon: 'files',
|
||||
description: 'Find the adoption of your all features in your app.',
|
||||
slug: 'resource-monitoring'
|
||||
},
|
||||
{
|
||||
title: 'Web Vitals',
|
||||
icon: 'activity',
|
||||
description: 'Find the adoption of your all features in your app.',
|
||||
slug: 'web-vitals'
|
||||
},
|
||||
{
|
||||
title: 'User Path',
|
||||
icon: 'signpost-split',
|
||||
description: 'Discover user journeys between 2 points.',
|
||||
slug: 'user-path'
|
||||
},
|
||||
{
|
||||
title: 'Retention',
|
||||
icon: 'arrow-repeat',
|
||||
description: 'Retension graph of users / features over a period of time.',
|
||||
slug: 'retention'
|
||||
},
|
||||
{
|
||||
title: 'Feature Adoption',
|
||||
icon: 'card-checklist',
|
||||
description: 'Find the adoption of your all features in your app.',
|
||||
slug: 'feature-adoption'
|
||||
},
|
||||
];
|
||||
|
||||
function MetricTypeItem({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
icon: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-start hover:bg-active-blue p-4 cursor-pointer">
|
||||
<div className="pr-4 pt-1">
|
||||
<Icon name={icon} size="20" />
|
||||
</div>
|
||||
<div className="flex flex-col items-start text-left">
|
||||
<div className="text-base color-gray-darkest">{title}</div>
|
||||
<div className="text-xs">{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyDashboardGrid() {
|
||||
return (
|
||||
<div className="bg-white rounded">
|
||||
<div className="border-b p-4">
|
||||
<div className="text-lg font-medium color-gray-darkest">
|
||||
There are no cards in this dashboard
|
||||
</div>
|
||||
<div className="text-sm">Try the most commonly used metrics or graphs to begin.</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 p-8 gap-2">
|
||||
{METRIC_TYPES.map((metric: MetricType) => (
|
||||
<MetricTypeItem
|
||||
icon={metric.icon}
|
||||
title={metric.title}
|
||||
description={metric.description}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export default class DashboardStore {
|
|||
page: number = 1
|
||||
pageSize: number = 10
|
||||
dashboardsSearch: string = ''
|
||||
sort: any = {}
|
||||
sort: any = { by: 'desc'}
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
|
|
|
|||
3
frontend/app/svg/icons/activity.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-activity" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M6 2a.5.5 0 0 1 .47.33L10 12.036l1.53-4.208A.5.5 0 0 1 12 7.5h3.5a.5.5 0 0 1 0 1h-3.15l-1.88 5.17a.5.5 0 0 1-.94 0L6 3.964 4.47 8.171A.5.5 0 0 1 4 8.5H.5a.5.5 0 0 1 0-1h3.15l1.88-5.17A.5.5 0 0 1 6 2Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 344 B |
4
frontend/app/svg/icons/card-checklist.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-card-checklist" viewBox="0 0 16 16">
|
||||
<path d="M14.5 3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h13zm-13-1A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-13z"/>
|
||||
<path d="M7 5.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm-1.496-.854a.5.5 0 0 1 0 .708l-1.5 1.5a.5.5 0 0 1-.708 0l-.5-.5a.5.5 0 1 1 .708-.708l.146.147 1.146-1.147a.5.5 0 0 1 .708 0zM7 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm-1.496-.854a.5.5 0 0 1 0 .708l-1.5 1.5a.5.5 0 0 1-.708 0l-.5-.5a.5.5 0 0 1 .708-.708l.146.147 1.146-1.147a.5.5 0 0 1 .708 0z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 707 B |
3
frontend/app/svg/icons/files.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-files" viewBox="0 0 16 16">
|
||||
<path d="M13 0H6a2 2 0 0 0-2 2 2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2 2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm0 13V4a2 2 0 0 0-2-2H5a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1zM3 4a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 362 B |
3
frontend/app/svg/icons/grid-1x2.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-grid-1x2" viewBox="0 0 16 16">
|
||||
<path d="M6 1H1v14h5V1zm9 0h-5v5h5V1zm0 9v5h-5v-5h5zM0 1a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1zm9 0a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1V1zm1 8a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1h-5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 359 B |
3
frontend/app/svg/icons/grid.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-grid" viewBox="0 0 16 16">
|
||||
<path d="M1 2.5A1.5 1.5 0 0 1 2.5 1h3A1.5 1.5 0 0 1 7 2.5v3A1.5 1.5 0 0 1 5.5 7h-3A1.5 1.5 0 0 1 1 5.5v-3zM2.5 2a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zm6.5.5A1.5 1.5 0 0 1 10.5 1h3A1.5 1.5 0 0 1 15 2.5v3A1.5 1.5 0 0 1 13.5 7h-3A1.5 1.5 0 0 1 9 5.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zM1 10.5A1.5 1.5 0 0 1 2.5 9h3A1.5 1.5 0 0 1 7 10.5v3A1.5 1.5 0 0 1 5.5 15h-3A1.5 1.5 0 0 1 1 13.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zm6.5.5A1.5 1.5 0 0 1 10.5 9h3a1.5 1.5 0 0 1 1.5 1.5v3a1.5 1.5 0 0 1-1.5 1.5h-3A1.5 1.5 0 0 1 9 13.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 857 B |
3
frontend/app/svg/icons/signpost-split.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-signpost-split" viewBox="0 0 16 16">
|
||||
<path d="M7 7V1.414a1 1 0 0 1 2 0V2h5a1 1 0 0 1 .8.4l.975 1.3a.5.5 0 0 1 0 .6L14.8 5.6a1 1 0 0 1-.8.4H9v10H7v-5H2a1 1 0 0 1-.8-.4L.225 9.3a.5.5 0 0 1 0-.6L1.2 7.4A1 1 0 0 1 2 7h5zm1 3V8H2l-.75 1L2 10h6zm0-5h6l.75-1L14 3H8v2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 346 B |
4
frontend/app/svg/icons/speedometer2.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-speedometer2" viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5V6a.5.5 0 0 1-1 0V4.5A.5.5 0 0 1 8 4zM3.732 5.732a.5.5 0 0 1 .707 0l.915.914a.5.5 0 1 1-.708.708l-.914-.915a.5.5 0 0 1 0-.707zM2 10a.5.5 0 0 1 .5-.5h1.586a.5.5 0 0 1 0 1H2.5A.5.5 0 0 1 2 10zm9.5 0a.5.5 0 0 1 .5-.5h1.5a.5.5 0 0 1 0 1H12a.5.5 0 0 1-.5-.5zm.754-4.246a.389.389 0 0 0-.527-.02L7.547 9.31a.91.91 0 1 0 1.302 1.258l3.434-4.297a.389.389 0 0 0-.029-.518z"/>
|
||||
<path fill-rule="evenodd" d="M0 10a8 8 0 1 1 15.547 2.661c-.442 1.253-1.845 1.602-2.932 1.25C11.309 13.488 9.475 13 8 13c-1.474 0-3.31.488-4.615.911-1.087.352-2.49.003-2.932-1.25A7.988 7.988 0 0 1 0 10zm8-7a7 7 0 0 0-6.603 9.329c.203.575.923.876 1.68.63C4.397 12.533 6.358 12 8 12s3.604.532 4.923.96c.757.245 1.477-.056 1.68-.631A7 7 0 0 0 8 3z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 861 B |