This commit is contained in:
Delirium 2025-06-08 07:57:05 +00:00 committed by GitHub
commit 37ab25e837
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
75 changed files with 880 additions and 335 deletions

View file

@ -10,6 +10,7 @@ import PublicRoutes from 'App/PublicRoutes';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import * as routes from './routes';
import Tracker from 'App/Tracker';
const components: any = {
SessionPure: lazy(() => import('Components/Session/Session')),
@ -43,6 +44,7 @@ function IFrameRoutes(props: Props) {
<ModalProvider>
<Layout hideHeader>
<Loader loading={!!loading} className="flex-1">
<Tracker />
<Suspense fallback={<Loader loading className="flex-1" />}>
<Switch key="content">
<Route

View file

@ -18,6 +18,7 @@ import { ModalProvider as NewModalProvider } from 'Components/ModalContext';
import { Loader } from 'UI';
import { observer } from 'mobx-react-lite';
import * as routes from './routes';
import Tracker from 'App/Tracker';
interface RouterProps extends RouteComponentProps {
match: {
@ -213,6 +214,7 @@ const Router: React.FC<RouterProps> = (props) => {
<NewModalProvider>
<ModalProvider>
<Loader loading={loading} className="flex-1">
<Tracker />
<Layout hideHeader={hideHeader}>
<PrivateRoutes />
</Layout>

3
frontend/app/Tracker.ts Normal file
View file

@ -0,0 +1,3 @@
export default function Tracker() {
return null;
}

View file

@ -6,8 +6,8 @@ import { MODULES } from 'Components/Client/Modules';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import AssistStats from '../../AssistStats';
import Recordings from '../RecordingsList/Recordings';
import { useTranslation } from 'react-i18next';
import TrainingVideosBtn from './TrainingVideosBtn';
function AssistSearchActions() {
const { t } = useTranslation();
@ -23,12 +23,6 @@ function AssistSearchActions() {
const showStats = () => {
showModal(<AssistStats />, { right: true, width: 960 });
};
const showRecords = () => {
showModal(<Recordings />, { right: true, width: 960 });
};
const originStr = window.env.ORIGIN || window.location.origin;
const isSaas = /app\.openreplay\.com/.test(originStr);
return (
<div className="flex items-center w-full gap-2">
<h3 className="text-2xl capitalize mr-2">
@ -44,9 +38,7 @@ function AssistSearchActions() {
{t('Clear')}
</Button>
</Tooltip>
{!isSaas && isEnterprise && !modules.includes(MODULES.OFFLINE_RECORDINGS)
? <Button size={'small'} onClick={showRecords}>{t('Training Videos')}</Button> : null
}
<TrainingVideosBtn />
{isEnterprise && userStore.account?.admin && (
<Button size={'small'} onClick={showStats}
disabled={modules.includes(MODULES.ASSIST_STATS) || modules.includes(MODULES.ASSIST)}>

View file

@ -0,0 +1,32 @@
import React from 'react';
import { useModal } from 'App/components/Modal';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { useTranslation } from 'react-i18next';
import { Button } from 'antd';
import { MODULES } from 'Components/Client/Modules';
import Recordings from '../RecordingsList/Recordings';
/** SAAS:
function TrainingVideosBtn() {
return null
}
*/
function TrainingVideosBtn() {
const { t } = useTranslation();
const { userStore } = useStore();
const modules = userStore.account.settings?.modules ?? [];
const { isEnterprise } = userStore;
const { showModal } = useModal();
const showRecords = () => {
showModal(<Recordings />, { right: true, width: 960 });
};
return (
isEnterprise && !modules.includes(MODULES.OFFLINE_RECORDINGS)
? <Button size={'small'} onClick={showRecords}>{t('Training Videos')}</Button> : null
)
}
export default observer(TrainingVideosBtn);

View file

@ -0,0 +1,3 @@
export default function Billing() {
return null;
}

View file

@ -1,7 +1,8 @@
import React from 'react';
import { withRouter } from 'react-router-dom';
import { Switch, Route, Redirect } from 'react-router';
import { CLIENT_TABS, client as clientRoute } from 'App/routes';
import { client as clientRoute } from 'App/routes';
import { CLIENT_TABS } from 'App/utils/routeUtils';
import { PANEL_SIZES } from 'App/constants/panelSizes'
import SessionsListingSettings from 'Components/Client/SessionsListingSettings';
@ -10,7 +11,7 @@ import ProfileSettings from './ProfileSettings';
import Integrations from './Integrations';
import UserView from './Users/UsersView';
import AuditView from './Audit/AuditView';
import Sites from './Sites';
import Billing from './Billing/Billing';
import Projects from './Projects';
import CustomFields from './CustomFields';
import Webhooks from './Webhooks';
@ -65,6 +66,12 @@ export default class Client extends React.PureComponent {
path={clientRoute(CLIENT_TABS.CUSTOM_FIELDS)}
component={CustomFields}
/>
<Route
exact
strict
path={clientRoute(CLIENT_TABS.BILLING)}
component={Billing}
/>
<Route
exact
strict

View file

@ -13,7 +13,7 @@ function Modules() {
const { userStore } = useStore();
const { updateModule } = userStore;
const modules = userStore.account.settings?.modules ?? [];
const isEnterprise = userStore.account.edition === 'ee';
const isEnterprise = userStore.isEnterprise;
const [modulesState, setModulesState] = React.useState<any[]>([]);
const onToggle = async (module: any) => {

View file

@ -0,0 +1,15 @@
const extra = (t: any) => []
export const enum MODULES {
ASSIST = 'assist',
HIGHLIGHTS = 'notes',
BUG_REPORTS = 'bug-reports',
OFFLINE_RECORDINGS = 'offline-recordings',
ALERTS = 'alerts',
ASSIST_STATS = 'assist-stats',
FEATURE_FLAGS = 'feature-flags',
RECOMMENDATIONS = 'recommendations',
USABILITY_TESTS = 'usability-tests',
}
export default extra;

View file

@ -1,18 +1,9 @@
import { TFunction } from 'i18next';
import extraModules, { MODULES } from './extra';
export * from './extra';
export { default } from './Modules';
export const enum MODULES {
ASSIST = 'assist',
HIGHLIGHTS = 'notes',
BUG_REPORTS = 'bug-reports',
OFFLINE_RECORDINGS = 'offline-recordings',
ALERTS = 'alerts',
ASSIST_STATS = 'assist-stats',
FEATURE_FLAGS = 'feature-flags',
RECOMMENDATIONS = 'recommendations',
USABILITY_TESTS = 'usability-tests',
}
export interface Module {
label: string;
@ -73,15 +64,6 @@ export const modules = (t: TFunction) => [
key: MODULES.FEATURE_FLAGS,
icon: 'toggles',
},
{
label: t('Recommendations'),
description: t(
'Get personalized recommendations for sessions to watch, based on your replay history and search preferences.',
),
key: MODULES.RECOMMENDATIONS,
icon: 'magic',
hidden: true,
},
{
label: t('Usability Tests'),
description: t(
@ -90,4 +72,5 @@ export const modules = (t: TFunction) => [
key: MODULES.USABILITY_TESTS,
icon: 'clipboard-check',
},
...extraModules(t),
];

View file

@ -0,0 +1,3 @@
function Clips() {
return null;
}

View file

@ -11,7 +11,6 @@ import {
AppWindow,
Combine,
Users,
Sparkles,
Globe,
MonitorSmartphone,
} from 'lucide-react';
@ -250,8 +249,6 @@ const AddCardSection = observer(
{ label: t('Web Analytics'), value: 'web_analytics' },
];
const originStr = window.env.ORIGIN || window.location.origin;
const isSaas = /api\.openreplay\.com/.test(originStr);
const onExistingClick = () => {
const dashboardId = dashboardStore.selectedDashboard?.dashboardId;
const siteId = projectsStore.activeSiteId;
@ -273,12 +270,6 @@ const AddCardSection = observer(
<div className="text-xl font-medium mb-1">
{t('What do you want to visualize?')}
</div>
{isSaas ? (
<div className="font-medium flex items-center gap-2 cursor-pointer">
<Sparkles color="#3C00FFD8" size={16} />
<div className="ai-gradient">{t('Ask AI')}</div>
</div>
) : null}
</div>
<div>
{options.length > 1 ? (

View file

@ -1,12 +1,12 @@
import { Card, Col, Modal, Row, Typography } from 'antd';
import { Col, Modal, Row, Typography } from 'antd';
import { GalleryVertical, Plus } from 'lucide-react';
import React from 'react';
import { useStore } from 'App/mstore';
import NewDashboardModal from 'Components/Dashboard/components/DashboardList/NewDashModal';
import AiQuery from './DashboardView/AiQuery';
import { useTranslation } from 'react-i18next';
import AiQuerySection, { panelSize } from './AiQuerySection';
interface Props {
open: boolean;
@ -32,8 +32,6 @@ function AddCardSelectionModal(props: Props) {
setOpen(true);
};
const originStr = window.env.ORIGIN || window.location.origin;
const isSaas = /app\.openreplay\.com/.test(originStr);
return (
<>
<Modal
@ -42,8 +40,9 @@ function AddCardSelectionModal(props: Props) {
footer={null}
onCancel={props.onClose}
className="addCard"
width={isSaas ? 900 : undefined}
width={panelSize}
>
<AiQuerySection />
<Row gutter={16} justify="center" className="py-5">
<Col span={12}>
<div

View file

@ -0,0 +1,30 @@
/** Saas:
import React from 'react'
import { Row } from 'antd'
import AiQuery from './DashboardView/AiQuery'
import { useTranslation } from 'react-i18next'
function AiQuerySection() {
const { t } = useTranslation()
return (
<>
<Row gutter={16} justify="center" className="py-2">
<AiQuery />
</Row>
<div className="flex items-center justify-center w-full text-disabled-text">
{t('or')}
</div>
</>
)
}
export const panelSize = 900
*/
function AiQuerySection() {
return null
}
export const panelSize = undefined
export default AiQuerySection

View file

@ -4,7 +4,7 @@ import { useStore } from 'App/mstore';
import { Loader } from 'UI';
import { withSiteId } from 'App/routes';
import withModal from 'App/components/Modal/withModal';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { RouteComponentProps } from 'react-router-dom';
import { useModal } from 'App/components/Modal';
import AlertFormModal from 'App/components/Alerts/AlertFormModal';
import withPageTitle from 'HOCs/withPageTitle';
@ -89,9 +89,6 @@ function DashboardView(props: Props) {
}, [dashboard]);
if (!dashboard) return null;
const originStr = window.env.ORIGIN || window.location.origin;
const isSaas = /app\.openreplay\.com/.test(originStr);
return (
<Loader loading={loading}>
<div
@ -104,7 +101,6 @@ function DashboardView(props: Props) {
siteId={siteId}
dashboardId={dashboardId}
/>
{isSaas ? <AiQuery /> : null}
<DashboardWidgetGrid
siteId={siteId}
dashboardId={dashboardId}

View file

@ -1,8 +1,6 @@
import React from 'react';
import { Icon } from 'UI';
import HealthModal from 'Components/Header/HealthStatus/HealthModal/HealthModal';
import HealthWidget from 'Components/Header/HealthStatus/HealthWidget';
import UserMenu from 'Components/Header/UserMenu/UserMenu';
import { Popover, Button } from 'antd';
import { ExclamationCircleOutlined } from '@ant-design/icons';
import { getHealthRequest } from './getHealth';

View file

@ -0,0 +1,3 @@
export default function SaasHeaderMenuItems() {
return null;
}

View file

@ -22,6 +22,7 @@ function Player() {
<div className={cn('relative flex-1', 'overflow-visible')}>
<Overlay isClickmap />
<div
data-openreplay-obscured
className={cn(stl.screenWrapper, stl.checkers, '!overflow-y-scroll')}
style={{ maxHeight: 800 }}
ref={screenWrapper}

View file

@ -11,6 +11,7 @@ import styles from 'Components/Session_/playerBlock.module.css';
import ClipPlayerOverlay from 'Components/Session/Player/ClipPlayer/ClipPlayerOverlay';
import { observer } from 'mobx-react-lite';
import { Icon } from 'UI';
import { Empty } from 'antd';
interface Props {
session: Session;
@ -25,8 +26,8 @@ interface Props {
function ClipPlayerContent(props: Props) {
const playerContext = React.useContext<IPlayerContext>(PlayerContext);
const screenWrapper = React.useRef<HTMLDivElement>(null);
const { time } = playerContext.store.get();
const { range, isFull } = props;
const { time, error } = playerContext.store.get();
const { range, isFull, isHighlight } = props;
React.useEffect(() => {
if (!playerContext.player) return;
@ -57,7 +58,16 @@ function ClipPlayerContent(props: Props) {
const outerHeight = props.isHighlight ? 556 + 39 : 556;
const innerHeight = props.isHighlight ? 504 + 39 : 504;
return (
return error ? (
<div
className="inset-0 flex items-center justify-center absolute"
style={{ height: 'auto' }}
>
<div className="flex flex-col items-center">
<Empty description="Session not found." />
</div>
</div>
) : (
<div
className={cn(
styles.playerBlock,
@ -73,7 +83,10 @@ function ClipPlayerContent(props: Props) {
>
<div className={cn(stl.playerBody, 'flex flex-1 flex-col relative')}>
<div className="relative flex-1 overflow-hidden group">
<ClipPlayerOverlay autoplay={props.autoplay} />
<ClipPlayerOverlay
isHighlight={isHighlight}
autoplay={props.autoplay}
/>
<div
className={cn(stl.screenWrapper, stl.checkers)}
ref={screenWrapper}
@ -91,7 +104,11 @@ function ClipPlayerContent(props: Props) {
<div className="leading-none font-medium">{props.message}</div>
</div>
) : null}
<ClipPlayerControls isFull={isFull} session={props.session} range={props.range} />
<ClipPlayerControls
isFull={isFull}
session={props.session}
range={props.range}
/>
</div>
</div>
);

View file

@ -35,7 +35,7 @@ function ClipPlayerHeader(props: Props) {
return (
<div className="bg-white p-3 flex justify-between items-center border-b relative">
{isHighlight && !isFull ? <PartialSessionBadge /> : null}
<UserCard session={props.session} />
{props.session ? <UserCard session={props.session} /> : null}
<Space>
<Tooltip title={t('Copy link to clipboard')} placement="bottom">

View file

@ -8,9 +8,10 @@ import AutoplayTimer from 'Components/Session/Player/ClipPlayer/AutoPlayTimer';
interface Props {
autoplay: boolean;
isHighlight?: boolean;
}
function Overlay({ autoplay }: Props) {
function Overlay({ autoplay, isHighlight }: Props) {
const { player, store } = React.useContext(PlayerContext);
const togglePlay = () => player.togglePlay();
@ -19,9 +20,11 @@ function Overlay({ autoplay }: Props) {
return (
<>
{messagesLoading ? <Loader /> : null}
{/* <div className="hidden group-hover:block"> */}
{/* <ClipFeedback/> */}
{/* </div> */}
{isHighlight ? null : (
<div className="hidden group-hover:block">
<ClipFeedback />
</div>
)}
<PlayIconLayer playing={playing} togglePlay={togglePlay} />
{completed && autoplay && <AutoplayTimer />}
</>

View file

@ -12,7 +12,7 @@ interface Props {}
function QueueControls(props: Props) {
const { t } = useTranslation();
const { clipStore, projectsStore, sessionStore, searchStore } = useStore();
const { clipStore } = useStore();
const previousId = clipStore.prevId;
const { nextId } = clipStore;

View file

@ -132,17 +132,7 @@ function Timeline({ range }: any) {
// };
return (
<div
className="flex items-center w-full"
style={
{
// top: '-4px',
// zIndex: 100,
// maxWidth: 'calc(100% - 5rem)',
// left: '3.5rem',
}
}
>
<div className="flex items-center w-full">
<div
className={stl.progress}
onClick={ready ? jumpToTime : undefined}

View file

@ -27,7 +27,7 @@ import {
} from 'App/mstore/uiPlayerStore';
import { useStore } from 'App/mstore';
import { session as sessionRoute, withSiteId } from 'App/routes';
import { SummaryButton } from 'Components/Session_/Player/Controls/Controls';
import SummaryButton from 'Components/Session_/Player/Controls/SummaryButton';
import useShortcuts from '../ReplayPlayer/useShortcuts';
import { useTranslation } from 'react-i18next';
@ -184,9 +184,6 @@ const DevtoolsButtons = observer(
} = store.get();
const showExceptions = exceptionsList.length > 0;
// @ts-ignore
const originStr = window.env.ORIGIN || window.location.origin;
const isSaas = /app\.openreplay\.com/.test(originStr);
const showSummary = () => {
player.pause();
@ -197,13 +194,11 @@ const DevtoolsButtons = observer(
};
return (
<>
{isSaas ? (
<SummaryButton
onClick={showSummary}
withToggle={bottomBlock === OVERVIEW}
toggleValue={aiSummaryStore.toggleSummary}
/>
) : null}
<SummaryButton
onClick={showSummary}
withToggle={bottomBlock === OVERVIEW}
toggleValue={aiSummaryStore.toggleSummary}
/>
<ControlButton
popover={
<div className="flex items-center gap-2">

View file

@ -143,6 +143,7 @@ function Player(props: IProps) {
isInspMode ? stl.solidBg : stl.checkers,
)}
ref={screenWrapper}
data-openreplay-obscured
/>
</div>
{!fullscreen && !!bottomBlock && (

View file

@ -7,15 +7,10 @@ import { debounce } from 'App/utils';
import { IResourceRequest, IResourceTiming } from 'App/player';
import { WsChannel } from 'App/player/web/messages';
import { PlayerContext } from 'App/components/Session/playerContext';
import MDRenderer from 'Shared/MDRenderer/MDRenderer';
let debounceUpdate: any = () => {};
const boldLine = /\*\*(.*?)\*\*/i;
function isTitleLine(line: string): boolean {
return boldLine.test(line);
}
function SummaryBlock({ sessionId }: { sessionId: string }) {
const { store } = React.useContext(PlayerContext);
const { tabStates } = store.get();
@ -24,7 +19,7 @@ function SummaryBlock({ sessionId }: { sessionId: string }) {
const zoomEnabled = uiPlayerStore.timelineZoom.enabled;
const zoomStartTs = uiPlayerStore.timelineZoom.startTs;
const zoomEndTs = uiPlayerStore.timelineZoom.endTs;
const { zoomTab } = uiPlayerStore;
const zoomTab = uiPlayerStore.zoomTab;
React.useEffect(() => {
debounceUpdate = debounce(
@ -76,31 +71,13 @@ function SummaryBlock({ sessionId }: { sessionId: string }) {
}
}, [zoomTab]);
const formattedText = aiSummaryStore.text.split('\n').map((line) => {
if (isTitleLine(line)) {
return (
<div className="font-semibold mt-2">{line.replace(/\*/g, '')}</div>
);
}
if (line.startsWith('*')) {
return (
<li className="ml-1 marker:mr-1 flex items-center gap-1">
<CodeStringFormatter text={line.replace(/\*/g, '')} />
</li>
);
}
return (
<div className="flex items-center gap-1">
<CodeStringFormatter text={line} />
</div>
);
});
return (
<div style={summaryBlockStyle}>
{aiSummaryStore.text ? (
<div className="rounded p-4 bg-white whitespace-pre-wrap flex flex-col">
{formattedText.map((v) => v)}
<div
className={'rounded p-4 bg-white whitespace-pre-wrap flex flex-col'}
>
<MDRenderer content={aiSummaryStore.text} />
</div>
) : (
<TextPlaceholder />
@ -111,44 +88,31 @@ function SummaryBlock({ sessionId }: { sessionId: string }) {
function TextPlaceholder() {
return (
<div className="animate-pulse rounded p-4 bg-white whitespace-pre-wrap flex flex-col gap-2">
<div className="h-2 bg-gray-medium rounded" />
<div className="h-2 bg-gray-medium rounded" />
<div className="grid grid-cols-3 gap-2">
<div className="h-2 bg-gray-medium rounded col-span-2" />
<div className="h-2 bg-gray-medium rounded col-span-1" />
<div
className={
'animate-pulse rounded p-4 bg-white whitespace-pre-wrap flex flex-col gap-2'
}
>
<div className={'h-2 bg-gray-medium rounded'} />
<div className={'h-2 bg-gray-medium rounded'} />
<div className={'grid grid-cols-3 gap-2'}>
<div className={'h-2 bg-gray-medium rounded col-span-2'} />
<div className={'h-2 bg-gray-medium rounded col-span-1'} />
</div>
<div className="grid grid-cols-4 gap-2 mt-3">
<div className="h-2 bg-gray-medium rounded col-span-1" />
<div className="h-2 bg-gray-medium rounded col-span-1" />
<div className="h-2 bg-gray-medium rounded col-span-2" />
<div className={'grid grid-cols-4 gap-2 mt-3'}>
<div className={'h-2 bg-gray-medium rounded col-span-1'} />
<div className={'h-2 bg-gray-medium rounded col-span-1'} />
<div className={'h-2 bg-gray-medium rounded col-span-2'} />
</div>
<div className="grid grid-cols-4 gap-2">
<div className="h-2 bg-gray-medium rounded col-span-2" />
<div className="h-2 bg-transparent rounded col-span-2" />
<div className={'grid grid-cols-4 gap-2'}>
<div className={'h-2 bg-gray-medium rounded col-span-2'} />
<div className={'h-2 bg-transparent rounded col-span-2'} />
</div>
</div>
);
}
function CodeStringFormatter({ text }: { text: string }) {
const parts = text.split(/(`[^`]*`)/).map((part, index) =>
part.startsWith('`') && part.endsWith('`') ? (
<div
key={index}
className="whitespace-nowrap bg-gray-lightest font-mono mx-1 px-1 border"
>
{part.substring(1, part.length - 1)}
</div>
) : (
<span key={index}>{part}</span>
),
);
return <>{parts}</>;
}
const summaryBlockStyle: React.CSSProperties = {
background:
'linear-gradient(180deg, #E8EBFF -24.14%, rgba(236, 254, 255, 0.00) 100%)',

View file

@ -10,7 +10,7 @@ import {
} from 'App/components/Session/playerContext';
import { useStore } from 'App/mstore';
import SummaryBlock from 'Components/Session/Player/ReplayPlayer/SummaryBlock';
import { SummaryButton } from 'Components/Session_/Player/Controls/Controls';
import SummaryButton from 'Components/Session_/Player/Controls/SummaryButton';
import TimelineZoomButton from 'Components/Session_/Player/Controls/components/TimelineZoomButton';
import { Icon, NoContent } from 'UI';
import TabSelector from '../../shared/DevTools/TabSelector';
@ -99,8 +99,6 @@ function MobileOverviewPanelCont() {
player.scale();
}, [selectedFeatures]);
const originStr = window.env.ORIGIN || window.location.origin;
const isSaas = /app\.openreplay\.com/.test(originStr);
return (
<PanelComponent
resources={resources}
@ -111,7 +109,7 @@ function MobileOverviewPanelCont() {
isMobile
performanceList={performanceList}
sessionId={sessionId}
showSummary={isSaas}
showSummary
toggleSummary={() =>
aiSummaryStore.setToggleSummary(!aiSummaryStore.toggleSummary)
}
@ -229,8 +227,6 @@ function WebOverviewPanelCont() {
],
);
const originStr = window.env.ORIGIN || window.location.origin;
const isSaas = /app\.openreplay\.com/.test(originStr);
return (
<PanelComponent
resources={resources}
@ -238,7 +234,7 @@ function WebOverviewPanelCont() {
selectedFeatures={selectedFeatures}
fetchPresented={fetchPresented}
setSelectedFeatures={setSelectedFeatures}
showSummary={isSaas}
showSummary
toggleSummary={() =>
aiSummaryStore.setToggleSummary(!aiSummaryStore.toggleSummary)
}

View file

@ -18,6 +18,7 @@ import {
LaunchStateShortcut,
LaunchXRaShortcut,
} from 'Components/Session_/Player/Controls/components/KeyboardHelp';
import { signalService } from 'App/services';
import {
CONSOLE,
GRAPHQL,
@ -29,12 +30,15 @@ import {
STACKEVENTS,
STORAGE,
BACKENDLOGS,
LONG_TASK
} from "App/mstore/uiPlayerStore";
} from 'App/mstore/uiPlayerStore';
import { Icon } from 'UI';
import LogsButton from 'App/components/Session/Player/SharedComponents/BackendLogs/LogsButton';
import { CodeOutlined, DashboardOutlined, ClusterOutlined } from '@ant-design/icons';
import { ArrowDownUp, ListCollapse, Merge, Waypoints, Timer } from 'lucide-react'
import {
CodeOutlined,
DashboardOutlined,
ClusterOutlined,
} from '@ant-design/icons';
import { ArrowDownUp, ListCollapse, Merge, Waypoints } from 'lucide-react';
import ControlButton from './ControlButton';
import Timeline from './Timeline';
@ -55,15 +59,30 @@ export const SKIP_INTERVALS = {
function getStorageName(type: any) {
switch (type) {
case STORAGE_TYPES.REDUX:
return { name: 'Redux', icon: <Icon name='integrations/redux' size={14} /> };
return {
name: 'Redux',
icon: <Icon name="integrations/redux" size={14} />,
};
case STORAGE_TYPES.MOBX:
return { name: 'Mobx', icon: <Icon name='integrations/mobx' size={14} /> };
return {
name: 'Mobx',
icon: <Icon name="integrations/mobx" size={14} />,
};
case STORAGE_TYPES.VUEX:
return { name: 'Vuex', icon: <Icon name='integrations/vuejs' size={14} /> };
return {
name: 'Vuex',
icon: <Icon name="integrations/vuejs" size={14} />,
};
case STORAGE_TYPES.NGRX:
return { name: 'NgRx', icon: <Icon name='integrations/ngrx' size={14} /> };
return {
name: 'NgRx',
icon: <Icon name="integrations/ngrx" size={14} />,
};
case STORAGE_TYPES.ZUSTAND:
return { name: 'Zustand', icon: <Icon name='integrations/zustand' size={14} /> };
return {
name: 'Zustand',
icon: <Icon name="integrations/zustand" size={14} />,
};
case STORAGE_TYPES.NONE:
return { name: 'State', icon: <ClusterOutlined size={14} /> };
default:
@ -80,6 +99,7 @@ function Controls({ setActiveTab, activeTab }: any) {
sessionStore,
userStore,
} = useStore();
const [mounted, setMounted] = React.useState(false);
const permissions = userStore.account.permissions || [];
const disableDevtools =
userStore.isEnterprise &&
@ -114,6 +134,7 @@ function Controls({ setActiveTab, activeTab }: any) {
const disabled =
disableDevtools || messagesLoading || inspectorMode || markedTargets;
const sessionTz = session?.timezone;
const sessionId = session?.sessionId;
const nextHandler = () => {
history.push(withSiteId(sessionRoute(nextSessionId), siteId));
@ -134,19 +155,63 @@ function Controls({ setActiveTab, activeTab }: any) {
disableDevtools,
});
React.useEffect(() => {
setMounted(true);
}, []);
React.useEffect(() => {
if (mounted) {
signalService.send(
{
source: 'speed',
value: speed,
},
sessionId,
);
}
}, [speed]);
const forthTenSeconds = () => {
// @ts-ignore
player.jumpInterval(SKIP_INTERVALS[skipInterval]);
signalService.send(
{
source: 'fast_forward',
},
sessionId,
);
};
const backTenSeconds = () => {
// @ts-ignore
player.jumpInterval(-SKIP_INTERVALS[skipInterval]);
signalService.send(
{
source: 'rewind',
},
sessionId,
);
};
const toggleBottomTools = (blockName: number) => {
player.toggleInspectorMode(false);
toggleBottomBlock(blockName);
signalService.send(
{
source: getBlockLabel(blockName)!,
},
sessionId,
);
};
const togglePlay = () => {
player.togglePlay();
signalService.send(
{
source: playing ? 'pause' : 'play',
},
sessionId,
);
};
const state = completed
@ -174,7 +239,7 @@ function Controls({ setActiveTab, activeTab }: any) {
playButton={
<PlayButton
state={state}
togglePlay={player.togglePlay}
togglePlay={togglePlay}
iconSize={36}
/>
}
@ -231,11 +296,6 @@ const DevtoolsButtons = observer(
const { t } = useTranslation();
const { aiSummaryStore, integrationsStore } = useStore();
const { store, player } = React.useContext(PlayerContext);
// @ts-ignore
const originStr = window.env.ORIGIN || window.location.origin;
const isSaas = /app\.openreplay\.com/.test(originStr);
const { inspectorMode, currentTab, tabStates } = store.get();
const disableButtons = disabled;
@ -299,12 +359,13 @@ const DevtoolsButtons = observer(
icon: <Timer size={14} strokeWidth={2} />,
label: t('Long Tasks'),
},
}
};
// @ts-ignore
const getLabel = (block: string) => labels[block][showIcons ? 'icon' : 'label']
const getLabel = (block: string) =>
labels[block][showIcons ? 'icon' : 'label'];
return (
<>
{isSaas ? <SummaryButton onClick={showSummary} /> : null}
<SummaryButton onClick={showSummary} />
<ControlButton
popover={
<div className="flex items-center gap-2">
@ -364,14 +425,6 @@ const DevtoolsButtons = observer(
label={getLabel('performance')}
/>
<ControlButton
customKey="longTask"
disabled={disableButtons}
onClick={() => toggleBottomTools(LONG_TASK)}
active={bottomBlock === LONG_TASK && !inspectorMode}
label={getLabel('longTask')}
/>
{showGraphql && (
<ControlButton
disabled={disableButtons}
@ -479,7 +532,7 @@ export const gradientButton = {
alignItems: 'center',
justifyContent: 'center',
};
const onHoverFillStyle = {
export const onHoverFillStyle = {
width: '100%',
height: '100%',
display: 'flex',
@ -489,7 +542,7 @@ const onHoverFillStyle = {
padding: '1px 8px',
background: 'linear-gradient(156deg, #E3E6FF 0%, #E4F3F4 69.48%)',
};
const fillStyle = {
export const fillStyle = {
width: '100%',
height: '100%',
display: 'flex',

View file

@ -0,0 +1,44 @@
/** Saas
import React from 'react'
import { Switch, } from 'antd'
import { Icon } from 'UI';
import { useTranslation } from 'react-i18next';
import { gradientButton, onHoverFillStyle, fillStyle } from './Controls';
function SummaryButton({
onClick,
withToggle,
onToggle,
toggleValue,
}: {
onClick?: () => void;
withToggle?: boolean;
onToggle?: () => void;
toggleValue?: boolean;
}) {
const { t } = useTranslation();
const [isHovered, setHovered] = React.useState(false);
return (
<div style={gradientButton} onClick={onClick}>
<div
style={isHovered ? onHoverFillStyle : fillStyle}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{withToggle ? (
<Switch size="small" checked={toggleValue} onChange={onToggle} />
) : null}
<Icon name="sparkles" size={16} />
<div className="font-semibold text-main">{t('Summary AI')}</div>
</div>
</div>
);
}
*/
function SummaryButton(props: any) {
return null;
}
export default SummaryButton;

View file

@ -16,6 +16,7 @@ import {
import stl from './timeline.module.css'
import TooltipContainer from './components/TooltipContainer';
import CustomDragLayer, { OnDragCallback } from './components/CustomDragLayer';
import { signalService } from 'App/services';
function Timeline({ isMobile }: { isMobile: boolean }) {
const { player, store } = useContext(PlayerContext);
@ -32,6 +33,7 @@ function Timeline({ isMobile }: { isMobile: boolean }) {
const highlightEnabled = uiPlayerStore.highlightSelection.enabled;
const { playing, skipToIssue, ready, endTime, devtoolsLoading, domLoading } =
store.get();
const sessionId = sessionStore.current.sessionId;
const progressRef = useRef<HTMLDivElement>(null);
const timelineRef = useRef<HTMLDivElement>(null);
@ -67,6 +69,10 @@ function Timeline({ isMobile }: { isMobile: boolean }) {
const time = Math.max(Math.round(p * endTime), 0);
debouncedJump(time);
hideTimeTooltip();
signalService.send({
source: 'jump',
value: time,
}, sessionId)
if (playing) {
setWasPlaying(true);
player.pause();

View file

@ -5,7 +5,6 @@ import React from 'react';
import { Link2 } from 'lucide-react';
import copy from 'copy-to-clipboard';
import { toast } from 'react-toastify';
import { PlayerContext } from 'App/components/Session/playerContext';
import {
CONSOLE,

View file

@ -0,0 +1,5 @@
function SimilarSessionsButton() {
return null
}
export default SimilarSessionsButton;

View file

@ -27,6 +27,7 @@ import QueueControls from './QueueControls';
import HighlightButton from './Highlight/HighlightButton';
import ShareModal from '../shared/SharePopup/SharePopup';
import { useTranslation } from 'react-i18next';
import SimilarSessionsButton from './SimilarSessions/SimilarSessionsButton';
const disableDevtools = 'or_devtools_uxt_toggle';
@ -167,6 +168,7 @@ function SubHeader(props) {
)}
style={{ width: 'max-content' }}
>
<SimilarSessionsButton />
<Tooltip title={t('Share Session')} placement="bottom">
<AntButton
size="small"

View file

@ -0,0 +1,5 @@
function Survey() {
return null;
}
export default Survey;

View file

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

View file

@ -4,6 +4,7 @@ import React, { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { signalService } from 'App/services';
interface Props {
sessionId: any;
@ -35,6 +36,14 @@ function Bookmark({ sessionId }: Props) {
onToggleFavorite(sessionId).then(() => {
toast.success(isFavorite ? REMOVED_MESSAGE : ADDED_MESSAGE);
setIsFavorite(!isFavorite);
signalService.send(
{
source: 'vault',
value: !isFavorite,
},
sessionId,
);
});
};

View file

@ -1,5 +1,6 @@
import React, { useState } from 'react';
import cn from 'classnames';
import ExplainButton from 'Shared/DevTools/ExplainButton';
import { Icon } from 'UI';
import JumpButton from 'Shared/DevTools/JumpButton';
import TabTag from '../TabTag';
@ -13,6 +14,7 @@ interface Props {
onClick?: () => void;
getTabNum?: (tab: string) => number;
showSingleTab: boolean;
sessionId: string;
}
function ConsoleRow(props: Props) {
const { log, iconProps, jump, renderWithNL, style } = props;
@ -106,7 +108,21 @@ function ConsoleRow(props: Props) {
</div>
))}
</div>
<JumpButton time={log.time} onClick={() => jump(log.time)} />
<JumpButton
extra={
<ExplainButton
sessionId={props.sessionId}
log={{
level: log.level,
message: log.message ? `${log.value} ${log.message}` : log.value,
}}
/>
}
time={log.time}
onClick={() => {
jump(log.time);
}}
/>
</div>
);
}

View file

@ -0,0 +1,3 @@
export default function ExplainButton(props: any) {
return null;
}

View file

@ -8,15 +8,14 @@ interface Props {
onClick: any;
time?: number;
tooltip?: string;
extra?: React.ReactNode;
}
function JumpButton(props: Props) {
const { tooltip } = props;
return (
<div className="absolute right-2 top-0 bottom-0 my-auto flex items-center">
{props.time ? (
<div className="block mr-2 text-sm">
{shortDurationFromMs(props.time)}
</div>
<div className="absolute right-2 top-0 bottom-0 my-auto flex items-center gap-2">
{props.extra ? (
<div className={'hidden group-hover:block'}>{props.extra}</div>
) : null}
<Tooltip title={tooltip} disabled={!tooltip}>
<Button
@ -32,6 +31,11 @@ function JumpButton(props: Props) {
>
JUMP
</Button>
{props.time ? (
<div className="block group-hover:hidden mr-2 text-sm">
{shortDurationFromMs(props.time)}
</div>
) : null}
</Tooltip>
</div>
);

View file

@ -35,6 +35,8 @@ import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
import WSPanel from './WSPanel';
import { useTranslation } from 'react-i18next';
import { mergeListsWithZoom, processInChunks } from './utils';
import check from './hasExplainAi';
import ExplainButton from '../ExplainButton';
// Constants remain the same
const INDEX_KEY = 'network';
@ -734,6 +736,10 @@ export const NetworkPanelComp = observer(
return cols;
}, [showSingleTab, activeTab, t, getTabName, getTabNum, isSpot]);
const hasExplainAi = (reqType: string) => {
// @ts-ignore
return check && [ResourceType.XHR, ResourceType.FETCH].includes(reqType);
};
return (
<BottomBlock
style={{ height: '100%' }}
@ -869,6 +875,19 @@ export const NetworkPanelComp = observer(
player.jump(row.time);
}}
activeIndex={activeIndex}
extra={(row) =>
hasExplainAi(row.type) ? (
<ExplainButton
sessionId={sessionId}
request={{
url: row.url,
status: parseInt(row.status),
payload: row.request,
response: row.response,
}}
/>
) : null
}
>
{tableCols}
</TimeTable>

View file

@ -0,0 +1 @@
export default false;

View file

@ -70,6 +70,7 @@ type Props = {
hoverable?: boolean;
onRowClick?: (row: any, index: number) => void;
onJump?: (obj: { time: number }) => void;
extra?: (row: Record<string, any>) => React.ReactNode;
};
type TimeLineInfo = {
@ -145,7 +146,10 @@ function TimeTable(props: Props) {
]);
React.useEffect(() => {
if (props.activeIndex && props.activeIndex >= 0 && scroller.current) {
scroller.current.scrollToIndex(props.activeIndex, { align: 'center', smooth: false });
scroller.current.scrollToIndex(props.activeIndex, {
align: 'center',
smooth: false,
});
setFirstVisibleRowIndex(props.activeIndex ?? 0);
}
}, [props.activeIndex]);
@ -296,6 +300,7 @@ function TimeTable(props: Props) {
onRowClick={onRowClick}
activeIndex={activeIndex}
onJump={onJump}
extra={props.extra}
/>
)}
</VList>
@ -316,6 +321,7 @@ function RowRenderer({
onRowClick,
activeIndex,
onJump,
extra,
}: any) {
if (!row) return;
return (
@ -350,7 +356,10 @@ function RowRenderer({
popup={renderPopup}
/>
</div>
<JumpButton onClick={() => onJump(index)} />
<JumpButton
extra={extra ? extra(row) : null}
onClick={() => onJump(index)}
/>
</div>
);
}

View file

@ -0,0 +1,4 @@
export default function MDRenderer(props: any) {
console.warn('saas comp')
return null;
}

View file

@ -17,7 +17,7 @@ const { Text } = Typography;
function ProjectDropdown(props: { location: any }) {
const mstore = useStore();
const { t } = useTranslation();
const { projectsStore, searchStore, searchStoreLive, userStore } = mstore;
const { projectsStore, searchStore, searchStoreLive, userStore, aiFiltersStore } = mstore;
const { account } = userStore;
const sites = projectsStore.list;
const { siteId } = projectsStore;
@ -32,6 +32,7 @@ function ProjectDropdown(props: { location: any }) {
const handleSiteChange = async (newSiteId: string) => {
mstore.initClient();
aiFiltersStore.clearFilters();
setSiteId(newSiteId);
searchStore.clearSearch();
searchStore.clearList();

View file

@ -0,0 +1,14 @@
/** Saas
import React from 'react'
import AiSessionSearchField from 'Shared/SessionFilters/AiSessionSearchField';
function AiSearchSection() {
return <AiSessionSearchField />
}
*/
function AiSearchSection() {
return null
}
export default AiSearchSection

View file

@ -3,7 +3,7 @@ import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import { Button, Tooltip } from 'antd';
import AiSessionSearchField from 'Shared/SessionFilters/AiSessionSearchField';
import AiSearchSection from './AiSearchSection';
import { useTranslation } from 'react-i18next';
import SavedSearch from '../SavedSearch/SavedSearch';
@ -30,13 +30,11 @@ function SearchActions() {
return t('Sessions');
}, [activeTab?.type, isEnterprise, i18n.language]);
// @ts-ignore
const originStr = window.env.ORIGIN || window.location.origin;
const isSaas = /app\.openreplay\.com/.test(originStr);
const showAiField = isSaas && activeTab?.type === 'sessions';
const showAiField = activeTab?.type === 'sessions';
const showPanel = hasEvents || hasFilters || aiFiltersStore.isLoading;
return !metaLoading ? (
<div className="mb-2">
{/* mobile */}
<div className={'flex flex-col lg:hidden items-start gap-2 w-full'}>
<div className='flex items-center justify-between w-full'>
<h2 className="text-2xl capitalize mr-4 inline">{title}</h2>
@ -54,12 +52,13 @@ function SearchActions() {
</Tooltip>
</div>
</div>
{isSaas ? <AiSessionSearchField/> : null}
<AiSearchSection />
</div>
{/* desktop */}
<div className={'hidden lg:flex items-center gap-2 w-full '}>
<h2 className="text-2xl capitalize mr-4">{title}</h2>
{isSaas && showAiField ? <AiSessionSearchField /> : null}
{showAiField ? <AiSearchSection /> : null}
<div className="ml-auto" />
<SavedSearch />
<Tooltip title={t('Clear Search Filters')}>

View file

@ -1,100 +1,5 @@
import { CloseOutlined, EnterOutlined } from '@ant-design/icons';
import { observer } from 'mobx-react-lite';
import React, { useState } from 'react';
import { useStore } from 'App/mstore';
import { Input } from 'UI';
const AiSearchField = observer(() => {
const { searchStore } = useStore();
const appliedFilter = searchStore.instance;
const hasFilters =
appliedFilter && appliedFilter.filters && appliedFilter.filters.length > 0;
const { aiFiltersStore } = useStore();
const [searchQuery, setSearchQuery] = useState('');
const onSearchChange = ({ target: { value } }: any) => {
setSearchQuery(value);
};
const fetchResults = () => {
if (searchQuery) {
void aiFiltersStore.getSearchFilters(searchQuery);
}
};
const handleKeyDown = (event: any) => {
if (event.key === 'Enter') {
fetchResults();
}
};
const clearAll = () => {
searchStore.clearSearch();
setSearchQuery('');
};
React.useEffect(() => {
if (aiFiltersStore.filtersSetKey !== 0) {
searchStore.edit(aiFiltersStore.filters);
}
}, [aiFiltersStore.filters, aiFiltersStore.filtersSetKey]);
return (
<div className="w-full">
<Input
onChange={onSearchChange}
placeholder='E.g., "Sessions with login issues this week"'
id="search"
onKeyDown={handleKeyDown}
value={searchQuery}
style={{ minWidth: 360, height: 30 }}
autoComplete="off"
className="px-4 py-1 text-lg placeholder-lg !border-0 nofocus"
leadingButton={
searchQuery !== '' ? (
<div
className="h-full flex items-center cursor-pointer"
onClick={hasFilters ? clearAll : fetchResults}
>
<div className="px-2 py-1 hover:bg-active-blue rounded mr-2">
{hasFilters ? <CloseOutlined /> : <EnterOutlined />}
</div>
</div>
) : null
}
/>
</div>
);
});
function AiSessionSearchField() {
const { aiFiltersStore } = useStore();
return (
<div className="bg-white rounded-full shadow-sm w-full">
<div
className={aiFiltersStore.isLoading ? 'animate-bg-spin' : ''}
style={gradientBox}
>
<AiSearchField />
</div>
</div>
);
return null;
}
export const gradientBox = {
border: 'double 1.5px transparent',
borderRadius: '100px',
background:
'linear-gradient(#ffffff, #ffffff), linear-gradient(-45deg, #394eff, #3eaaaf, #3ccf65)',
backgroundOrigin: 'border-box',
backgroundSize: '200% 200%',
backgroundClip: 'content-box, border-box',
display: 'flex',
gap: '0.25rem',
alignItems: 'center',
width: '100%',
overflow: 'hidden',
};
export default observer(AiSessionSearchField);
export default AiSessionSearchField;

View file

@ -7,6 +7,7 @@ import { useStore } from 'App/mstore';
import SessionCopyLink from './SessionCopyLink';
import IntegrateSlackButton from '../IntegrateSlackButton/IntegrateSlackButton';
import { useTranslation } from 'react-i18next';
import { signalService } from 'App/services';
interface Channel {
webhookId: string;
@ -126,7 +127,19 @@ const ShareModalComp: React.FC<Props> = ({ showCopyLink, hideModal, time }) => {
);
const sendMsg = async () => {
shareTo === 'slack' ? await shareToSlack() : await shareToMSTeams();
if (shareTo === 'slack') {
await shareToSlack();
} else {
await shareToMSTeams();
}
signalService.send(
{
source: 'share',
value: shareTo,
},
sessionId,
);
};
const hasBoth = slackOptions.length > 0 && msTeamsOptions.length > 0;

View file

@ -17,6 +17,12 @@ const DATE_RANGE_LABELS = {
[CUSTOM_RANGE]: 'Custom Range',
};
const LONG_RANGE_LABELS = {
LAST_3_MONTHS: 'Last 3 Months',
LAST_YEAR: 'Last Year',
CUSTOM_RANGE: 'Custom Range',
}
const COMPARISON_DATE_RANGE_LABELS = {
PREV_24_HOURS: 'Previous Day',
PREV_7_DAYS: 'Previous Week',
@ -26,11 +32,15 @@ const COMPARISON_DATE_RANGE_LABELS = {
};
const DATE_RANGE_VALUES = {};
const LONG_RANGE_VALUES = {};
Object.keys(DATE_RANGE_LABELS).forEach((key) => {
DATE_RANGE_VALUES[key] = key;
});
Object.keys(LONG_RANGE_LABELS).forEach((key) => {
LONG_RANGE_VALUES[key] = key;
});
export { DATE_RANGE_VALUES };
export { DATE_RANGE_VALUES, LONG_RANGE_LABELS };
export const dateRangeValues = Object.keys(DATE_RANGE_VALUES);
export const DATE_RANGE_OPTIONS =
@ -38,6 +48,12 @@ export const DATE_RANGE_OPTIONS =
label: DATE_RANGE_LABELS[key],
value: key,
}));
export const LONG_DATE_RANGE_OPTIONS =
Object.keys(LONG_RANGE_LABELS).map((key) => ({
label: LONG_RANGE_LABELS[key],
value: key,
}));
export const DATE_RANGE_COMPARISON_OPTIONS =
Object.keys(COMPARISON_DATE_RANGE_LABELS).map((key) => ({
label: COMPARISON_DATE_RANGE_LABELS[key],
@ -45,9 +61,11 @@ export const DATE_RANGE_COMPARISON_OPTIONS =
}));
export function getDateRangeLabel(value, t) {
return t(DATE_RANGE_LABELS[value]);
const string = DATE_RANGE_LABELS[value] ?? LONG_RANGE_LABELS[value];
return t(string);
}
export function getDateRangeFromValue(value) {
const tz = JSON.parse(localStorage.getItem(TIMEZONE));
const offset = tz ? tz.value : undefined;
@ -106,6 +124,14 @@ export function getDateRangeFromValue(value) {
// return Interval.fromDateTimes(now.startOf('year'), now.endOf('year'));
// case DATE_RANGE_VALUES.CUSTOM_RANGE:
// return Interval.fromDateTimes(now, now);
case LONG_RANGE_VALUES.LAST_YEAR:
const lastYear = now.minus({ years: 1 });
return Interval.fromDateTimes(lastYear.startOf('year'), lastYear.endOf('year'));
case LONG_RANGE_VALUES.LAST_3_MONTHS:
return Interval.fromDateTimes(
now.minus({ months: 3 }).startOf('month'),
now.endOf('month'),
);
default:
throw new Error('Invalid date range value');
}

View file

@ -0,0 +1,2 @@
export const routeIdRequired = [];
export const changeAvailable = [];

View file

@ -0,0 +1,3 @@
export default function CrispIframe({ WEBSITE_ID }: { WEBSITE_ID?: string }) {
return null;
}

View file

@ -2,7 +2,7 @@ import { ArrowRightOutlined } from '@ant-design/icons';
import { Button, Drawer, Space, Typography } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import CrispIframe from './CrispIframe';
import { Icon } from 'UI';
const { Text } = Typography;
@ -126,19 +126,7 @@ function SupportModal(props: Props) {
</div>
</div>
{!!WEBSITE_ID && (
<div className="flex rounded border w-full">
<iframe
src={`https://go.crisp.chat/chat/embed/?website_id=${WEBSITE_ID}`}
style={{
height: '415px',
margin: '0',
padding: '0',
width: '100%',
}}
/>
</div>
)}
<CrispIframe WEBSITE_ID={WEBSITE_ID} />
</div>
</Drawer>
);

View file

@ -4,8 +4,7 @@ import { getInitials } from 'App/utils';
import Notifications from 'Components/Alerts/Notifications/Notifications';
import HealthStatus from 'Components/Header/HealthStatus';
import UserMenu from 'Components/Header/UserMenu/UserMenu';
import LanguageSwitcher from 'Components/LanguageSwitcher/LanguageSwitcher';
import SaasHeaderMenuItems from 'Components/Header/SaasHeaderMenuItems/SaasHeaderMenuItems';
import GettingStartedProgress from 'Shared/GettingStarted/GettingStartedProgress';
import ProjectDropdown from 'Shared/ProjectDropdown';
import { useStore } from 'App/mstore';
@ -20,6 +19,7 @@ function TopRight() {
<Space style={{ lineHeight: '0' }}>
{spotOnly ? null : (
<>
<SaasHeaderMenuItems />
<ProjectDropdown />
<GettingStartedProgress />

View file

@ -49,6 +49,11 @@ export default class AiFiltersStore {
console.log(r);
};
clearFilters = (): void => {
this.filters = { filters: [] };
this.filtersSetKey = 0;
};
setLoading = (loading: boolean): void => {
this.isLoading = loading;
};

View file

@ -0,0 +1,2 @@
export default class BillingStore {}
// SAAS only

View file

@ -0,0 +1,9 @@
export default class ClipStore {
prevId = undefined;
nextId = undefined;
currentId = undefined;
next() {}
prev() {}
}
// SAAS only

View file

@ -34,6 +34,8 @@ import userStore from './userStore';
import UxtestingStore from './uxtestingStore';
import WeeklyReportStore from './weeklyReportConfigStore';
import logger from '@/logger';
import BillingStore from "@/mstore/billingStore";
import ClipStore from "@/mstore/clipStore";
const projectStore = new ProjectsStore();
const sessionStore = new SessionStore();
@ -110,6 +112,8 @@ export class RootStore {
searchStoreLive: SearchStoreLive;
integrationsStore: IntegrationsStore;
projectsStore: ProjectsStore;
billingStore: BillingStore;
clipStore: ClipStore;
constructor() {
this.dashboardStore = new DashboardStore();
@ -142,6 +146,8 @@ export class RootStore {
this.searchStore = searchStore;
this.searchStoreLive = searchStoreLive;
this.integrationsStore = new IntegrationsStore();
this.billingStore = new BillingStore();
this.clipStore = new ClipStore();
}
initClient() {

View file

@ -66,6 +66,8 @@ class UserFilter {
return {
endDate: this.period.end,
startDate: this.period.start,
startTimestamp: this.startDate,
endTimestamp: this.endDate,
filters: this.filters.map(filterMap),
page: this.page,
limit: this.limit,

View file

@ -94,7 +94,8 @@ class UserStore {
get isEnterprise() {
return (
this.account?.edition === 'ee' ||
this.authStore.authDetails?.edition === 'ee'
this.authStore.authDetails?.edition === 'ee' ||
this.account?.plan?.type === 'enterprise'
);
}

View file

@ -118,7 +118,7 @@ export default class Player extends Animator {
}
// toggle range (start, end)
toggleRange(start: number, end: number) {
toggleRange = (start: number, end: number) => {
this.pState.update({ range: [start, end] });
}

View file

@ -1,3 +1,8 @@
import { CLIENT_TABS } from './utils/routeUtils'
import { routeIdRequired, changeAvailable } from './extraRoutes';
export * from './extraRoutes';
const hashed = (path: string, hash?: string | number): string => {
if ((typeof hash === 'string' && hash !== '') || typeof hash === 'number') {
return `${path}#${hash}`;
@ -77,20 +82,6 @@ export const signup = (): string => '/signup';
export const forgotPassword = (): string => '/reset-password';
export const CLIENT_TABS = {
INTEGRATIONS: 'integrations',
PROFILE: 'account',
SESSIONS_LISTING: 'sessions-listing',
MANAGE_USERS: 'team',
MANAGE_ROLES: 'roles',
SITES: 'projects',
CUSTOM_FIELDS: 'metadata',
WEBHOOKS: 'webhooks',
NOTIFICATIONS: 'notifications',
AUDIT: 'audit',
BILLING: 'billing',
MODULES: 'modules',
};
export const CLIENT_DEFAULT_TAB = CLIENT_TABS.PROFILE;
const routerClientTabString = `:activeTab(${Object.values(CLIENT_TABS).join('|')})`;
export const client = (tab = routerClientTabString): string => `/client/${tab}`;
@ -203,6 +194,7 @@ export const highlights = (): string => '/highlights';
export const kai = (): string => '/kai';
const REQUIRED_SITE_ID_ROUTES = [
...routeIdRequired,
liveSession(''),
session(''),
sessions(),
@ -275,6 +267,7 @@ export function isRoute(route: string, path: string): boolean {
}
const SITE_CHANGE_AVAILABLE_ROUTES = [
...changeAvailable,
sessions(),
notes(),
bookmarks(),

View file

@ -17,7 +17,7 @@ export default class AiService extends BaseService {
},
);
return r.json();
return r.text();
}
async getDetailedSummary(
@ -37,7 +37,7 @@ export default class AiService extends BaseService {
},
);
return r.json();
return r.text();
}
async getSearchFilters(query: string): Promise<Record<string, any>> {

View file

@ -0,0 +1,38 @@
export default class BillingService {
initClient(arg: any) {
return;
}
fetchPlan = async () => {
return Promise.resolve()
}
fetchInvoices = async (period: {
startTimestamp: number,
endTimestamp: number,
limit: number,
page: number,
}) => {
return Promise.resolve()
}
upgradePlan = async (instance: any) => {
return Promise.resolve()
}
updatePlan = async (instance: any) => {
return Promise.resolve()
}
cancelPlan = async () => {
return Promise.resolve()
}
enablePlan = async () => {
return Promise.resolve()
}
getOnboard = async () => {
return Promise.resolve()
}
}

View file

@ -25,4 +25,10 @@ export default class ConfigService extends BaseService {
.then((r) => r.json())
.then((j) => j.data);
}
async checkElasticConnection(params: any): Promise<any> {
return this.client.post('/integartions/elasticsearch/test', params)
.then((r) => r.json())
.then((j) => j.data)
}
}

View file

@ -45,7 +45,9 @@ export default class SettingsService {
.catch((e) => Promise.reject(e));
}
getFirstMobUrl(sessionId: string): Promise<{ domURL: string[], fileKey?: string }> {
getFirstMobUrl(
sessionId: string,
): Promise<{ domURL: string[]; fileKey?: string }> {
return this.client
.get(`/sessions/${sessionId}/first-mob`)
.then((r) => r.json())
@ -53,6 +55,44 @@ export default class SettingsService {
.catch(console.error);
}
getRecommendedSessions(sort?: any): Promise<{
sessions: ISession[];
total: number;
}> {
return this.client
.post('/sessions-recommendations', sort)
.then((r) => r.json())
.then((response) => response || [])
.catch((e) => Promise.reject(e));
}
getFinetuneSessions(): Promise<{ sessions: string[] }> {
return this.client
.get('/PROJECT_ID/finetuning/sessions')
.then((r) => r.json())
.catch(Promise.reject);
}
sendFeedback(data: any): Promise<any> {
return this.client
.post(`/session-feedback`, data)
.then((r) => r.json())
.then((j) => j.data || [])
.catch(Promise.reject);
}
signalFinetune() {
return this.client.get('/PROJECT_ID/finetune');
}
checkFeedback(sessionId: string): Promise<any> {
return this.client
.get(`/session-feedback/${sessionId}`)
.then((r) => r.json())
.then((j) => j.data || false)
.catch(Promise.reject);
}
getSessionInfo(sessionId: string, isLive?: boolean): Promise<ISession> {
return this.client
.get(
@ -129,6 +169,22 @@ export default class SettingsService {
.catch(Promise.reject);
}
async fetchSimilarSessions(
sessionId: string,
params: any,
): Promise<{ sessions: ISession[] }> {
try {
const r = await this.client.post(
`/PROJECT_ID/similar-sessions/${sessionId}`,
params,
);
const j = await r.json();
return j.sessions || [];
} catch (reason) {
return Promise.reject(reason);
}
}
async getAssistCredentials(): Promise<any> {
try {
const r = await this.client.get('/config/assist/credentials');
@ -138,4 +194,26 @@ export default class SettingsService {
return Promise.reject(reason);
}
}
generateShorts(projectId: string) {
try {
void this.client.post(`/${projectId}/generate/shorts`, {});
} catch (reason) {
console.error('Error generating shorts:', reason);
}
}
async fetchSessionClips(): Promise<{ clips: any[] }> {
try {
const r = await this.client.get('/PROJECT_ID/shorts-recommendations', {
sortBy: 'startTs',
sortOrder: 'desc',
});
// .get('/PROJECT_ID/shorts-recommendations');
const j = await r.json();
return j || {};
} catch (reason) {
return Promise.reject(reason);
}
}
}

View file

@ -0,0 +1,9 @@
export default class SignalService {
initClient(arg: any) {
return;
}
send(...args: any[]): Promise<any> {
return Promise.resolve()
}
}

View file

@ -18,6 +18,7 @@ import SessionService from './SessionService';
import UserService from './UserService';
import UxtestingService from './UxtestingService';
import WebhookService from './WebhookService';
import SpotService from './spotService';
import LoginService from './loginService';
import FilterService from './FilterService';
@ -26,6 +27,8 @@ import CustomFieldService from './CustomFieldService';
import IntegrationsService from './IntegrationsService';
import ProjectsService from './ProjectsService';
import KaiService from '@/components/Kai/KaiService';
import SignalService from './SignalService';
import BillingService from '@/services/BillingService';
export const dashboardService = new DashboardService();
export const metricService = new MetricService();
@ -39,6 +42,8 @@ export const recordingsService = new RecordingsService();
export const configService = new ConfigService();
export const alertsService = new AlertsService();
export const webhookService = new WebhookService();
export const signalService = new SignalService();
export const healthService = new HealthService();
export const fflagsService = new FFlagsService();
export const assistStatsService = new AssistStatsService();
@ -54,6 +59,7 @@ export const integrationsService = new IntegrationsService();
export const searchService = new SearchService();
export const projectsService = new ProjectsService();
export const kaiService = new KaiService();
export const billingService = new BillingService();
export const services = [
projectsService,
@ -69,6 +75,7 @@ export const services = [
configService,
alertsService,
webhookService,
signalService,
healthService,
fflagsService,
assistStatsService,
@ -83,4 +90,5 @@ export const services = [
integrationsService,
searchService,
kaiService,
billingService,
];

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0.11 0.18 40.55 40.27">
<path d="M1.11279 0.182831C0.560508 0.182831 0.112793 0.630546 0.112793 1.18283C0.112793 1.73512 0.560508 2.18283 1.11279 2.18283L1.11279 0.182831ZM32.5924 40.1554C32.9829 40.5459 33.6161 40.5459 34.0066 40.1554L40.3706 33.7914C40.7611 33.4009 40.7611 32.7677 40.3706 32.3772C39.98 31.9867 39.3469 31.9867 38.9564 32.3772L33.2995 38.0341L27.6426 32.3772C27.2521 31.9867 26.619 31.9867 26.2284 32.3772C25.8379 32.7677 25.8379 33.4009 26.2284 33.7914L32.5924 40.1554ZM1.11279 2.18283C9.04936 2.18283 14.9162 3.5514 19.2628 5.7905C23.5955 8.02247 26.4705 11.1461 28.3935 14.7581C32.2781 22.0548 32.2995 31.3525 32.2995 39.4483H34.2995C34.2995 31.4709 34.3209 21.6359 30.1589 13.8182C28.0584 9.87288 24.8985 6.44392 20.1787 4.01254C15.4727 1.5883 9.26958 0.182831 1.11279 0.182831L1.11279 2.18283Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 906 B

View file

@ -0,0 +1,54 @@
<svg viewBox="0 0 46 46" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_66_7413)">
<rect width="24" height="30" transform="translate(13 16)" fill="white" fill-opacity="0.01"/>
<path d="M37.3252 19.0175H12.6756C11.8553 19.0175 11.343 19.9114 11.7549 20.6246L19.5428 33.8634V41.9192C19.5428 42.5119 20.0183 42.9907 20.6075 42.9907H29.3933C29.9825 42.9907 30.458 42.5119 30.458 41.9192V33.8634L38.2493 20.6246C38.6578 19.9114 38.1455 19.0175 37.3252 19.0175ZM28.0607 40.58H21.9401V35.3567H28.064V40.58H28.0607ZM28.3821 32.6581L28.064 33.2139H21.9368L21.6187 32.6581L14.9792 21.4282H35.0216L28.3821 32.6581Z" fill="url(#paint0_linear_66_7413)" fill-opacity="0.85"/>
</g>
<g clip-path="url(#clip1_66_7413)">
<rect width="16" height="16" transform="translate(0.957764 3.83905)" fill="white" fill-opacity="0.01"/>
<path d="M3.79422 12.8529L4.90769 12.3336L5.4269 13.4471C5.44049 13.4762 5.48019 13.4853 5.51579 13.4687L6.29263 13.1065C6.32823 13.0899 6.3468 13.0536 6.33246 13.0229L5.814 11.911L6.92746 11.3918C6.95659 11.3782 6.96566 11.3385 6.94906 11.3029L6.58682 10.5261C6.57021 10.4905 6.53397 10.4719 6.50322 10.4862L5.39138 11.0047L4.87216 9.89125C4.85858 9.86212 4.81888 9.85304 4.78327 9.86965L4.00644 10.2319C3.97083 10.2485 3.95227 10.2847 3.96585 10.3139L4.48507 11.4273L3.37161 11.9465C3.34247 11.9601 3.3334 11.9998 3.35 12.0354L3.71225 12.8123C3.72885 12.8479 3.76509 12.8664 3.79422 12.8529ZM9.1026 10.3775L12.2358 8.91647C12.265 8.90288 12.274 8.86319 12.2574 8.82758L11.8952 8.05075C11.8786 8.01514 11.8423 7.99658 11.8116 8.01091L8.67998 9.47121C8.65085 9.4848 8.64177 9.52449 8.65838 9.5601L9.02062 10.3369C9.03722 10.3725 9.07347 10.3911 9.1026 10.3775ZM11.1025 14.6663L14.2357 13.2052C14.2649 13.1917 14.2739 13.152 14.2573 13.1164L13.8951 12.3395C13.8785 12.3039 13.8422 12.2854 13.8115 12.2997L10.6799 13.76C10.6507 13.7736 10.6417 13.8133 10.6583 13.8489L11.0205 14.6257C11.0371 14.6613 11.0734 14.6799 11.1025 14.6663ZM11.8874 16.3494L15.0206 14.8884C15.0497 14.8748 15.0588 14.8351 15.0422 14.7995L14.6799 14.0227C14.6633 13.9871 14.6271 13.9685 14.5964 13.9828L11.4647 15.4431C11.4356 15.4567 11.4265 15.4964 11.4431 15.532L11.8054 16.3089C11.822 16.3445 11.8582 16.363 11.8874 16.3494ZM8.10884 16.5154L8.53405 14.8414C8.5644 14.7248 8.44652 14.6241 8.33971 14.6739L7.51755 15.0573C7.48033 15.0746 7.45227 15.1074 7.44396 15.1487L7.20764 16.0786L6.34344 15.6619C6.32521 15.653 6.30528 15.6482 6.28502 15.6478C6.26475 15.6474 6.24464 15.6514 6.22607 15.6595L5.4023 16.0436C5.29548 16.0934 5.29684 16.2485 5.40567 16.3002L6.96139 17.0505L6.52624 18.7666C6.49427 18.884 6.61216 18.9847 6.71897 18.9349L7.54112 18.5515C7.57835 18.5341 7.6064 18.5013 7.61472 18.46L7.86097 17.4881L8.76379 17.9242C8.80165 17.942 8.84556 17.9432 8.88116 17.9266L9.70331 17.5432C9.81013 17.4934 9.80877 17.3384 9.69994 17.2867L8.10884 16.5154Z" fill="url(#paint1_linear_66_7413)" fill-opacity="0.85"/>
</g>
<g clip-path="url(#clip2_66_7413)">
<rect width="15" height="15" transform="translate(16.9578 0.611725)" fill="white" fill-opacity="0.01"/>
<path d="M19.5945 4.88907H20.7664V11.3344H19.5945V4.88907ZM24.9998 7.52579H23.9158C23.9041 7.52579 23.8929 7.53042 23.8847 7.53866C23.8765 7.5469 23.8718 7.55808 23.8718 7.56973V8.65372C23.8718 8.66537 23.8765 8.67655 23.8847 8.68479C23.8929 8.69303 23.9041 8.69766 23.9158 8.69766H24.9998C25.0114 8.69766 25.0226 8.69303 25.0308 8.68479C25.0391 8.67655 25.0437 8.66537 25.0437 8.65372V7.56973C25.0437 7.55808 25.0391 7.5469 25.0308 7.53866C25.0226 7.53042 25.0114 7.52579 24.9998 7.52579V7.52579ZM28.1492 5.94376H29.321V10.2797H28.1492V5.94376Z" fill="#E2E4F6"/>
<path d="M30.2 4.94766H29.2332V3.54141C29.2332 3.47696 29.1804 3.42422 29.116 3.42422H28.3542C28.2898 3.42422 28.2371 3.47696 28.2371 3.54141V4.94766H27.2703C27.2058 4.94766 27.1531 5.0004 27.1531 5.06485V11.1586C27.1531 11.2231 27.2058 11.2758 27.2703 11.2758H28.2371V12.682C28.2371 12.7465 28.2898 12.7992 28.3542 12.7992H29.116C29.1804 12.7992 29.2332 12.7465 29.2332 12.682V11.2758H30.2C30.2644 11.2758 30.3171 11.2231 30.3171 11.1586V5.06485C30.3171 5.0004 30.2644 4.94766 30.2 4.94766ZM29.321 10.2797H28.1492V5.94376H29.321V10.2797ZM25.9226 6.52969H24.9558V4.01016C24.9558 3.94571 24.9031 3.89297 24.8386 3.89297H24.0769C24.0125 3.89297 23.9597 3.94571 23.9597 4.01016V6.52969H22.9929C22.9285 6.52969 22.8757 6.58243 22.8757 6.64688V9.57657C22.8757 9.64102 22.9285 9.69376 22.9929 9.69376H23.9597V12.2133C23.9597 12.2777 24.0125 12.3305 24.0769 12.3305H24.8386C24.9031 12.3305 24.9558 12.2777 24.9558 12.2133V9.69376H25.9226C25.9871 9.69376 26.0398 9.64102 26.0398 9.57657V6.64688C26.0398 6.58243 25.9871 6.52969 25.9226 6.52969ZM25.0437 8.65372C25.0437 8.66537 25.0391 8.67655 25.0308 8.68479C25.0226 8.69303 25.0114 8.69766 24.9998 8.69766H23.9158C23.9041 8.69766 23.8929 8.69303 23.8847 8.68479C23.8765 8.67655 23.8718 8.66537 23.8718 8.65372V7.56973C23.8718 7.55808 23.8765 7.5469 23.8847 7.53866C23.8929 7.53042 23.9041 7.52579 23.9158 7.52579H24.9998C25.0114 7.52579 25.0226 7.53042 25.0308 7.53866C25.0391 7.5469 25.0437 7.55808 25.0437 7.56973V8.65372ZM21.6453 3.89297H20.6785V3.07266C20.6785 3.00821 20.6257 2.95547 20.5613 2.95547H19.7996C19.7351 2.95547 19.6824 3.00821 19.6824 3.07266V3.89297H18.7156C18.6511 3.89297 18.5984 3.94571 18.5984 4.01016V12.2133C18.5984 12.2777 18.6511 12.3305 18.7156 12.3305H19.6824V13.1508C19.6824 13.2152 19.7351 13.268 19.7996 13.268H20.5613C20.6257 13.268 20.6785 13.2152 20.6785 13.1508V12.3305H21.6453C21.7097 12.3305 21.7625 12.2777 21.7625 12.2133V4.01016C21.7625 3.94571 21.7097 3.89297 21.6453 3.89297ZM20.7664 11.3344H19.5945V4.88907H20.7664V11.3344Z" fill="url(#paint2_linear_66_7413)" fill-opacity="0.85"/>
</g>
<g clip-path="url(#clip3_66_7413)">
<rect width="14.6667" height="16" transform="matrix(0 1 -1 0 45.321 5.93465)" fill="white" fill-opacity="0.01"/>
<g clip-path="url(#clip4_66_7413)">
<path d="M36.0824 6.26764C35.7036 6.26764 35.3402 6.41815 35.0723 6.68606C34.8044 6.95397 34.6539 7.31733 34.6539 7.69621C34.6539 8.07509 34.8044 8.43845 35.0723 8.70636C35.3402 8.97427 35.7036 9.12478 36.0824 9.12478C36.4613 9.12478 36.8247 8.97427 37.0926 8.70636C37.3605 8.43845 37.511 8.07509 37.511 7.69621C37.511 7.31733 37.3605 6.95397 37.0926 6.68606C36.8247 6.41815 36.4613 6.26764 36.0824 6.26764ZM41.0824 8.90157C40.7983 8.90157 40.5257 9.01445 40.3248 9.21538C40.1239 9.41631 40.011 9.68884 40.011 9.973C40.011 10.2572 40.1239 10.5297 40.3248 10.7306C40.5257 10.9315 40.7983 11.0444 41.0824 11.0444C41.3666 11.0444 41.6391 10.9315 41.84 10.7306C42.041 10.5297 42.1539 10.2572 42.1539 9.973C42.1539 9.68884 42.041 9.41631 41.84 9.21538C41.6391 9.01445 41.3666 8.90157 41.0824 8.90157ZM35.9931 12.4284C35.4248 12.4284 34.8798 12.6541 34.4779 13.056C34.0761 13.4578 33.8503 14.0029 33.8503 14.5712C33.8503 15.1395 34.0761 15.6846 34.4779 16.0864C34.8798 16.4883 35.4248 16.7141 35.9931 16.7141C36.5615 16.7141 37.1065 16.4883 37.5084 16.0864C37.9102 15.6846 38.136 15.1395 38.136 14.5712C38.136 14.0029 37.9102 13.4578 37.5084 13.056C37.1065 12.6541 36.5615 12.4284 35.9931 12.4284ZM43.0021 15.7319C42.6706 15.7319 42.3526 15.8636 42.1182 16.098C41.8838 16.3325 41.7521 16.6504 41.7521 16.9819C41.7521 17.3134 41.8838 17.6314 42.1182 17.8658C42.3526 18.1002 42.6706 18.2319 43.0021 18.2319C43.3336 18.2319 43.6515 18.1002 43.886 17.8658C44.1204 17.6314 44.2521 17.3134 44.2521 16.9819C44.2521 16.6504 44.1204 16.3325 43.886 16.098C43.6515 15.8636 43.3336 15.7319 43.0021 15.7319Z" fill="url(#paint3_linear_66_7413)" fill-opacity="0.85"/>
</g>
</g>
<defs>
<linearGradient id="paint0_linear_66_7413" x1="25.0011" y1="19.0175" x2="25.0011" y2="42.9907" gradientUnits="userSpaceOnUse">
<stop stop-color="#3ECEAC"/>
<stop offset="1" stop-color="#050EFF"/>
</linearGradient>
<linearGradient id="paint1_linear_66_7413" x1="5.93962" y1="6.43406" x2="11.977" y2="19.3813" gradientUnits="userSpaceOnUse">
<stop stop-color="#050EFF"/>
<stop offset="1" stop-color="#5AD4B9"/>
</linearGradient>
<linearGradient id="paint2_linear_66_7413" x1="24.4578" y1="2.95547" x2="24.4578" y2="13.268" gradientUnits="userSpaceOnUse">
<stop stop-color="#050EFF"/>
<stop offset="1" stop-color="#5AD3BA"/>
</linearGradient>
<linearGradient id="paint3_linear_66_7413" x1="45.9932" y1="11.2676" x2="30.2789" y2="11.2676" gradientUnits="userSpaceOnUse">
<stop stop-color="#050EFF"/>
<stop offset="1" stop-color="#5AD4B9"/>
</linearGradient>
<clipPath id="clip0_66_7413">
<rect width="24" height="30" fill="white" transform="translate(13 16)"/>
</clipPath>
<clipPath id="clip1_66_7413">
<rect width="16" height="16" fill="white" transform="translate(0.957764 3.83905)"/>
</clipPath>
<clipPath id="clip2_66_7413">
<rect width="15" height="15" fill="white" transform="translate(16.9578 0.611725)"/>
</clipPath>
<clipPath id="clip3_66_7413">
<rect width="14.6667" height="16" fill="white" transform="matrix(0 1 -1 0 45.321 5.93465)"/>
</clipPath>
<clipPath id="clip4_66_7413">
<rect width="14.6667" height="16" fill="white" transform="matrix(0 1 -1 0 45.321 5.93465)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 8.8 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-hand-thumbs-down" viewBox="0 0 16 16">
<path d="M8.864 15.674c-.956.24-1.843-.484-1.908-1.42-.072-1.05-.23-2.015-.428-2.59-.125-.36-.479-1.012-1.04-1.638-.557-.624-1.282-1.179-2.131-1.41C2.685 8.432 2 7.85 2 7V3c0-.845.682-1.464 1.448-1.546 1.07-.113 1.564-.415 2.068-.723l.048-.029c.272-.166.578-.349.97-.484C6.931.08 7.395 0 8 0h3.5c.937 0 1.599.478 1.934 1.064.164.287.254.607.254.913 0 .152-.023.312-.077.464.201.262.38.577.488.9.11.33.172.762.004 1.15.069.13.12.268.159.403.077.27.113.567.113.856 0 .289-.036.586-.113.856-.035.12-.08.244-.138.363.394.571.418 1.2.234 1.733-.206.592-.682 1.1-1.2 1.272-.847.283-1.803.276-2.516.211a9.877 9.877 0 0 1-.443-.05 9.364 9.364 0 0 1-.062 4.51c-.138.508-.55.848-1.012.964l-.261.065zM11.5 1H8c-.51 0-.863.068-1.14.163-.281.097-.506.229-.776.393l-.04.025c-.555.338-1.198.73-2.49.868-.333.035-.554.29-.554.55V7c0 .255.226.543.62.65 1.095.3 1.977.997 2.614 1.709.635.71 1.064 1.475 1.238 1.977.243.7.407 1.768.482 2.85.025.362.36.595.667.518l.262-.065c.16-.04.258-.144.288-.255a8.34 8.34 0 0 0-.145-4.726.5.5 0 0 1 .595-.643h.003l.014.004.058.013a8.912 8.912 0 0 0 1.036.157c.663.06 1.457.054 2.11-.163.175-.059.45-.301.57-.651.107-.308.087-.67-.266-1.021L12.793 7l.353-.354c.043-.042.105-.14.154-.315.048-.167.075-.37.075-.581 0-.211-.027-.414-.075-.581-.05-.174-.111-.273-.154-.315l-.353-.354.353-.354c.047-.047.109-.176.005-.488a2.224 2.224 0 0 0-.505-.804l-.353-.354.353-.354c.006-.005.041-.05.041-.17a.866.866 0 0 0-.121-.415C12.4 1.272 12.063 1 11.5 1z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-hand-thumbs-up" viewBox="0 0 16 16">
<path d="M8.864.046C7.908-.193 7.02.53 6.956 1.466c-.072 1.051-.23 2.016-.428 2.59-.125.36-.479 1.013-1.04 1.639-.557.623-1.282 1.178-2.131 1.41C2.685 7.288 2 7.87 2 8.72v4.001c0 .845.682 1.464 1.448 1.545 1.07.114 1.564.415 2.068.723l.048.03c.272.165.578.348.97.484.397.136.861.217 1.466.217h3.5c.937 0 1.599-.477 1.934-1.064a1.86 1.86 0 0 0 .254-.912c0-.152-.023-.312-.077-.464.201-.263.38-.578.488-.901.11-.33.172-.762.004-1.149.069-.13.12-.269.159-.403.077-.27.113-.568.113-.857 0-.288-.036-.585-.113-.856a2.144 2.144 0 0 0-.138-.362 1.9 1.9 0 0 0 .234-1.734c-.206-.592-.682-1.1-1.2-1.272-.847-.282-1.803-.276-2.516-.211a9.84 9.84 0 0 0-.443.05 9.365 9.365 0 0 0-.062-4.509A1.38 1.38 0 0 0 9.125.111L8.864.046zM11.5 14.721H8c-.51 0-.863-.069-1.14-.164-.281-.097-.506-.228-.776-.393l-.04-.024c-.555-.339-1.198-.731-2.49-.868-.333-.036-.554-.29-.554-.55V8.72c0-.254.226-.543.62-.65 1.095-.3 1.977-.996 2.614-1.708.635-.71 1.064-1.475 1.238-1.978.243-.7.407-1.768.482-2.85.025-.362.36-.594.667-.518l.262.066c.16.04.258.143.288.255a8.34 8.34 0 0 1-.145 4.725.5.5 0 0 0 .595.644l.003-.001.014-.003.058-.014a8.908 8.908 0 0 1 1.036-.157c.663-.06 1.457-.054 2.11.164.175.058.45.3.57.65.107.308.087.67-.266 1.022l-.353.353.353.354c.043.043.105.141.154.315.048.167.075.37.075.581 0 .212-.027.414-.075.582-.05.174-.111.272-.154.315l-.353.353.353.354c.047.047.109.177.005.488a2.224 2.224 0 0 1-.505.805l-.353.353.353.354c.006.005.041.05.041.17a.866.866 0 0 1-.121.416c-.165.288-.503.56-1.066.56z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tv-minimal-play"><path d="M10 7.75a.75.75 0 0 1 1.142-.638l3.664 2.249a.75.75 0 0 1 0 1.278l-3.664 2.25a.75.75 0 0 1-1.142-.64z"/><path d="M7 21h10"/><rect width="20" height="14" x="2" y="3" rx="2"/></svg>

After

Width:  |  Height:  |  Size: 408 B

View file

@ -0,0 +1,6 @@
<svg viewBox="0 0 19 19" xmlns="http://www.w3.org/2000/svg">
<g id="Frame">
<path id="Vector" d="M11.792 4.75C11.792 5.74456 11.3969 6.69839 10.6936 7.40165C9.99038 8.10491 9.03655 8.5 8.04199 8.5C7.04743 8.5 6.0936 8.10491 5.39034 7.40165C4.68708 6.69839 4.29199 5.74456 4.29199 4.75C4.29199 3.75544 4.68708 2.80161 5.39034 2.09835C6.0936 1.39509 7.04743 1 8.04199 1C9.03655 1 9.99038 1.39509 10.6936 2.09835C11.3969 2.80161 11.792 3.75544 11.792 4.75ZM8.04199 7.25C8.70503 7.25 9.34092 6.98661 9.80976 6.51777C10.2786 6.04893 10.542 5.41304 10.542 4.75C10.542 4.08696 10.2786 3.45107 9.80976 2.98223C9.34092 2.51339 8.70503 2.25 8.04199 2.25C7.37895 2.25 6.74307 2.51339 6.27423 2.98223C5.80538 3.45107 5.54199 4.08696 5.54199 4.75C5.54199 5.41304 5.80538 6.04893 6.27423 6.51777C6.74307 6.98661 7.37895 7.25 8.04199 7.25ZM8.36199 16C8.21859 15.5941 8.12254 15.173 8.07574 14.745H1.79199C1.79324 14.4375 1.98449 13.5125 2.83199 12.665C3.64699 11.85 5.18074 11 8.04199 11C8.36699 11 8.67574 11.0112 8.96699 11.0312C9.24949 10.605 9.58699 10.2188 9.97199 9.88375C9.38824 9.7975 8.74699 9.75 8.04199 9.75C1.79199 9.75 0.541992 13.5 0.541992 14.75C0.541992 16 1.79199 16 1.79199 16H8.36199Z" />
<path id="Vector_2" d="M18.042 14.125C18.042 15.2853 17.5811 16.3981 16.7606 17.2186C15.9401 18.0391 14.8273 18.5 13.667 18.5C12.5067 18.5 11.3939 18.0391 10.5734 17.2186C9.75293 16.3981 9.29199 15.2853 9.29199 14.125C9.29199 12.9647 9.75293 11.8519 10.5734 11.0314C11.3939 10.2109 12.5067 9.75 13.667 9.75C14.8273 9.75 15.9401 10.2109 16.7606 11.0314C17.5811 11.8519 18.042 12.9647 18.042 14.125ZM13.667 11.625C13.5012 11.625 13.3423 11.6908 13.225 11.8081C13.1078 11.9253 13.042 12.0842 13.042 12.25V14.125C13.042 14.2908 13.1078 14.4497 13.225 14.5669C13.3423 14.6842 13.5012 14.75 13.667 14.75C13.8328 14.75 13.9917 14.6842 14.1089 14.5669C14.2261 14.4497 14.292 14.2908 14.292 14.125V12.25C14.292 12.0842 14.2261 11.9253 14.1089 11.8081C13.9917 11.6908 13.8328 11.625 13.667 11.625ZM13.667 16.625C13.8328 16.625 13.9917 16.5592 14.1089 16.4419C14.2261 16.3247 14.292 16.1658 14.292 16C14.292 15.8342 14.2261 15.6753 14.1089 15.5581C13.9917 15.4408 13.8328 15.375 13.667 15.375C13.5012 15.375 13.3423 15.4408 13.225 15.5581C13.1078 15.6753 13.042 15.8342 13.042 16C13.042 16.1658 13.1078 16.3247 13.225 16.4419C13.3423 16.5592 13.5012 16.625 13.667 16.625Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -38,6 +38,7 @@ export function debounceCall(func, wait) {
};
}
export function randomInt(a, b) {
const min = (b ? a : 0) - 0.5;
const max = b || a || Number.MAX_SAFE_INTEGER;
@ -623,6 +624,152 @@ export function exportAntCsv(tableColumns, tableData, filename = 'table.csv') {
saveAsFile(blob, filename);
}
export const numFormatter = (num: any) => {
if (num > 999 && num < 1000000) {
return num / 1000 + 'K';
} else if (num >= 1000000) {
return num / 1000000 + 'M';
} else if (num < 900) {
return num;
}
};
export const loadStripe = (callback: any) => {
loadDynamicScript('stripeTag', 'https://js.stripe.com/v3/', callback);
};
export const loadDynamicScript = (tag: string, path: string, callback: any) => {
const scriptExists = document.getElementById(tag)
if (scriptExists) return callback();
const scriptJs = document.createElement('script');
scriptJs.src = path;
scriptJs.async = true;
scriptJs.id = tag;
scriptJs.onload = () => {
setTimeout(callback, 10);
};
document.body && document.body.appendChild(scriptJs);
}
export const getDeviceType = () => {
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
const isTablet = /iPad|Android/i.test(navigator.userAgent) && !/Mobile/i.test(navigator.userAgent);
if (isMobile) {
return 2;
} else if (isTablet) {
return 0;
} else {
return 1;
}
}
export const sensitiveParams = new Set([
'password',
'pass',
'pwd',
'mdp',
'token',
'bearer',
'jwt',
'api_key',
'api-key',
'apiKey',
'key',
'secret',
'id',
'user',
'userId',
'email',
'ssn',
'name',
'firstname',
'lastname',
'birthdate',
'dob',
'address',
'zip',
'zipcode',
'x-api-key',
'www-authenticate',
'x-csrf-token',
'x-requested-with',
'x-forwarded-for',
'x-real-ip',
'cookie',
'authorization',
'auth',
'proxy-authorization',
'set-cookie',
'account_key',
])
export function filterHeaders(headers: Record<string, string> | { name: string; value: string }[]) {
const filteredHeaders: Record<string, string> = {}
if (Array.isArray(headers)) {
headers.forEach(({ name, value }) => {
if (sensitiveParams.has(name.toLowerCase())) {
filteredHeaders[name] = '******'
} else {
filteredHeaders[name] = value
}
})
} else {
for (const [key, value] of Object.entries(headers)) {
if (sensitiveParams.has(key.toLowerCase())) {
filteredHeaders[key] = '******'
} else {
filteredHeaders[key] = value
}
}
}
return filteredHeaders
}
export function filterBody<T extends Record<string, any>>(body: T): T {
if (!body) {
return body
}
obscureSensitiveData(body)
return body
}
export function obscureSensitiveData(obj: Record<string, any> | any[]) {
if (Array.isArray(obj)) {
obj.forEach(obscureSensitiveData)
} else if (obj && typeof obj === 'object') {
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
if (sensitiveParams.has(key.toLowerCase())) {
obj[key] = '******'
} else if (obj[key] !== null && typeof obj[key] === 'object') {
obscureSensitiveData(obj[key])
}
}
}
}
}
export function tryFilterUrl(url: string) {
if (!url) return ''
try {
const urlObj = new URL(url)
if (urlObj.searchParams) {
for (const key of urlObj.searchParams.keys()) {
if (sensitiveParams.has(key.toLowerCase())) {
urlObj.searchParams.set(key, '******')
}
}
}
return urlObj.toString()
} catch (e) {
return url
}
}
export function roundToNextMinutes(timestamp: number, minutes: number): number {
const date = new Date(timestamp);
date.setSeconds(0, 0);

View file

@ -0,0 +1,14 @@
export const CLIENT_TABS = {
INTEGRATIONS: 'integrations',
PROFILE: 'account',
SESSIONS_LISTING: 'sessions-listing',
MANAGE_USERS: 'team',
MANAGE_ROLES: 'roles',
SITES: 'projects',
CUSTOM_FIELDS: 'metadata',
WEBHOOKS: 'webhooks',
NOTIFICATIONS: 'notifications',
AUDIT: 'audit',
BILLING: '$_this_url_does_not_exist',
MODULES: 'modules',
};