Merge pull request #435 from openreplay/reporting

Dashboard - Report Generation
This commit is contained in:
Shekar Siri 2022-04-29 15:36:06 +02:00 committed by GitHub
commit 6a855a947c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1190 additions and 622 deletions

View file

@ -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 => (

View file

@ -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}

View file

@ -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 }

View file

@ -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>

View file

@ -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">

View file

@ -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);

View file

@ -0,0 +1 @@
export { default } from './DashboardOptions';

View file

@ -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)))
);

View file

@ -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}

View file

@ -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>
));
}

View 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);
}

View file

@ -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" />

View file

@ -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;

View file

@ -12,6 +12,7 @@ export default ({ transition = Flip, position = 'bottom-right', autoClose = 3000
autoClose={ autoClose }
className={ styles.container }
toastClassName={ styles.toast }
closeButton={false}
{ ...props }
/>
);

View file

@ -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>
);
}

View file

@ -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');
}

View file

@ -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,

View file

@ -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;
}

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View 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

View file

@ -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;
}

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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 |