Merge pull request #435 from openreplay/reporting
Dashboard - Report Generation
|
|
@ -100,7 +100,7 @@ class CustomFields extends React.Component {
|
|||
title="No data available."
|
||||
size="small"
|
||||
show={ fields.size === 0 }
|
||||
icon
|
||||
animatedIcon="empty-state"
|
||||
>
|
||||
<div className={ styles.list }>
|
||||
{ fields.filter(i => i.index).map(field => (
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ function CustomMetricOverviewChart(props: Props) {
|
|||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ {
|
||||
top: 50, right: 0, left: 0, bottom: 5,
|
||||
top: 50, right: 0, left: 0, bottom: 1,
|
||||
} }
|
||||
>
|
||||
{gradientDef}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ function ResponseTime(props: Props) {
|
|||
/> */}
|
||||
<AvgLabel className="ml-auto" text="Avg" count={Math.round(metric.data.avg)} unit="ms" />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 200 } width="100%">
|
||||
<ResponsiveContainer height={ 207 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
|
|
|
|||
|
|
@ -47,6 +47,19 @@ function DashboardEditModal(props: Props) {
|
|||
value={ dashboard.name }
|
||||
onChange={write}
|
||||
placeholder="Title"
|
||||
maxLength={100}
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field>
|
||||
<label>{'Description:'}</label>
|
||||
<input
|
||||
className=""
|
||||
name="description"
|
||||
value={ dashboard.description }
|
||||
onChange={write}
|
||||
placeholder="Description"
|
||||
maxLength={300}
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ function DashboardForm(props: Props) {
|
|||
<div className="mb-8 grid grid-cols-2 gap-8">
|
||||
<div className="form-field flex flex-col">
|
||||
<label htmlFor="name" className="font-medium mb-2">Title</label>
|
||||
<Input type="text" name="name" onChange={write} value={dashboard.name} />
|
||||
<Input type="text" name="name" onChange={write} value={dashboard.name} maxLength={100} />
|
||||
</div>
|
||||
|
||||
<div className="form-field flex flex-col">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
import { ItemMenu } from 'UI';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
interface Props {
|
||||
editHandler: any
|
||||
deleteHandler: any
|
||||
renderReport: any
|
||||
isEnterprise: boolean
|
||||
}
|
||||
function DashboardOptions(props: Props) {
|
||||
const { editHandler, deleteHandler, renderReport, isEnterprise } = props;
|
||||
const menuItems = [
|
||||
{ icon: 'pencil', text: 'Rename', onClick: editHandler },
|
||||
{ icon: 'text-paragraph', text: 'Add Description', onClick: editHandler },
|
||||
{ icon: 'users', text: 'Visibility & Access', onClick: editHandler },
|
||||
{ icon: 'trash', text: 'Delete', onClick: deleteHandler },
|
||||
]
|
||||
if (isEnterprise) {
|
||||
menuItems.unshift({ icon: 'pdf-download', text: 'Download Report', onClick: renderReport });
|
||||
}
|
||||
|
||||
return (
|
||||
<ItemMenu
|
||||
label="Options"
|
||||
items={menuItems}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
isEnterprise: state.getIn([ 'user', 'client', 'edition' ]) === 'ee',
|
||||
}))(DashboardOptions);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DashboardOptions';
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { observer, useObserver } from 'mobx-react-lite';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Button, PageTitle, Link, Loader, NoContent, ItemMenu } from 'UI';
|
||||
import { withSiteId, dashboardMetricCreate, dashboardSelected, dashboard } from 'App/routes';
|
||||
import { Button, PageTitle, Loader, NoContent } from 'UI';
|
||||
import { withSiteId } from 'App/routes';
|
||||
import withModal from 'App/components/Modal/withModal';
|
||||
import DashboardWidgetGrid from '../DashboardWidgetGrid';
|
||||
import { confirm } from 'UI/Confirmation';
|
||||
|
|
@ -13,12 +13,15 @@ import DashboardEditModal from '../DashboardEditModal';
|
|||
import DateRange from 'Shared/DateRange';
|
||||
import AlertFormModal from 'App/components/Alerts/AlertFormModal';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import withReport from 'App/components/hocs/withReport';
|
||||
import DashboardOptions from '../DashboardOptions';
|
||||
|
||||
interface Props {
|
||||
siteId: number;
|
||||
history: any
|
||||
match: any
|
||||
dashboardId: any
|
||||
renderReport?: any
|
||||
}
|
||||
function DashboardView(props: Props) {
|
||||
const { siteId, dashboardId } = props;
|
||||
|
|
@ -84,8 +87,15 @@ function DashboardView(props: Props) {
|
|||
/>
|
||||
<div className="flex items-center mb-4 justify-between">
|
||||
<div className="flex items-center">
|
||||
<PageTitle title={dashboard?.name} className="mr-3" />
|
||||
<Button primary size="small" onClick={onAddWidgets}>Add Metric</Button>
|
||||
<PageTitle
|
||||
title={dashboard?.name}
|
||||
className="mr-3"
|
||||
subTitle={dashboard?.description}
|
||||
actionButton={
|
||||
<Button primary size="small" onClick={onAddWidgets}>Add Metric</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center">
|
||||
|
|
@ -101,12 +111,10 @@ function DashboardView(props: Props) {
|
|||
</div>
|
||||
<div className="mx-4" />
|
||||
<div className="flex items-center">
|
||||
<ItemMenu
|
||||
label="Options"
|
||||
items={[
|
||||
{ text: 'Rename', onClick: onEdit },
|
||||
{ text: 'Delete', onClick: onDelete },
|
||||
]}
|
||||
<DashboardOptions
|
||||
editHandler={onEdit}
|
||||
deleteHandler={onDelete}
|
||||
renderReport={props.renderReport}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -115,6 +123,7 @@ function DashboardView(props: Props) {
|
|||
siteId={siteId}
|
||||
dashboardId={dashboardId}
|
||||
onEditHandler={onAddWidgets}
|
||||
id="report"
|
||||
/>
|
||||
<AlertFormModal
|
||||
showModal={showAlertModal}
|
||||
|
|
@ -126,4 +135,6 @@ function DashboardView(props: Props) {
|
|||
));
|
||||
}
|
||||
|
||||
export default withPageTitle('Dashboards - OpenReplay')(withRouter(withModal(DashboardView)));
|
||||
export default withPageTitle('Dashboards - OpenReplay')(
|
||||
withReport(withRouter(withModal(DashboardView)))
|
||||
);
|
||||
|
|
@ -8,13 +8,14 @@ interface Props {
|
|||
siteId: string,
|
||||
dashboardId: string;
|
||||
onEditHandler: () => void;
|
||||
id?: string;
|
||||
}
|
||||
function DashboardWidgetGrid(props) {
|
||||
const { dashboardId, siteId } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const loading = useObserver(() => dashboardStore.isLoading);
|
||||
const dashbaord: any = dashboardStore.selectedDashboard;
|
||||
const list: any = useObserver(() => dashbaord?.widgets);
|
||||
const dashboard: any = dashboardStore.selectedDashboard;
|
||||
const list: any = useObserver(() => dashboard?.widgets);
|
||||
|
||||
return useObserver(() => (
|
||||
<Loader loading={loading}>
|
||||
|
|
@ -29,13 +30,13 @@ function DashboardWidgetGrid(props) {
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<div className="grid gap-4 grid-cols-4 items-start pb-10">
|
||||
<div className="grid gap-4 grid-cols-4 items-start pb-10" id={props.id}>
|
||||
{list && list.map((item, index) => (
|
||||
<WidgetWrapper
|
||||
index={index}
|
||||
widget={item}
|
||||
key={item.widgetId}
|
||||
moveListItem={(dragIndex, hoverIndex) => dashbaord.swapWidgetPosition(dragIndex, hoverIndex)}
|
||||
moveListItem={(dragIndex, hoverIndex) => dashboard.swapWidgetPosition(dragIndex, hoverIndex)}
|
||||
dashboardId={dashboardId}
|
||||
siteId={siteId}
|
||||
isWidget={true}
|
||||
|
|
|
|||
|
|
@ -86,14 +86,15 @@ function WidgetWrapper(props: Props) {
|
|||
}}
|
||||
ref={dragDropRef}
|
||||
onClick={props.onClick ? props.onClick : () => {}}
|
||||
id={`widget-${widget.widgetId}`}
|
||||
>
|
||||
{isTemplate && <TemplateOverlay />}
|
||||
<div
|
||||
className={cn("p-3 flex items-center justify-between", { "cursor-move" : !isTemplate })}
|
||||
>
|
||||
<h3 className="capitalize">{widget.name}</h3>
|
||||
<div className="capitalize w-full font-medium">{widget.name}</div>
|
||||
{isWidget && (
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center" id="no-print">
|
||||
{!isPredefined && (
|
||||
<>
|
||||
<AlertButton seriesId={widget.series[0] && widget.series[0].seriesId} />
|
||||
|
|
@ -118,11 +119,11 @@ function WidgetWrapper(props: Props) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<LazyLoad height={!isTemplate ? 300 : 10} offset={!isTemplate ? 100 : 10} >
|
||||
{/* <LazyLoad height={!isTemplate ? 300 : 10} offset={!isTemplate ? 100 : 10} > */}
|
||||
<div className="px-4" onClick={onChartClick}>
|
||||
<WidgetChart metric={widget} isWidget={isWidget} />
|
||||
</div>
|
||||
</LazyLoad>
|
||||
{/* </LazyLoad> */}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
|
|
|||
162
frontend/app/components/hocs/withReport.tsx
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { convertElementToImage } from 'App/utils';
|
||||
import { jsPDF } from "jspdf";
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer, useObserver } from 'mobx-react-lite';
|
||||
import { connect } from 'react-redux';
|
||||
import { fileNameFormat } from 'App/utils';
|
||||
import { toast } from 'react-toastify';
|
||||
interface Props {
|
||||
site: any
|
||||
}
|
||||
export default function withReport<P extends Props>(
|
||||
WrappedComponent: React.ComponentType<P>,
|
||||
) {
|
||||
const ComponentWithReport = (props: P) => {
|
||||
const [rendering, setRendering] = React.useState(false);
|
||||
const { site } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const dashboard: any = useObserver(() => dashboardStore.selectedDashboard);
|
||||
const widgets: any = useObserver(() => dashboard?.widgets);
|
||||
const period = useObserver(() => dashboardStore.period);
|
||||
|
||||
const addFooters = (doc) => {
|
||||
const pageCount = doc.internal.getNumberOfPages();
|
||||
for(var i = 1; i <= pageCount; i++) {
|
||||
doc.setPage(i);
|
||||
doc.setFontSize(8);
|
||||
doc.setTextColor(136,136,136);
|
||||
doc.text('Page ' + String(i) + ' of ' + String(pageCount), 200,290,null,null,"right");
|
||||
doc.addImage('/logo-open-replay-grey.png', 'png', 10, 288, 20, 0);
|
||||
}
|
||||
}
|
||||
|
||||
const renderPromise = async (): Promise<any> => {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
renderReport(resolve);
|
||||
});
|
||||
toast.promise(promise, {
|
||||
pending: 'Generating report...',
|
||||
success: 'Report generated successfully',
|
||||
})
|
||||
}
|
||||
|
||||
const renderReport = async (cb) => {
|
||||
document.body.scrollIntoView();
|
||||
const doc = new jsPDF('p', 'mm', 'a4');
|
||||
const now = new Date().toISOString();
|
||||
|
||||
doc.addMetadata('Author', 'OpenReplay');
|
||||
doc.addMetadata('Title', 'OpenReplay Report');
|
||||
doc.addMetadata('Subject', 'OpenReplay Report');
|
||||
doc.addMetadata('Keywords', 'OpenReplay Report');
|
||||
doc.addMetadata('Creator', 'OpenReplay');
|
||||
doc.addMetadata('Producer', 'OpenReplay');
|
||||
doc.addMetadata('CreationDate', now);
|
||||
|
||||
|
||||
const parentElement = document.getElementById('report') as HTMLElement;
|
||||
const pageHeight = 1200;
|
||||
const pagesCount = parentElement.offsetHeight / pageHeight;
|
||||
const pages: Array<any> = [];
|
||||
for(let i = 0; i < pagesCount; i++) {
|
||||
const page = document.createElement('div');
|
||||
page.classList.add('page');
|
||||
page.style.height = `${pageHeight}px`;
|
||||
page.style.whiteSpace = 'no-wrap !important';
|
||||
|
||||
const childrens = Array.from(parentElement.children).filter((child) => {
|
||||
const rect = child.getBoundingClientRect();
|
||||
const parentRect = parentElement.getBoundingClientRect();
|
||||
const top = rect.top - parentRect.top;
|
||||
return top >= i * pageHeight && top < (i + 1) * pageHeight;
|
||||
});
|
||||
if (childrens.length > 0) {
|
||||
pages.push(childrens);
|
||||
}
|
||||
}
|
||||
|
||||
const rportLayer = document.getElementById("report-layer");
|
||||
|
||||
pages.forEach(async (page, index) => {
|
||||
const pageDiv = document.createElement('div');
|
||||
pageDiv.classList.add('grid', 'gap-4', 'grid-cols-4', 'items-start', 'pb-10', 'auto-rows-min', 'printable-report');
|
||||
pageDiv.id = `page-${index}`;
|
||||
pageDiv.style.backgroundColor = '#f6f6f6';
|
||||
pageDiv.style.gridAutoRows = 'min-content';
|
||||
pageDiv.style.padding = '50px';
|
||||
pageDiv.style.height = '490mm';
|
||||
|
||||
if (index > 0) {
|
||||
pageDiv.style.paddingTop = '100px';
|
||||
}
|
||||
|
||||
if (index === 0) {
|
||||
const header = document.getElementById('report-header')?.cloneNode(true) as HTMLElement;
|
||||
header.classList.add('col-span-4');
|
||||
header.style.display = 'block';
|
||||
pageDiv.appendChild(header);
|
||||
}
|
||||
page.forEach((child) => {
|
||||
pageDiv.appendChild(child.cloneNode(true));
|
||||
})
|
||||
rportLayer?.appendChild(pageDiv);
|
||||
})
|
||||
|
||||
setTimeout(async () => {
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const pageDiv = document.getElementById(`page-${i}`) as HTMLElement;
|
||||
const pageImage = await convertElementToImage(pageDiv);
|
||||
doc.addImage(pageImage, 'PNG', 0, 0, 210, 0);
|
||||
if (i === pages.length - 1) {
|
||||
addFooters(doc);
|
||||
doc.save(fileNameFormat(dashboard.name + '_Report_' + Date.now(), '.pdf'));
|
||||
rportLayer!.innerHTML = '';
|
||||
cb();
|
||||
} else {
|
||||
doc.addPage();
|
||||
}
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2" id="report-header" style={{ display: 'none' }}>
|
||||
<div className="flex items-end justify-between" style={{ margin: '-50px', padding: '25px 50px', backgroundColor: 'white' }}>
|
||||
<div className="flex items-center">
|
||||
<img src="/logo.svg" style={{ height: '30px' }} />
|
||||
<div className="text-lg color-gray-medium ml-2 mt-1">REPORT</div>
|
||||
</div>
|
||||
<div style={{ whiteSpace: 'nowrap' }}>
|
||||
<span className="font-semibold">Project:</span> {site && site.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end mt-20 justify-between">
|
||||
<div className="text-2xl font-semibold">{dashboard && dashboard.name}</div>
|
||||
<div className="font-semibold">
|
||||
{period && (period.range.start.format('MMM Do YY') + ' - ' + period.range.end.format('MMM Do YY'))}
|
||||
</div>
|
||||
</div>
|
||||
{dashboard && dashboard.description && <div className="color-gray-medum whitespace-pre-wrap my-2">{dashboard.description}</div>}
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="report-layer"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
left: '0',
|
||||
zIndex: '-1',
|
||||
opacity: '0',
|
||||
}}
|
||||
></div>
|
||||
<WrappedComponent {...props} renderReport={renderPromise} rendering={rendering} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return connect(state => ({
|
||||
site: state.getIn(['site', 'instance']),
|
||||
}))(ComponentWithReport);
|
||||
}
|
||||
|
|
@ -22,17 +22,21 @@ export default class ItemMenu extends React.PureComponent {
|
|||
render() {
|
||||
const { items, label = "" } = this.props;
|
||||
const { displayed } = this.state;
|
||||
const parentStyles = label ? 'rounded px-2 py-1 hover:bg-gray-light' : '';
|
||||
|
||||
return (
|
||||
<div className={ styles.wrapper }>
|
||||
<OutsideClickDetectingDiv
|
||||
onClickOutside={ this.closeMenu }
|
||||
>
|
||||
<div onClick={ this.toggleMenu } className="flex items-center cursor-pointer">
|
||||
<div
|
||||
onClick={ this.toggleMenu }
|
||||
className={cn("flex items-center cursor-pointer select-none", parentStyles, { 'bg-gray-light' : displayed && label })}
|
||||
>
|
||||
{label && <span className="mr-1 color-gray-medium ">{label}</span>}
|
||||
<div
|
||||
ref={ (ref) => { this.menuBtnRef = ref; } }
|
||||
className={cn("w-10 h-10 rounded-full flex items-center justify-center hover:bg-gray-light", { 'bg-gray-light' : displayed })}
|
||||
className={cn("rounded-full flex items-center justify-center hover:bg-gray-light", { 'bg-gray-light' : displayed, "w-10 h-10" : !label })}
|
||||
role="button"
|
||||
>
|
||||
<Icon name="ellipsis-v" size="16" />
|
||||
|
|
|
|||
|
|
@ -28,11 +28,12 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
white-space: nowrap;
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
right: 31px;
|
||||
top: 18px;
|
||||
width: 150px;
|
||||
top: 27px;
|
||||
min-width: 150px;
|
||||
background-color: $white;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0px 1px 3px 0 $gray-light;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export default ({ transition = Flip, position = 'bottom-right', autoClose = 3000
|
|||
autoClose={ autoClose }
|
||||
className={ styles.container }
|
||||
toastClassName={ styles.toast }
|
||||
closeButton={false}
|
||||
{ ...props }
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
|
||||
function PageTitle({ title, className = '' }) {
|
||||
function PageTitle({ title, actionButton = null, subTitle = '', className = '', subTitleClass }) {
|
||||
return (
|
||||
<h1 className={cn("text-2xl", className)}>
|
||||
{title}
|
||||
</h1>
|
||||
<div>
|
||||
<div className='flex items-center'>
|
||||
<h1 className={cn("text-2xl", className)}>
|
||||
{title}
|
||||
</h1>
|
||||
{ actionButton && actionButton}
|
||||
</div>
|
||||
{subTitle && <h2 className={cn("my-1 font-normal color-gray-dark", subTitleClass)}>{subTitle}</h2>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export const getDateFromMill = date =>
|
|||
* @param {Date} Date to be checked.
|
||||
* @return {Boolean}
|
||||
*/
|
||||
export const isToday = (date: Date):boolean => date.hasSame(new Date(), 'day');
|
||||
export const isToday = (date: DateTime):boolean => date.hasSame(new Date(), 'day');
|
||||
|
||||
|
||||
export function formatDateTimeDefault(timestamp: number): string {
|
||||
|
|
@ -113,4 +113,8 @@ export const formatMs = (ms: number): string => ms < 1000 ? `${ Math.trunc(ms) }
|
|||
|
||||
export const convertTimestampToUtcTimestamp = (timestamp: number): number => {
|
||||
return DateTime.fromMillis(timestamp).toUTC().toMillis();
|
||||
}
|
||||
}
|
||||
|
||||
export const nowFormatted = (format?: string): string => {
|
||||
return DateTime.local().toFormat(format || 'LLL dd, yyyy, hh:mm a');
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import { toast } from 'react-toastify';
|
|||
export interface IDashboard {
|
||||
dashboardId: any
|
||||
name: string
|
||||
description: string
|
||||
isPublic: boolean
|
||||
widgets: IWidget[]
|
||||
metrics: any[]
|
||||
|
|
@ -35,6 +36,7 @@ export default class Dashboard implements IDashboard {
|
|||
public static get ID_KEY():string { return "dashboardId" }
|
||||
dashboardId: any = undefined
|
||||
name: string = "New Dashboard"
|
||||
description: string = ""
|
||||
isPublic: boolean = true
|
||||
widgets: IWidget[] = []
|
||||
metrics: any[] = []
|
||||
|
|
@ -46,6 +48,7 @@ export default class Dashboard implements IDashboard {
|
|||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
name: observable,
|
||||
description: observable,
|
||||
isPublic: observable,
|
||||
widgets: observable,
|
||||
isValid: observable,
|
||||
|
|
|
|||
|
|
@ -260,8 +260,6 @@ p {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tippy-tooltip.openreplay-theme {
|
||||
background-color: $tealx;
|
||||
color: white;
|
||||
|
|
@ -273,4 +271,29 @@ p {
|
|||
|
||||
.tippy-tooltip.openreplay-theme .tippy-backdrop {
|
||||
background-color: $tealx;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.no-print {
|
||||
display:none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.printable-report * {
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
.recharts-default-legend {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.recharts-legend-item {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
.recharts-legend-item-text {
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-graph-up-arrow" viewBox="0 0 16 16">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-graph-up-arrow" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M0 0h1v15h15v1H0V0Zm10 3.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-1 0V4.9l-3.613 4.417a.5.5 0 0 1-.74.037L7.06 6.767l-3.656 5.027a.5.5 0 0 1-.808-.588l4-5.5a.5.5 0 0 1 .758-.06l2.609 2.61L13.445 4H10.5a.5.5 0 0 1-.5-.5Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 402 B After Width: | Height: | Size: 359 B |
|
|
@ -1,3 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-hash" viewBox="0 0 16 16">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-hash" viewBox="0 0 16 16">
|
||||
<path d="M8.39 12.648a1.32 1.32 0 0 0-.015.18c0 .305.21.508.5.508.266 0 .492-.172.555-.477l.554-2.703h1.204c.421 0 .617-.234.617-.547 0-.312-.188-.53-.617-.53h-.985l.516-2.524h1.265c.43 0 .618-.227.618-.547 0-.313-.188-.524-.618-.524h-1.046l.476-2.304a1.06 1.06 0 0 0 .016-.164.51.51 0 0 0-.516-.516.54.54 0 0 0-.539.43l-.523 2.554H7.617l.477-2.304c.008-.04.015-.118.015-.164a.512.512 0 0 0-.523-.516.539.539 0 0 0-.531.43L6.53 5.484H5.414c-.43 0-.617.22-.617.532 0 .312.187.539.617.539h.906l-.515 2.523H4.609c-.421 0-.609.219-.609.531 0 .313.188.547.61.547h.976l-.516 2.492c-.008.04-.015.125-.015.18 0 .305.21.508.5.508.265 0 .492-.172.554-.477l.555-2.703h2.242l-.515 2.492zm-1-6.109h2.266l-.515 2.563H6.859l.532-2.563z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 855 B After Width: | Height: | Size: 812 B |
5
frontend/app/svg/icons/pdf-download.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg viewBox="0 0 19 19" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0938 5.24878C10.0938 5.09131 10.0312 4.94028 9.91986 4.82893C9.80851 4.71759 9.65749 4.65503 9.50002 4.65503C9.34255 4.65503 9.19152 4.71759 9.08017 4.82893C8.96882 4.94028 8.90627 5.09131 8.90627 5.24878V9.75297L7.54539 8.3909C7.49019 8.3357 7.42465 8.29191 7.35252 8.26203C7.2804 8.23216 7.20309 8.21678 7.12502 8.21678C7.04695 8.21678 6.96964 8.23216 6.89751 8.26203C6.82538 8.29191 6.75985 8.3357 6.70464 8.3909C6.64944 8.44611 6.60565 8.51165 6.57577 8.58377C6.54589 8.6559 6.53052 8.73321 6.53052 8.81128C6.53052 8.88935 6.54589 8.96666 6.57577 9.03878C6.60565 9.11091 6.64944 9.17645 6.70464 9.23165L9.07964 11.6067C9.1348 11.6619 9.20032 11.7058 9.27245 11.7358C9.34459 11.7657 9.42192 11.7811 9.50002 11.7811C9.57812 11.7811 9.65545 11.7657 9.72758 11.7358C9.79972 11.7058 9.86524 11.6619 9.92039 11.6067L12.2954 9.23165C12.3506 9.17645 12.3944 9.11091 12.4243 9.03878C12.4541 8.96666 12.4695 8.88935 12.4695 8.81128C12.4695 8.73321 12.4541 8.6559 12.4243 8.58377C12.3944 8.51165 12.3506 8.44611 12.2954 8.3909C12.2402 8.3357 12.1747 8.29191 12.1025 8.26203C12.0304 8.23216 11.9531 8.21678 11.875 8.21678C11.7969 8.21678 11.7196 8.23216 11.6475 8.26203C11.5754 8.29191 11.5098 8.3357 11.4546 8.3909L10.0938 9.75297V5.24878Z" />
|
||||
<path d="M16.625 16.625V5.34375L11.2812 0H4.75C4.12011 0 3.51602 0.250223 3.07062 0.695621C2.62522 1.14102 2.375 1.74511 2.375 2.375V16.625C2.375 17.2549 2.62522 17.859 3.07062 18.3044C3.51602 18.7498 4.12011 19 4.75 19H14.25C14.8799 19 15.484 18.7498 15.9294 18.3044C16.3748 17.859 16.625 17.2549 16.625 16.625ZM11.2812 3.5625C11.2812 4.03492 11.4689 4.48799 11.803 4.82203C12.137 5.15608 12.5901 5.34375 13.0625 5.34375H15.4375V16.625C15.4375 16.9399 15.3124 17.242 15.0897 17.4647C14.867 17.6874 14.5649 17.8125 14.25 17.8125H4.75C4.43506 17.8125 4.13301 17.6874 3.91031 17.4647C3.68761 17.242 3.5625 16.9399 3.5625 16.625V2.375C3.5625 2.06006 3.68761 1.75801 3.91031 1.53531C4.13301 1.31261 4.43506 1.1875 4.75 1.1875H11.2812V3.5625Z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.58 13.49H3.42004V17.86H5.20894V14.348H6.61037C6.89179 14.348 7.1338 14.3995 7.33642 14.5024C7.54064 14.6053 7.69743 14.7468 7.80678 14.9269C7.91613 15.1054 7.9708 15.3096 7.9708 15.5396C7.9708 15.7663 7.91613 15.9641 7.80678 16.133C7.69743 16.3018 7.54064 16.4337 7.33642 16.5285C7.1338 16.6218 6.89179 16.6684 6.61037 16.6684H6.05559V17.86H8.42669V14.348H8.77404H9.27334H9.55556C9.79355 14.348 10.0114 14.389 10.2092 14.471C10.4086 14.5514 10.5807 14.6672 10.7254 14.8184C10.8718 14.9679 10.9843 15.1456 11.0631 15.3514C11.1435 15.5557 11.1837 15.7824 11.1837 16.0316V16.1788C11.1837 16.4264 11.1435 16.6532 11.0631 16.859C10.9843 17.0648 10.8726 17.2425 10.7278 17.3921C10.5831 17.5416 10.4119 17.6574 10.2141 17.7394C10.0211 17.8185 9.80952 17.8587 9.57924 17.86H11.6251V14.348H12.2306H12.4718H13.9745V15.0017H12.4718V15.8097H13.8371V16.461H12.4718V17.86H15.58V13.49ZM6.05559 15.0017V16.0148H6.61037C6.72937 16.0148 6.82586 15.9947 6.89983 15.9545C6.9738 15.9143 7.02767 15.8588 7.06144 15.788C7.09682 15.7157 7.11451 15.6345 7.11451 15.5444C7.11451 15.4479 7.09682 15.3587 7.06144 15.2767C7.02767 15.1946 6.9738 15.1287 6.89983 15.0789C6.82586 15.0274 6.72937 15.0017 6.61037 15.0017H6.05559ZM9.56762 17.2088H9.27334V15.0017H9.55556C9.67938 15.0017 9.78873 15.0234 9.88361 15.0668C9.98009 15.1102 10.0605 15.1753 10.1248 15.2622C10.1891 15.3474 10.2374 15.4543 10.2695 15.583C10.3033 15.71 10.3202 15.858 10.3202 16.0268V16.1788C10.3202 16.4039 10.2904 16.5937 10.231 16.748C10.1731 16.9008 10.0878 17.0158 9.97527 17.093C9.86431 17.1702 9.72843 17.2088 9.56762 17.2088Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
|
|
@ -1,3 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-table" viewBox="0 0 16 16">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-table" viewBox="0 0 16 16">
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm15 2h-4v3h4V4zm0 4h-4v3h4V8zm0 4h-4v3h3a1 1 0 0 0 1-1v-2zm-5 3v-3H6v3h4zm-5 0v-3H1v2a1 1 0 0 0 1 1h3zm-4-4h4V8H1v3zm0-4h4V4H1v3zm5-3v3h4V4H6zm4 4H6v3h4V8z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 371 B After Width: | Height: | Size: 328 B |
3
frontend/app/svg/icons/text-paragraph.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-text-paragraph" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2 12.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 417 B |
|
|
@ -1,5 +1,6 @@
|
|||
import JSBI from 'jsbi';
|
||||
import chroma from "chroma-js";
|
||||
import * as htmlToImage from 'html-to-image';
|
||||
|
||||
export function debounce(callback, wait, context = this) {
|
||||
let timeout = null;
|
||||
|
|
@ -26,6 +27,11 @@ export function randomInt(a, b) {
|
|||
return Math.round(rand);
|
||||
}
|
||||
|
||||
export const fileNameFormat = (str = '', ext = '') => {
|
||||
const name = str.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
return `${name}${ext}`;
|
||||
};
|
||||
|
||||
export const toUnderscore = s => s.split(/(?=[A-Z])/).join('_').toLowerCase();
|
||||
|
||||
export const getUniqueFilter = keys =>
|
||||
|
|
@ -246,4 +252,16 @@ export const positionOfTheNumber = (min, max, value, length) => {
|
|||
const interval = (max - min) / length;
|
||||
const position = Math.round((value - min) / interval);
|
||||
return position;
|
||||
}
|
||||
|
||||
export const convertElementToImage = async (el) => {
|
||||
const fontEmbedCss = await htmlToImage.getFontEmbedCSS(el);
|
||||
const image = await htmlToImage.toJpeg(el, {
|
||||
pixelRatio: 2,
|
||||
fontEmbedCss,
|
||||
filter: function (node) {
|
||||
return node.id !== 'no-print';
|
||||
},
|
||||
});
|
||||
return image;
|
||||
}
|
||||
1438
frontend/package-lock.json
generated
|
|
@ -22,9 +22,11 @@
|
|||
"codemirror": "^5.62.3",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"deep-diff": "^1.0.2",
|
||||
"html-to-image": "^1.9.0",
|
||||
"immutable": "^4.0.0-rc.12",
|
||||
"jsbi": "^4.1.0",
|
||||
"jshint": "^2.11.1",
|
||||
"jspdf": "^2.5.1",
|
||||
"luxon": "^1.24.1",
|
||||
"mobx": "^6.3.8",
|
||||
"mobx-react-lite": "^3.1.6",
|
||||
|
|
@ -51,7 +53,7 @@
|
|||
"react-router-dom": "^4.3.1",
|
||||
"react-svg-map": "^2.2.0",
|
||||
"react-tippy": "^1.4.0",
|
||||
"react-toastify": "^5.5.0",
|
||||
"react-toastify": "^8.2.0",
|
||||
"react-virtualized": "^9.22.2",
|
||||
"recharts": "^2.1.9",
|
||||
"redux": "^4.0.5",
|
||||
|
|
|
|||
|
|
@ -98,6 +98,8 @@ Below is the list of dependencies used in OpenReplay software. Licenses may chan
|
|||
| geoip-lite | Apache2 | JavaScript |
|
||||
| ua-parser-js | MIT | JavaScript |
|
||||
| express | MIT | JavaScript |
|
||||
| jspdf | MIT | JavaScript |
|
||||
| html-to-image | MIT | JavaScript |
|
||||
| kafka | Apache2 | Infrastructure |
|
||||
| stern | Apache2 | Infrastructure |
|
||||
| k9s | Apache2 | Infrastructure |
|
||||
|
|
|
|||