-
{widget.name}
+
{widget.name}
{isWidget && (
-
+
{!isPredefined && (
<>
@@ -118,11 +119,11 @@ function WidgetWrapper(props: Props) {
)}
-
+ {/* */}
-
+ {/* */}
));
}
diff --git a/frontend/app/components/hocs/withReport.tsx b/frontend/app/components/hocs/withReport.tsx
new file mode 100644
index 000000000..be95952be
--- /dev/null
+++ b/frontend/app/components/hocs/withReport.tsx
@@ -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
(
+ WrappedComponent: React.ComponentType
,
+) {
+ 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 => {
+ 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 = [];
+ 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 (
+ <>
+
+
+
+
+ >
+ )
+ }
+
+ return connect(state => ({
+ site: state.getIn(['site', 'instance']),
+ }))(ComponentWithReport);
+}
\ No newline at end of file
diff --git a/frontend/app/components/ui/ItemMenu/ItemMenu.js b/frontend/app/components/ui/ItemMenu/ItemMenu.js
index e97f6999b..450cd7fc7 100644
--- a/frontend/app/components/ui/ItemMenu/ItemMenu.js
+++ b/frontend/app/components/ui/ItemMenu/ItemMenu.js
@@ -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 (
-
+
{label &&
{label}}
{ 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"
>
diff --git a/frontend/app/components/ui/ItemMenu/itemMenu.css b/frontend/app/components/ui/ItemMenu/itemMenu.css
index eabfb050a..7a87fb77a 100644
--- a/frontend/app/components/ui/ItemMenu/itemMenu.css
+++ b/frontend/app/components/ui/ItemMenu/itemMenu.css
@@ -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;
diff --git a/frontend/app/components/ui/Notification/Notification.js b/frontend/app/components/ui/Notification/Notification.js
index c2e69d054..fb4ffb742 100644
--- a/frontend/app/components/ui/Notification/Notification.js
+++ b/frontend/app/components/ui/Notification/Notification.js
@@ -12,6 +12,7 @@ export default ({ transition = Flip, position = 'bottom-right', autoClose = 3000
autoClose={ autoClose }
className={ styles.container }
toastClassName={ styles.toast }
+ closeButton={false}
{ ...props }
/>
);
diff --git a/frontend/app/components/ui/PageTitle/PageTitle.tsx b/frontend/app/components/ui/PageTitle/PageTitle.tsx
index 6ee102c0d..5bef94e3f 100644
--- a/frontend/app/components/ui/PageTitle/PageTitle.tsx
+++ b/frontend/app/components/ui/PageTitle/PageTitle.tsx
@@ -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 (
-
- {title}
-
+
+
+
+ {title}
+
+ { actionButton && actionButton}
+
+ {subTitle &&
{subTitle}
}
+
);
}
diff --git a/frontend/app/date.js b/frontend/app/date.ts
similarity index 94%
rename from frontend/app/date.js
rename to frontend/app/date.ts
index 089c48362..8a9501a86 100644
--- a/frontend/app/date.js
+++ b/frontend/app/date.ts
@@ -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();
-}
\ No newline at end of file
+}
+
+export const nowFormatted = (format?: string): string => {
+ return DateTime.local().toFormat(format || 'LLL dd, yyyy, hh:mm a');
+}
diff --git a/frontend/app/mstore/types/dashboard.ts b/frontend/app/mstore/types/dashboard.ts
index 57b191252..fbed41eb7 100644
--- a/frontend/app/mstore/types/dashboard.ts
+++ b/frontend/app/mstore/types/dashboard.ts
@@ -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,
diff --git a/frontend/app/styles/general.css b/frontend/app/styles/general.css
index 7e99468ad..245158049 100644
--- a/frontend/app/styles/general.css
+++ b/frontend/app/styles/general.css
@@ -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;
}
\ No newline at end of file
diff --git a/frontend/app/svg/icons/graph-up-arrow.svg b/frontend/app/svg/icons/graph-up-arrow.svg
index fd582e467..9a54cd2de 100644
--- a/frontend/app/svg/icons/graph-up-arrow.svg
+++ b/frontend/app/svg/icons/graph-up-arrow.svg
@@ -1,3 +1,3 @@
-