change(ui): module settings and nav changes (#1443)

* change(ui): route refactor

* change(ui): new navigation

* change(ui): new navigation - icons and other fixes

* change(ui): modules

* change(ui): moduels and nav fixes
This commit is contained in:
Shekar Siri 2023-08-09 12:07:57 +05:30 committed by GitHub
parent a01a66afeb
commit 8d1bf1a401
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 795 additions and 307 deletions

View file

@ -17,6 +17,58 @@ def __generate_invitation_token():
return secrets.token_urlsafe(64)
def get_user_settings(user_id):
# read user settings from users.settings:jsonb column
with pg_client.PostgresClient() as cur:
cur.execute(
cur.mogrify(
f"""SELECT
settings
FROM public.users
WHERE users.deleted_at IS NULL
AND users.user_id=%(user_id)s
LIMIT 1""",
{"user_id": user_id})
)
return helper.dict_to_camel_case(cur.fetchone())
def update_user_module(user_id, module):
# example data = {"settings": {"modules": ['ASSIST', 'METADATA']}
# update user settings from users.settings:jsonb column only update settings.modules
# if module property is not exists, it will be created
# if module property exists, it will be updated, modify here and call update_user_settings
# module is a single element to be added or removed
settings = get_user_settings(user_id)["settings"]
if settings is None:
settings = {}
if settings.get("modules") is None:
settings["modules"] = []
if module in settings["modules"]:
settings["modules"].remove(module)
else:
settings["modules"].append(module)
return update_user_settings(user_id, settings)
def update_user_settings(user_id, settings):
# update user settings from users.settings:jsonb column
with pg_client.PostgresClient() as cur:
cur.execute(
cur.mogrify(
f"""UPDATE public.users
SET settings = %(settings)s
WHERE users.user_id = %(user_id)s
AND deleted_at IS NULL
RETURNING settings;""",
{"user_id": user_id, "settings": settings})
)
return helper.dict_to_camel_case(cur.fetchone())
def create_new_member(email, invitation_token, admin, name, owner=False):
with pg_client.PostgresClient() as cur:
query = cur.mogrify(f"""\
@ -230,7 +282,8 @@ def get(user_id, tenant_id):
(CASE WHEN role = 'owner' THEN TRUE ELSE FALSE END) AS super_admin,
(CASE WHEN role = 'admin' THEN TRUE ELSE FALSE END) AS admin,
(CASE WHEN role = 'member' THEN TRUE ELSE FALSE END) AS member,
TRUE AS has_password
TRUE AS has_password,
settings
FROM public.users LEFT JOIN public.basic_authentication ON users.user_id=basic_authentication.user_id
WHERE
users.user_id = %(userId)s
@ -548,7 +601,7 @@ def auth_exists(user_id, tenant_id, jwt_iat, jwt_aud):
WHERE user_id = %(userId)s
AND deleted_at IS NULL
LIMIT 1;""",
{"userId": user_id})
{"userId": user_id})
)
r = cur.fetchone()
return r is not None \
@ -566,7 +619,7 @@ def change_jwt_iat(user_id):
SET jwt_iat = timezone('utc'::text, now())
WHERE user_id = %(user_id)s
RETURNING jwt_iat;""",
{"user_id": user_id})
{"user_id": user_id})
cur.execute(query)
return cur.fetchone().get("jwt_iat")

View file

@ -655,6 +655,12 @@ def generate_new_tenant_token(context: schemas.CurrentContext = Depends(OR_conte
}
@app.post('/users/modules', tags=['users'])
def update_user_module(context: schemas.CurrentContext = Depends(OR_context),
data: schemas.ModuleStatus = Body(...)):
return users.update_user_module(context.user_id, data)
@app.get('/notifications', tags=['notifications'])
def get_notifications(context: schemas.CurrentContext = Depends(OR_context)):
return {"data": notifications.get_all(tenant_id=context.tenant_id, user_id=context.user_id)}

View file

@ -1036,11 +1036,11 @@ class __CardSchema(BaseModel):
class CardSchema(__CardSchema, CardChartSchema):
view_type: Union[MetricTimeseriesViewType, \
MetricTableViewType, MetricOtherViewType] = Field(...)
MetricTableViewType, MetricOtherViewType] = Field(...)
metric_type: MetricType = Field(...)
metric_of: Union[MetricOfTimeseries, MetricOfTable, MetricOfErrors, \
MetricOfPerformance, MetricOfResources, MetricOfWebVitals, \
MetricOfClickMap] = Field(default=MetricOfTable.user_id)
MetricOfPerformance, MetricOfResources, MetricOfWebVitals, \
MetricOfClickMap] = Field(default=MetricOfTable.user_id)
metric_value: List[IssueType] = Field(default=[])
is_template: bool = Field(default=False)
@ -1212,7 +1212,7 @@ class LiveSessionSearchFilterSchema(BaseModel):
type: LiveFilterType = Field(...)
source: Optional[str] = Field(default=None)
operator: Literal[SearchEventOperator._is, \
SearchEventOperator._contains] = Field(default=SearchEventOperator._contains)
SearchEventOperator._contains] = Field(default=SearchEventOperator._contains)
transform = root_validator(pre=True, allow_reuse=True)(transform_old_FilterType)
@ -1434,6 +1434,15 @@ class FeatureFlagStatus(BaseModel):
alias_generator = attribute_to_camel_case
class ModuleStatus(BaseModel):
module: str = Field(..., description="Possible values: notes, bugs, live",
regex="^(assist|notes|bug_reports|sessions|alerts)$")
status: bool = Field(...)
class Config:
alias_generator = attribute_to_camel_case
class FeatureFlagSchema(BaseModel):
payload: Optional[str] = Field(default=None)
flag_key: str = Field(..., regex=r'^[a-zA-Z0-9\-]+$')

View file

@ -120,10 +120,6 @@ const Router: React.FC<RouterProps> = (props) => {
match: { params: { siteId: siteIdFromPath } }
} = props;
useEffect(() => {
console.log('siteId from router', siteId);
}, [siteId]);
useEffect(() => {
const fetchInitialData = async () => {
const siteIdFromPath = parseInt(window.location.pathname.split('/')[1]);
@ -193,8 +189,8 @@ const Router: React.FC<RouterProps> = (props) => {
const redirectToOnboarding = !onboarding && localStorage.getItem(GLOBAL_HAS_NO_RECORDINGS) === 'true';
return isLoggedIn ? (
<Layout hideHeader={hideHeader} siteId={siteId}>
<ModalProvider>
<ModalProvider>
<Layout hideHeader={hideHeader} siteId={siteId}>
<Loader loading={loading} className='flex-1'>
<Notification />
@ -276,8 +272,8 @@ const Router: React.FC<RouterProps> = (props) => {
</Suspense>
</Loader>
{!isEnterprise && !isPlayer && <SupportCallout />}
</ModalProvider>
</Layout>
</Layout>
</ModalProvider>
) :
<PublicRoutes />;
};

View file

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-slack" viewBox="0 0 16 16">
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-slack" viewBox="0 0 16 16">
<path d="M3.362 10.11c0 .926-.756 1.681-1.681 1.681S0 11.036 0 10.111C0 9.186.756 8.43 1.68 8.43h1.682v1.68zm.846 0c0-.924.756-1.68 1.681-1.68s1.681.756 1.681 1.68v4.21c0 .924-.756 1.68-1.68 1.68a1.685 1.685 0 0 1-1.682-1.68v-4.21zM5.89 3.362c-.926 0-1.682-.756-1.682-1.681S4.964 0 5.89 0s1.68.756 1.68 1.68v1.682H5.89zm0 .846c.924 0 1.68.756 1.68 1.681S6.814 7.57 5.89 7.57H1.68C.757 7.57 0 6.814 0 5.89c0-.926.756-1.682 1.68-1.682h4.21zm6.749 1.682c0-.926.755-1.682 1.68-1.682.925 0 1.681.756 1.681 1.681s-.756 1.681-1.68 1.681h-1.681V5.89zm-.848 0c0 .924-.755 1.68-1.68 1.68A1.685 1.685 0 0 1 8.43 5.89V1.68C8.43.757 9.186 0 10.11 0c.926 0 1.681.756 1.681 1.68v4.21zm-1.681 6.748c.926 0 1.682.756 1.682 1.681S11.036 16 10.11 16s-1.681-.756-1.681-1.68v-1.682h1.68zm0-.847c-.924 0-1.68-.755-1.68-1.68 0-.925.756-1.681 1.68-1.681h4.21c.924 0 1.68.756 1.68 1.68 0 .926-.756 1.681-1.68 1.681h-4.21z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 990 B

View file

@ -5,6 +5,9 @@ import { useModal } from 'App/components/Modal';
import AlertTriggersModal from 'Shared/AlertTriggersModal';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { Badge, Button } from 'antd';
import { BellOutlined } from '@ant-design/icons';
const AUTOREFRESH_INTERVAL = 5 * 60 * 1000;
@ -22,18 +25,16 @@ function Notifications() {
}, []);
return (
<Tooltip title={`Alerts`}>
<div
className={stl.button}
onClick={() => showModal(<AlertTriggersModal />, { right: true })}
>
<div className={stl.counter} data-hidden={count === 0}>
{count}
</div>
<Icon name="bell" size="18" color="gray-dark" />
</div>
</Tooltip>
<Badge dot={count > 0} size='small'>
<Tooltip title='Alerts'>
<Button
icon={<BellOutlined />}
onClick={() => showModal(<AlertTriggersModal />, { right: true })}>
{/*<Icon name='bell' size='18' color='gray-dark' />*/}
</Button>
</Tooltip>
</Badge>
);
}
export default observer(Notifications)
export default observer(Notifications);

View file

@ -29,7 +29,7 @@ function AuditView() {
}
return useObserver(() => (
<div>
<div className="bg-white rounded-lg">
<div className="flex items-center mb-4 px-5 pt-5">
<PageTitle title={
<div className="flex items-center">

View file

@ -16,6 +16,7 @@ import PreferencesMenu from './PreferencesMenu';
import Notifications from './Notifications';
import Roles from './Roles';
import SessionsListingSettings from 'Components/Client/SessionsListingSettings';
import Modules from 'Components/Client/Modules';
@withRouter
export default class Client extends React.PureComponent {
@ -39,6 +40,7 @@ export default class Client extends React.PureComponent {
<Route exact strict path={clientRoute(CLIENT_TABS.NOTIFICATIONS)} component={Notifications} />
<Route exact strict path={clientRoute(CLIENT_TABS.MANAGE_ROLES)} component={Roles} />
<Route exact strict path={clientRoute(CLIENT_TABS.AUDIT)} component={AuditView} />
<Route exact strict path={clientRoute(CLIENT_TABS.MODULES)} component={Modules} />
<Redirect to={clientRoute(CLIENT_TABS.PROFILE)} />
</Switch>
);
@ -46,20 +48,13 @@ export default class Client extends React.PureComponent {
render() {
const {
match: {
params: { activeTab },
},
params: { activeTab }
}
} = this.props;
return (
// <div className={cn('page-margin container-90 flex relative')}>
// <div className={styles.tabMenu}>
// <PreferencesMenu activeTab={activeTab} />
// </div>
// <div className={cn('side-menu-margined w-full')}>
<div className="bg-white w-full rounded-lg mx-auto mb-8 border" style={{ maxWidth: '1300px'}}>
{activeTab && this.renderActiveTab()}
</div>
// </div>
// </div>
<div className='w-full mx-auto mb-8' style={{ maxWidth: '1300px' }}>
{activeTab && this.renderActiveTab()}
</div>
);
}
}

View file

@ -68,8 +68,8 @@ function CustomFields(props) {
const { fields, loading } = props;
return (
<div>
<div className={cn(styles.tabHeader, 'px-5 pt-5')}>
<div className="p-5 bg-white rounded-lg">
<div className={cn(styles.tabHeader)}>
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3>
<div style={{ marginRight: '15px' }}>
<SiteDropdown value={currentSite && currentSite.id} onChange={onChangeSelect} />

View file

@ -0,0 +1,32 @@
import React from 'react';
import { Icon } from 'UI';
import { Switch } from 'antd';
import { Module } from 'Components/Client/Modules/index';
interface Props {
module: Module;
onToggle: (module: Module) => void;
}
function ModuleCard(props: Props) {
const { module } = props;
return (
<div className='bg-white rounded-lg border p-4 flex h-full'>
<div className='w-10 shrink-0 pt-1'>
<Icon name={module.icon} size={20} color='gray-darkest' />
</div>
<div className='flex flex-col flex-grow'>
<div className='flex flex-col flex-grow'>
<h3 className='text-lg font-medium'>{module.label}</h3>
<p className='flex-grow'>{module.description}</p>
</div>
<div className='flex items-end'>
<Switch size='small' checked={module.isEnabled} title={module.isEnabled ? 'A' : 'B'} />
</div>
</div>
</div>
);
}
export default ModuleCard;

View file

@ -0,0 +1,42 @@
import React from 'react';
import ModuleCard from 'Components/Client/Modules/ModuleCard';
import { modules as list } from './';
import withPageTitle from 'HOCs/withPageTitle';
import { connect } from 'react-redux';
interface Props {
modules: string[];
}
function Modules(props: Props) {
const { modules } = props;
const onToggle = (module: any) => {
module.isEnabled = !module.isEnabled;
};
return (
<div>
<div className='bg-white rounded-lg border p-4'>
<h3 className='text-2xl'>Modules</h3>
<div className='mt-3'>
<p>Copy
OpenReplay's modules are a collection of advanced features that provide enhanced functionality.</p>
<p>Easily enable any desired module within the user interface to access its capabilities</p>
</div>
</div>
<div className='mt-4 grid grid-cols-3 gap-3'>
{list.map((module) => (
<div key={module.key} className='flex flex-col h-full'>
<ModuleCard module={module} onToggle={onToggle} />
</div>
))}
</div>
</div>
);
}
export default withPageTitle('Modules - OpenReplay Preferences')(connect((state: any) => ({
modules: state.getIn(['user', 'account', 'modules']) || []
}))(Modules));

View file

@ -0,0 +1,51 @@
export { default } from './Modules';
export const enum MODULES {
ASSIST = 'assist',
NOTES = 'notes',
BUG_REPORTS = 'bug-reports',
OFFLINE_RECORDINGS = 'offline-recordings',
ALERTS = 'alerts',
}
export interface Module {
label: string;
description: string;
key: string;
icon?: string;
isEnabled?: boolean;
}
export const modules = [
{
label: 'Assist',
description: 'Record and replay user sessions to see a video of what users did on your website.',
key: MODULES.ASSIST,
icon: 'broadcast'
},
{
label: 'Notes',
description: 'Add notes to sessions and recordings to share with your team.',
key: MODULES.NOTES,
icon: 'stickies',
isEnabled: true
},
{
label: 'Bug Reports',
description: 'Allow users to report bugs and issues on your website.',
key: MODULES.BUG_REPORTS,
icon: 'filetype-pdf'
},
{
label: 'Offline Recordings',
description: 'Record user sessions even when they are offline.',
key: MODULES.OFFLINE_RECORDINGS,
icon: 'record2'
},
{
label: 'Alerts',
description: 'Get notified when users encounter errors on your website.',
key: MODULES.ALERTS,
icon: 'bell'
}
];

View file

@ -20,7 +20,7 @@ function Notifications() {
};
return (
<div className="p-5">
<div className="bg-white rounded-lg p-5">
<div className={stl.tabHeader}>{<h3 className={cn(stl.tabTitle, 'text-2xl')}>{'Notifications'}</h3>}</div>
<div className="">
<div className="text-lg font-medium">Weekly project summary</div>

View file

@ -19,7 +19,7 @@ export default class ProfileSettings extends React.PureComponent {
render() {
const { account, isEnterprise } = this.props;
return (
<div className="p-5">
<div className="bg-white rounded-lg p-5">
<PageTitle title={<div>Account</div>} />
<div className="flex items-center">
<div className={styles.left}>

View file

@ -65,7 +65,7 @@ function Roles(props: Props) {
return (
<React.Fragment>
<Loader loading={loading}>
<div className={stl.wrapper}>
<div className="bg-white rounded-lg">
<div className={cn(stl.tabHeader, 'flex items-center')}>
<div className="flex items-center mr-auto px-5 pt-5">
<h3 className={cn(stl.tabTitle, 'text-2xl')}>Roles and Access</h3>

View file

@ -19,7 +19,7 @@ const connector = connect(mapStateToProps);
function SessionsListingSettings(props: Props) {
return (
<div className='p-5'>
<div className='bg-white rounded-lg p-5'>
<PageTitle title={<div>Sessions Listing</div>} />
<div className='flex flex-col mt-4'>

View file

@ -132,7 +132,7 @@ const Sites = ({
return (
<Loader loading={loading}>
<div className={stl.wrapper}>
<div className="bg-white rounded-lg">
<div className={cn(stl.tabHeader, 'px-5 pt-5')}>
<PageTitle
title={<div className='mr-4'>Projects</div>}

View file

@ -36,7 +36,7 @@ function UsersView(props: Props) {
}, []);
return (
<div>
<div className="bg-white rounded-lg">
<div className="flex items-center justify-between px-5 pt-5">
<PageTitle
title={

View file

@ -44,8 +44,8 @@ function Webhooks() {
};
return (
<div>
<div className={cn(styles.tabHeader, 'px-5 pt-5')}>
<div className="p-5 bg-white rounded-lg">
<div className={cn(styles.tabHeader)}>
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Webhooks'}</h3>
<Button className="ml-auto" variant="primary" onClick={() => init()}>Add Webhook</Button>
</div>

View file

@ -6,6 +6,8 @@ import HealthWidget from 'Components/Header/HealthStatus/HealthWidget';
import { getHealthRequest } from './getHealth';
import UserMenu from 'Components/Header/UserMenu/UserMenu';
import { Popover } from 'antd';
import { Button } from 'antd';
import { ExclamationCircleOutlined } from '@ant-design/icons';
export interface IServiceStats {
name: 'backendServices' | 'databases' | 'ingestionPipeline' | 'SSL';
@ -68,15 +70,16 @@ function HealthStatus() {
isError={isError}
/>
}>
<div
className={
'rounded cursor-pointer flex items-center h-full'
}
>
<div className={'rounded p-2 border border-light-gray bg-white flex items-center'}>
<Icon name={icon} size={18} />
</div>
</div>
<Button icon={<ExclamationCircleOutlined />}></Button>
{/*<div*/}
{/* className={*/}
{/* 'rounded cursor-pointer flex items-center h-full'*/}
{/* }*/}
{/*>*/}
{/* <div className={'rounded p-2 border border-light-gray bg-white flex items-center'}>*/}
{/* <Icon name={icon} size={18} />*/}
{/* </div>*/}
{/*</div>*/}
</Popover>
{showModal ? (
<HealthModal

View file

@ -31,7 +31,7 @@ function HealthWidget({
}, [lastAsked]);
const title = !isError && healthOk ? 'All Systems Operational' : 'Service disruption';
const icon = !isError && healthOk ? ('check-circle-fill' as const) : ('exclamation-circle-fill' as const);
const icon = !isError && healthOk ? ('check-circle-fill' as const) : ('ic-errors' as const);
const problematicServices = Object.values(healthResponse?.healthMap || {}).filter(
(service: Record<string, any>) => !service.healthOk
@ -50,12 +50,12 @@ function HealthWidget({
<div
className={cn(
'p-2 gap-2 w-full font-semibold flex items-center rounded',
healthOk
!isError && healthOk
? 'color-green bg-figmaColors-secondary-outlined-hover-background'
: 'bg-red-lightest'
: 'bg-red-lightest color-red'
)}
>
<Icon name={icon} size={16} color={'green'} />
<Icon name={icon} size={16} color={!isError && healthOk ? 'green': 'red'} />
<span>{title}</span>
</div>
<div className={'text-secondary flex w-full justify-between items-center text-sm'}>
@ -70,9 +70,9 @@ function HealthWidget({
{isError && <div className={'text-secondary text-sm'}>Error getting service health status</div>}
<div className={'w-full'}>
<div className={'font-semibold'}>Captured in total</div>
<div>Sessions: {healthResponse.details?.numberOfSessionsCaptured.toLocaleString()}</div>
<div>Events: {healthResponse.details?.numberOfEventCaptured.toLocaleString()}</div>
<div className={'font-semibold'}>Captured</div>
<div>{healthResponse.details?.numberOfSessionsCaptured.toLocaleString()} Sessions</div>
<div>{healthResponse.details?.numberOfEventCaptured.toLocaleString()} Events</div>
</div>
<div className={'w-full'}>
{!isError && !healthOk ? (

View file

@ -23,6 +23,7 @@ function SubHeader(props) {
const { player, store } = React.useContext(PlayerContext);
const { width, height, endTime, location: currentLocation = 'loading...', } = store.get();
const enabledIntegration = useMemo(() => {
const { integrations } = props;
if (!integrations || !integrations.size) {
@ -160,4 +161,5 @@ function SubHeader(props) {
export default connect((state) => ({
siteId: state.getIn(['site', 'siteId']),
integrations: state.getIn(['issues', 'list']),
modules: state.getIn(['user', 'account', 'modules']) || [],
}))(observer(SubHeader));

View file

@ -0,0 +1,108 @@
import React from 'react';
import { connect } from 'react-redux';
import { Button, Divider, Dropdown, Menu, Space, Typography } from 'antd';
import { CaretDownOutlined, FolderAddOutlined, FolderOutlined } from '@ant-design/icons';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { hasSiteId, siteChangeAvailable } from 'App/routes';
import { setSiteId } from 'Duck/site';
import { fetchListActive as fetchIntegrationVariables } from 'Duck/customField';
import { clearSearch } from 'Duck/search';
import { clearSearch as clearSearchLive } from 'Duck/liveSearch';
import { useModal } from 'Components/Modal';
import { init as initProject } from 'Duck/site';
import NewSiteForm from 'Components/Client/Sites/NewSiteForm';
import { withStore } from 'App/mstore';
const { Text } = Typography;
interface Site {
id: string;
host: string;
}
interface Props extends RouteComponentProps {
sites: Site[];
siteId: string;
setSiteId: (siteId: string) => void;
fetchIntegrationVariables: () => void;
clearSearch: (isSession: boolean) => void;
clearSearchLive: () => void;
initProject: (data: any) => void;
mstore: any;
account: any;
}
function ProjectDropdown(props: Props) {
const { sites, siteId, location, account } = props;
const isAdmin = account.admin || account.superAdmin;
const activeSite = sites.find((s) => s.id === siteId);
const showCurrent = hasSiteId(location.pathname) || siteChangeAvailable(location.pathname);
const { showModal, hideModal } = useModal();
console.log('activeSite', activeSite);
const handleSiteChange = (newSiteId: string) => {
props.setSiteId(newSiteId); // Fixed: should set the new siteId, not the existing one
props.fetchIntegrationVariables();
props.clearSearch(location.pathname.includes('/sessions'));
props.clearSearchLive();
props.mstore.initClient();
};
const addProjectClickHandler = () => {
props.initProject({});
showModal(<NewSiteForm onClose={hideModal} />, { right: true });
};
const menu = (
<Menu>
{isAdmin && (
<>
<Menu.Item icon={<FolderAddOutlined />} key='all-projects' onClick={addProjectClickHandler}>
Add Project
</Menu.Item>
<Divider style={{ margin: 0 }} />
</>
)}
{sites.map((site) => (
<Menu.Item
icon={<FolderOutlined />}
key={site.id}
onClick={() => handleSiteChange(site.id)}
className='px-3 py-2'
>
{site.host}
</Menu.Item>
))}
</Menu>
);
return (
<Dropdown overlay={menu} placement='bottomLeft'>
<Button >
<Space>
{showCurrent && activeSite ? activeSite.host : 'All Projects'}
<CaretDownOutlined />
</Space>
</Button>
</Dropdown>
);
}
const mapStateToProps = (state: any) => ({
sites: state.getIn(['site', 'list']),
siteId: state.getIn(['site', 'siteId']),
account: state.getIn(['user', 'account'])
});
export default withRouter(
connect(mapStateToProps, {
setSiteId,
fetchIntegrationVariables,
clearSearch,
clearSearchLive,
initProject
})(withStore(ProjectDropdown))
);

View file

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

View file

@ -2,21 +2,21 @@ import React from 'react';
import { connect } from 'react-redux';
import Select from 'Shared/Select';
const SiteDropdown = ({ contextName="", sites, onChange, value }) => {
const options = sites.map(site => ({ value: site.id, label: site.host })).toJS();
const SiteDropdown = ({ contextName = '', sites, onChange, value }) => {
const options = sites.map(site => ({ value: site.id, label: site.host })).toJS();
return (
<Select
name={ `${ contextName }_site` }
placeholder="Select Site"
options={ options }
value={ options.find(option => option.value === value) }
onChange={ onChange }
/>
);
}
<Select
name={`${contextName}_site`}
placeholder='Select Site'
options={options}
value={options.find(option => option.value === value)}
onChange={onChange}
/>
);
};
SiteDropdown.displayName = "SiteDropdown";
SiteDropdown.displayName = 'SiteDropdown';
export default connect(state => ({
sites: state.getIn([ 'site', 'list' ]),
sites: state.getIn(['site', 'list'])
}))(SiteDropdown);

View file

@ -1,19 +1,17 @@
import React from 'react';
import cn from 'classnames';
import { avatarIconName } from 'App/iconNames';
import stl from './avatar.module.css';
import { Icon, Tooltip } from 'UI';
const Avatar = ({
isActive = false,
isAssist = false,
width = '38px',
height = '38px',
iconSize = 26,
seed,
}) => {
isActive = false,
isAssist = false,
width = '38px',
height = '38px',
iconSize = 26,
seed
}) => {
var iconName = avatarIconName(seed);
console.log('iconName', iconName)
return (
<Tooltip title={isActive ? 'Active user' : 'User might be inactive'} disabled={!isAssist}>
<div
@ -23,16 +21,16 @@ const Avatar = ({
)}
// style={{ width, height }}
>
<Icon name={iconName} size={iconSize} color="tealx" />
<Icon name={iconName} size={iconSize} color='tealx' />
{isAssist && (
<div
className={cn('w-2 h-2 rounded-full absolute right-0 bottom-0', {
'bg-green': isActive,
'bg-orange': !isActive,
'bg-orange': !isActive
})}
style={{ marginRight: '3px', marginBottom: '3px' }}
>
{isActive ? null : <Icon name={'sleep'} size={9} style={{ position: 'absolute', right: -6, top: -3 }} />}
{isActive ? null : <Icon name={'sleep'} size={9} style={{ position: 'absolute', right: -6, top: -3 }} />}
</div>
)}
</div>

File diff suppressed because one or more lines are too long

View file

@ -6,23 +6,26 @@ import PromiseErrorButton from './PromiseErrorButton';
import EvalErrorBtn from './EvalErrorBtn';
import InternalErrorButton from './InternalErrorButton';
import { options } from '../console';
import UserMenu from 'Components/Header/UserMenu/UserMenu';
import { Popover, Button } from 'antd';
import { BugOutlined } from '@ant-design/icons';
export default function ErrorGenPanel() {
if (window.env.PRODUCTION && !options.enableCrash) return null;
return (
<Popover content={
<div className='flex flex-col gap-3'>
<CrashReactAppButton />
<EventErrorButton />
<MemoryCrushButton />
<PromiseErrorButton />
<EvalErrorBtn />
<InternalErrorButton />
</div>
}>
<Button danger type="primary" className="ml-3">Show buttons</Button>
<Popover
content={
<div className='flex flex-col gap-3'>
<CrashReactAppButton />
<EventErrorButton />
<MemoryCrushButton />
<PromiseErrorButton />
<EvalErrorBtn />
<InternalErrorButton />
</div>
}
placement={'topRight'}
>
<Button danger type='primary' className='ml-3' icon={<BugOutlined />}></Button>
</Popover>
);
}

View file

@ -8,17 +8,29 @@ import Router from './Router';
import { StoreProvider, RootStore } from './mstore';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { DndProvider } from 'react-dnd';
import { ConfigProvider } from 'antd';
import { ConfigProvider, theme } from 'antd';
import colors from 'App/theme/colors';
// @ts-ignore
window.getCommitHash = () => console.log(window.env.COMMIT_HASH);
// Custom theme configuration
const customTheme = {
'@primary-color': 'red', // Change the primary color to red
'@text-color': 'red', // Change the default text color to red
'@font-size-base': '20px', // Change the base font size
// Add more custom variables as needed...
// algorithm: theme.darkAlgorithm,
token: {
// Seed Token
colorPrimary: colors.teal,
colorPrimaryActive: '#394EFF',
colorSecondary: '#3EAAAF',
colorBgLayout: colors['gray-lightest'],
colorBgContainer: colors['white'],
borderRadius: 4,
fontSize: 14,
fontFamily: '\'Roboto\', \'ArialMT\', \'Arial\''
// Alias Token
// colorBgContainer: '#f6ffed'
}
};
document.addEventListener('DOMContentLoaded', () => {

View file

@ -17,20 +17,7 @@ function Layout(props: Props) {
return (
<AntLayout style={{ minHeight: '100vh' }}>
{!hideHeader && (
<Header
style={{
position: 'sticky',
top: 0,
zIndex: 1,
padding: '0 15px',
display: 'flex',
alignItems: 'center',
height: '60px'
}}
className='justify-between bg-gray-lightest'
>
<TopHeader />
</Header>
<TopHeader />
)}
<AntLayout>
{!hideHeader && (

View file

@ -3,6 +3,7 @@ import { sessions, withSiteId } from 'App/routes';
import AnimatedSVG from 'Shared/AnimatedSVG';
import { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import { NavLink } from 'react-router-dom';
import { Tooltip } from 'antd';
const SESSIONS_PATH = sessions();
@ -13,14 +14,11 @@ interface Props {
function Logo(props: Props) {
return (
<NavLink to={withSiteId(SESSIONS_PATH, props.siteId)}>
<div className='relative select-none'>
<div className=''>
<Tooltip title={`v${window.env.VERSION}`}>
<div>
<AnimatedSVG name={ICONS.LOGO_SMALL} size='30' />
</div>
<div className='absolute bottom-0' style={{ fontSize: '7px', right: '-12px', bottom: '-15px' }}>
v{window.env.VERSION}
</div>
</div>
</Tooltip>
</NavLink>
);
}

View file

@ -4,21 +4,49 @@ import SVG from 'UI/SVG';
import * as routes from 'App/routes';
import { client, CLIENT_DEFAULT_TAB, CLIENT_TABS, withSiteId } from 'App/routes';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { categories, MENU, preferences, PREFERENCES_MENU } from './data';
import { categories as main_menu, MENU, preferences, PREFERENCES_MENU } from './data';
import { connect } from 'react-redux';
import { MODULES } from 'Components/Client/Modules';
import cn from 'classnames';
import { Icon } from 'UI';
const { Text } = Typography;
interface Props {
siteId?: string;
modules: string[];
}
function SideMenu(props: RouteComponentProps<Props>) {
// @ts-ignore
const { siteId } = props;
const { siteId, modules } = props;
const isPreferencesActive = props.location.pathname.includes('/client/');
let menu = isPreferencesActive ? preferences : main_menu;
menu.forEach((category) => {
category.items.forEach((item) => {
if (item.key === MENU.NOTES && !modules.includes(MODULES.NOTES)) {
item.hidden = true;
}
if ((item.key === MENU.LIVE_SESSIONS || item.key === MENU.RECORDINGS) && !modules.includes(MODULES.ASSIST)) {
item.hidden = true;
}
if (item.key === MENU.SESSIONS && !modules.includes(MODULES.OFFLINE_RECORDINGS)) {
item.hidden = true;
}
if (item.key === MENU.ALERTS && !modules.includes(MODULES.ALERTS)) {
item.hidden = true;
}
});
});
const menuRoutes: any = {
exit: () => props.history.push(withSiteId(routes.sessions(), siteId)),
[MENU.SESSIONS]: () => withSiteId(routes.sessions(), siteId),
@ -42,6 +70,7 @@ function SideMenu(props: RouteComponentProps<Props>) {
[PREFERENCES_MENU.TEAM]: () => client(CLIENT_TABS.MANAGE_USERS),
[PREFERENCES_MENU.NOTIFICATIONS]: () => client(CLIENT_TABS.NOTIFICATIONS),
[PREFERENCES_MENU.BILLING]: () => client(CLIENT_TABS.BILLING),
[PREFERENCES_MENU.MODULES]: () => client(CLIENT_TABS.MODULES)
};
const handleClick = (item: any) => {
@ -68,31 +97,37 @@ function SideMenu(props: RouteComponentProps<Props>) {
};
return (
<Menu defaultSelectedKeys={['1']} mode='inline' onClick={handleClick}
style={{ backgroundColor: '#f6f6f6', border: 'none' }}>
{isPreferencesActive && <Menu.Item key='exit' icon={<SVG name='arrow-bar-left' />}>
<Text className='ml-2'>Exit</Text>
</Menu.Item>}
{(isPreferencesActive ? preferences : categories).map((category, index) => (
{(isPreferencesActive ? preferences : main_menu).map((category, index) => (
<React.Fragment key={category.key}>
{index > 0 && <Divider style={{ margin: '6px 0' }} />}
<Menu.ItemGroup key={category.key}
title={<Text className='uppercase text-sm' type='secondary'>{category.title}</Text>}>
{category.items.map((item) => item.children ? (
<Menu.SubMenu key={item.key} title={<Text className='ml-2'>{item.label}</Text>}
icon={<SVG name={item.icon} size={16} />}>
{item.children.map((child) => <Menu.Item key={child.key}>{child.label}</Menu.Item>)}
</Menu.SubMenu>
) : (
<Menu.Item key={item.key} icon={<SVG name={item.icon} size={16} />}
style={{ color: '#333' }}
className={isMenuItemActive(item.key) ? 'ant-menu-item-selected bg-active-blue color-teal' : ''}>
<Text className='ml-2'>{item.label}</Text>
</Menu.Item>
))}
{category.items.filter((item: any) => !item.hidden).map((item: any) => {
const isActive = isMenuItemActive(item.key);
return item.children ? (
<Menu.SubMenu
key={item.key}
title={<Text
className={cn('ml-5 !rounded')}>{item.label}</Text>}
icon={<SVG name={item.icon} size={16} />}>
{item.children.map((child: any) => <Menu.Item
className={cn('ml-8', { 'ant-menu-item-selected !bg-active-dark-blue': isMenuItemActive(child.key) })}
key={child.key}>{child.label}</Menu.Item>)}
</Menu.SubMenu>
) : (
<Menu.Item key={item.key} icon={<Icon name={item.icon} size={16} color={isActive ? 'teal' : ''} />}
style={{ color: '#333' }}
className={cn('!rounded', { 'ant-menu-item-selected !bg-active-dark-blue': isActive })}>
<Text className={cn('ml-2', { 'color-teal': isActive })}>{item.label}</Text>
</Menu.Item>
);
})}
</Menu.ItemGroup>
</React.Fragment>
))}
@ -100,4 +135,6 @@ function SideMenu(props: RouteComponentProps<Props>) {
);
}
export default withRouter(SideMenu);
export default withRouter(connect((state: any) => ({
modules: state.getIn(['user', 'account', 'modules']) || []
}))(SideMenu));

View file

@ -1,48 +1,33 @@
import React from 'react';
import Header from 'Components/Header/Header';
import { Menu, MenuProps } from 'antd';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import { client, CLIENT_DEFAULT_TAB, withSiteId } from 'App/routes';
import { NavLink } from 'react-router-dom';
import Logo from 'App/layout/Logo';
import SiteDropdown from 'Components/Header/SiteDropdown';
import styles from 'Components/Header/header.module.css';
import GettingStartedProgress from 'Shared/GettingStarted/GettingStartedProgress';
import Notifications from 'Components/Alerts/Notifications/Notifications';
import cn from 'classnames';
import { Icon, Tooltip } from 'UI';
import SettingsMenu from 'Components/Header/SettingsMenu/SettingsMenu';
import HealthStatus from 'Components/Header/HealthStatus';
import { getInitials } from 'App/utils';
import UserMenu from 'Components/Header/UserMenu/UserMenu';
import ErrorGenPanel from 'App/dev/components/ErrorGenPanel';
import TopRight from 'App/layout/TopRight';
import ProjectDropdown from 'Shared/ProjectDropdown';
import { Layout } from 'antd';
interface Props {
const { Header } = Layout;
}
const CLIENT_PATH = client(CLIENT_DEFAULT_TAB);
const items1: MenuProps['items'] = ['1', '2', '3'].map((key) => ({
key,
label: `nav ${key}`
}));
function TopHeader(props: Props) {
// @ts-ignore
function TopHeader() {
return (
<>
<Header
style={{
position: 'sticky',
top: 0,
zIndex: 1,
padding: '0 15px',
display: 'flex',
alignItems: 'center',
height: '60px'
}}
className='justify-between bg-gray-lightest'
>
<div className='flex items-center'>
<Logo siteId={1} />
<div className='mx-4' />
<SiteDropdown />
<ProjectDropdown />
</div>
<TopRight />
</>
</Header>
);
}

View file

@ -12,7 +12,9 @@ import UserMenu from 'Components/Header/UserMenu/UserMenu';
import ErrorGenPanel from 'App/dev/components/ErrorGenPanel';
import { client, CLIENT_DEFAULT_TAB } from 'App/routes';
import { connect } from 'react-redux';
import { Menu, MenuProps, Popover, Button } from 'antd';
import { Menu, MenuProps, Popover } from 'antd';
import { Button } from 'antd';
import { SettingOutlined } from '@ant-design/icons';
const CLIENT_PATH = client(CLIENT_DEFAULT_TAB);
@ -42,12 +44,15 @@ function TopRight(props: Props) {
<div className='mx-2' />
<Popover content={<SettingsMenu account={account} />}>
{/*<Button type='primary'>Hover me</Button>*/}
<NavLink to={CLIENT_PATH}>
<Icon name='gear' size='20' color='gray-dark' className='cursor-pointer' />
</NavLink>
</Popover>
{/*<Button type='primary'>Hover me</Button>*/}
<NavLink to={CLIENT_PATH}>
<Popover content={<SettingsMenu account={account} />}>
<Button icon={<SettingOutlined />}></Button>
{/*<Icon name='gear' size='20' color='gray-dark' className='cursor-pointer' />*/}
</Popover>
</NavLink>
<div className='mx-2' />
@ -55,7 +60,7 @@ function TopRight(props: Props) {
<div className='mx-2' />
<Popover content={<UserMenu className='' />}>
<Popover content={<UserMenu className='' />} placement={'topRight'}>
<div className='flex items-center cursor-pointer'>
<div className='w-10 h-10 bg-tealx rounded-full flex items-center justify-center color-white'>
{getInitials(account.name)}

View file

@ -1,12 +1,14 @@
import React from 'react';
import PreferencesMenu from 'Components/Client/PreferencesMenu';
interface MenuItem {
export interface MenuItem {
label: React.ReactNode;
key: React.Key;
icon?: string;
children?: MenuItem[];
route?: string;
hidden?: boolean;
disabled?: boolean;
}
interface Category {
@ -21,6 +23,7 @@ export const enum PREFERENCES_MENU {
INTEGRATIONS = 'integrations',
METADATA = 'metadata',
WEBHOOKS = 'webhooks',
MODULES = 'modules',
PROJECTS = 'projects',
ROLES_ACCESS = 'roles-access',
AUDIT = 'audit',
@ -56,7 +59,7 @@ export const categories: Category[] = [
{ label: 'Sessions', key: MENU.SESSIONS, icon: 'collection-play' },
// { label: 'Recommendations', key: MENU.RECOMMENDATIONS, icon: 'magic' },
// { label: 'Vault', key: MENU.VAULT, icon: 'safe' },
{ label: 'Bookmarks', key: MENU.BOOKMARKS, icon: 'safe' },
{ label: 'Bookmarks', key: MENU.BOOKMARKS, icon: 'bookmark' },
{ label: 'Notes', key: MENU.NOTES, icon: 'stickies' }
]
},
@ -73,14 +76,15 @@ export const categories: Category[] = [
key: 'analytics',
items: [
{ label: 'Dashboards', key: MENU.DASHBOARDS, icon: 'columns-gap' },
{
label: 'Cards', key: MENU.CARDS, icon: 'bar-chart-line', children: [
{ label: 'All', key: MENU.CARDS },
{ label: 'Funnels', key: MENU.FUNNELS },
{ label: 'Error Tracking', key: MENU.ERROR_TRACKING },
{ label: 'Resource Monitoring', key: MENU.RESOURCE_MONITORING }
]
},
{ label: 'Card', key: MENU.CARDS, icon: 'bar-chart-line' },
// {
// label: 'Cards', key: MENU.CARDS, icon: 'bar-chart-line', children: [
// { label: 'All', key: MENU.CARDS },
// { label: 'Funnels', key: MENU.FUNNELS },
// { label: 'Error Tracking', key: MENU.ERROR_TRACKING },
// { label: 'Resource Monitoring', key: MENU.RESOURCE_MONITORING }
// ]
// },
{ label: 'Alerts', key: MENU.ALERTS, icon: 'bell' }
]
},
@ -105,6 +109,7 @@ export const preferences: Category[] = [
{ label: 'Integrations', key: PREFERENCES_MENU.INTEGRATIONS, icon: 'plug' },
{ label: 'Metadata', key: PREFERENCES_MENU.METADATA, icon: 'tags' },
{ label: 'Webhooks', key: PREFERENCES_MENU.WEBHOOKS, icon: 'link-45deg' },
{ label: 'Modules', key: PREFERENCES_MENU.MODULES, icon: 'link-45deg' },
{ label: 'Projects', key: PREFERENCES_MENU.PROJECTS, icon: 'folder2' },
{ label: 'Roles & Access', key: PREFERENCES_MENU.ROLES_ACCESS, icon: 'diagram-3' },
{ label: 'Audit', key: PREFERENCES_MENU.AUDIT, icon: 'list-ul' },

View file

@ -5,6 +5,7 @@ import { toast } from 'react-toastify';
import Webhook, { IWebhook } from 'Types/webhook';
import { webhookService } from 'App/services';
import { GettingStarted } from './types/gettingStarted';
import ModuleSettings from 'MOBX/types/moduleSettings';
export default class SettingsStore {
loadingCaptureRate: boolean = false;
@ -15,6 +16,7 @@ export default class SettingsStore {
webhookInst = new Webhook();
hooksLoading = false;
gettingStarted: GettingStarted = new GettingStarted();
// moduleSettings: ModuleSettings = new ModuleSettings();
constructor() {
makeAutoObservable(this, {

View file

@ -0,0 +1,71 @@
import { makeAutoObservable, runInAction } from 'mobx';
const enum MODULE {
ASSIST = 'assist',
NOTES = 'notes',
BUG_REPORTS = 'bug-reports',
OFFLINE_RECORDINGS = 'offline-recordings',
ALERTS = 'alerts',
}
export const modules = [
{
label: 'Assist',
description: 'Record and replay user sessions to see a video of what users did on your website.',
key: MODULE.ASSIST,
icon: 'broadcast'
},
{
label: 'Notes',
description: 'Add notes to sessions and recordings to share with your team.',
key: MODULE.NOTES,
icon: 'stickies',
isEnabled: true
},
{
label: 'Bug Reports',
description: 'Allow users to report bugs and issues on your website.',
key: MODULE.BUG_REPORTS,
icon: 'filetype-pdf'
},
{
label: 'Offline Recordings',
description: 'Record user sessions even when they are offline.',
key: MODULE.OFFLINE_RECORDINGS,
icon: 'record2'
},
{
label: 'Alerts',
description: 'Get notified when users encounter errors on your website.',
key: MODULE.ALERTS,
icon: 'bell'
}
];
export default class ModuleSettings {
assist: boolean = false;
notes: boolean = false;
bugReports: boolean = false;
offlineRecordings: boolean = false;
alerts: boolean = false;
constructor() {
makeAutoObservable(this);
}
setModuleSettings = (settings: any) => {
this.assist = settings[MODULE.ASSIST];
this.notes = settings[MODULE.NOTES];
this.bugReports = settings[MODULE.BUG_REPORTS];
this.offlineRecordings = settings[MODULE.OFFLINE_RECORDINGS];
this.alerts = settings[MODULE.ALERTS];
}
updateKey = (key: string, value: any) => {
runInAction(() => {
this[key] = value;
});
}
}

View file

@ -69,6 +69,7 @@ export const CLIENT_TABS = {
NOTIFICATIONS: 'notifications',
AUDIT: 'audit',
BILLING: 'billing',
MODULES: 'modules',
};
export const CLIENT_DEFAULT_TAB = CLIENT_TABS.PROFILE;
const routerClientTabString = `:activeTab(${Object.values(CLIENT_TABS).join('|')})`;

View file

@ -29,6 +29,7 @@
.fill-blue { fill: $blue }
.fill-blue2 { fill: $blue2 }
.fill-active-blue { fill: $active-blue }
.fill-active-dark-blue { fill: $active-dark-blue }
.fill-bg-blue { fill: $bg-blue }
.fill-active-blue-border { fill: $active-blue-border }
.fill-pink { fill: $pink }
@ -65,6 +66,7 @@
.hover-fill-blue:hover svg { fill: $blue }
.hover-fill-blue2:hover svg { fill: $blue2 }
.hover-fill-active-blue:hover svg { fill: $active-blue }
.hover-fill-active-dark-blue:hover svg { fill: $active-dark-blue }
.hover-fill-bg-blue:hover svg { fill: $bg-blue }
.hover-fill-active-blue-border:hover svg { fill: $active-blue-border }
.hover-fill-pink:hover svg { fill: $pink }
@ -103,6 +105,7 @@
.color-blue { color: $blue }
.color-blue2 { color: $blue2 }
.color-active-blue { color: $active-blue }
.color-active-dark-blue { color: $active-dark-blue }
.color-bg-blue { color: $bg-blue }
.color-active-blue-border { color: $active-blue-border }
.color-pink { color: $pink }
@ -141,6 +144,7 @@
.hover-blue:hover { color: $blue }
.hover-blue2:hover { color: $blue2 }
.hover-active-blue:hover { color: $active-blue }
.hover-active-dark-blue:hover { color: $active-dark-blue }
.hover-bg-blue:hover { color: $bg-blue }
.hover-active-blue-border:hover { color: $active-blue-border }
.hover-pink:hover { color: $pink }
@ -178,6 +182,7 @@
.border-blue { border-color: $blue }
.border-blue2 { border-color: $blue2 }
.border-active-blue { border-color: $active-blue }
.border-active-dark-blue { border-color: $active-dark-blue }
.border-bg-blue { border-color: $bg-blue }
.border-active-blue-border { border-color: $active-blue-border }
.border-pink { border-color: $pink }

View file

@ -1,171 +1,186 @@
#app {
padding: 0;
min-height: 100%;
display: flex;
flex-direction: column;
padding: 0;
min-height: 100%;
display: flex;
flex-direction: column;
}
* {
border-color: #eeeeee;
border-color: #eeeeee;
}
.page {
padding-top: 50px;
padding-top: 50px;
}
.page-margin {
padding-top: 81px;
padding-top: 81px;
}
.container-fit {
margin: 0 30px 0px;
margin: 0 30px 0px;
}
.container {
margin: 0 30px 30px;
margin: 0 30px 30px;
}
@media only screen and (max-width: 1380px) {
.container-70 {
width: 90%;
}
.container-70 {
width: 90%;
}
}
@media only screen and (min-width: 1380px) {
.container-70 {
width: 1280px;
}
.container-70 {
width: 1280px;
}
}
.container-70 {
position: relative;
margin: 0 auto;
position: relative;
margin: 0 auto;
}
.container-90 {
width: 98%;
margin: 0 auto;
width: 98%;
margin: 0 auto;
}
.side-menu {
width: 250px;
height: calc(100vh - 80px);
overflow-y: auto;
padding-right: 20px;
position: fixed;
top: 81px;
&::-webkit-scrollbar {
width: 250px;
height: calc(100vh - 80px);
overflow-y: auto;
padding-right: 20px;
position: fixed;
top: 81px;
&
::-webkit-scrollbar {
width: 0px;
}
&:hover {
&::-webkit-scrollbar {
width: 0px;
}
}
}
&
:hover {
&
::-webkit-scrollbar {
width: 0px;
}
}
}
.side-menu-margined {
margin-left: 250px;
margin-left: 250px;
}
.top-header {
margin-bottom: 25px;
/* border: dashed thin gray; */
min-height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 25px;
/* border: dashed thin gray; */
min-height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
}
.page-title {
font-size: 22px;
margin-right: 15px;
& > span {
font-weight: 300;
}
& .title {
font-size: 22px;
margin-right: 15px;
& span {
color: $gray-medium;
font-weight: 300;
}
}
&
> span {
font-weight: 300;
}
&
.title {
margin-right: 15px;
&
span {
color: $ gray-medium;
font-weight: 300;
}
}
}
.page-title-flex {
display: flex;
align-items: center;
display: flex;
align-items: center;
}
[data-hidden='true'] {
display: none !important;
display: none !important;
}
[data-disabled='true'] {
pointer-events: none;
opacity: 0.5;
pointer-events: none;
opacity: 0.5;
}
.form-group {
margin-bottom: 25px;
& label {
margin-bottom: 25px;
&
label {
display: inline-block;
margin-bottom: 5px;
}
}
}
.disabled {
opacity: 0.4;
pointer-events: none;
opacity: 0.4;
pointer-events: none;
}
.hover {
&:hover {
background-color: $active-blue;
}
&
:hover {
background-color: $ active-blue;
}
}
.hover-teal:hover {
background-color: $active-blue;
color: $teal;
& svg {
fill: $teal;
}
background-color: $ active-blue;
color: $ teal;
&
svg {
fill: $ teal;
}
}
.note-hover {
border: solid thin transparent;
&:hover {
border: solid thin transparent;
&
:hover {
background-color: #FFFEF5;
border-color: $gray-lightest;
}
border-color: $ gray-lightest;
}
}
.note-hover-bg {
&:hover {
&
:hover {
background-color: #FFFEF5;
}
}
}
.text-dotted-underline {
text-decoration: underline dotted !important;
text-decoration: underline dotted !important;
}
/* .divider {
width: 1px;
margin: 0 15px;
background-color: $gray-light;
}
.divider-h {
height: 1px;
width: 100%;
margin: 25px 0;
background-color: $gray-light;
} */
.no-scroll {
height: 100vh;
overflow-y: hidden;
padding-right: 15px;
height: 100vh;
overflow-y: hidden;
padding-right: 15px;
}

View file

@ -31,9 +31,9 @@ html {
-webkit-text-size-adjust: 100%; /* 2 */
-moz-tab-size: 4; /* 3 */
tab-size: 4; /* 3 */
font-family: theme('fontFamily.sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); /* 4 */
font-feature-settings: theme('fontFamily.sans[1].fontFeatureSettings', normal); /* 5 */
font-variation-settings: theme('fontFamily.sans[1].fontVariationSettings', normal); /* 6 */
/*font-family: theme('fontFamily.sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); !* 4 *!*/
/*font-feature-settings: theme('fontFamily.sans[1].fontFeatureSettings', normal); !* 5 *!*/
/*font-variation-settings: theme('fontFamily.sans[1].fontVariationSettings', normal); !* 6 *!*/
}
/*

View file

@ -1 +0,0 @@
@layout-header-background: #FFFFFF;

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-bookmark" viewBox="0 0 16 16">
<path d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v13.5a.5.5 0 0 1-.777.416L8 13.101l-5.223 2.815A.5.5 0 0 1 2 15.5V2zm2-1a1 1 0 0 0-1 1v12.566l4.723-2.482a.5.5 0 0 1 .554 0L13 14.566V2a1 1 0 0 0-1-1H4z"/>
</svg>

After

Width:  |  Height:  |  Size: 291 B

View file

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-broadcast" viewBox="0 0 16 16">
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-broadcast" viewBox="0 0 16 16">
<path d="M3.05 3.05a7 7 0 0 0 0 9.9.5.5 0 0 1-.707.707 8 8 0 0 1 0-11.314.5.5 0 0 1 .707.707zm2.122 2.122a4 4 0 0 0 0 5.656.5.5 0 1 1-.708.708 5 5 0 0 1 0-7.072.5.5 0 0 1 .708.708zm5.656-.708a.5.5 0 0 1 .708 0 5 5 0 0 1 0 7.072.5.5 0 1 1-.708-.708 4 4 0 0 0 0-5.656.5.5 0 0 1 0-.708zm2.122-2.12a.5.5 0 0 1 .707 0 8 8 0 0 1 0 11.313.5.5 0 0 1-.707-.707 7 7 0 0 0 0-9.9.5.5 0 0 1 0-.707zM10 8a2 2 0 1 1-4 0 2 2 0 0 1 4 0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 559 B

After

Width:  |  Height:  |  Size: 516 B

View file

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-card-list" viewBox="0 0 16 16">
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-card-list" viewBox="0 0 16 16">
<path d="M14.5 3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h13zm-13-1A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-13z"/>
<path d="M5 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 5 8zm0-2.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm0 5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-1-5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0zM4 8a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0zm0 2.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 638 B

After

Width:  |  Height:  |  Size: 595 B

View file

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-collection-play" viewBox="0 0 16 16">
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-collection-play" viewBox="0 0 16 16">
<path d="M2 3a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 0-1h-11A.5.5 0 0 0 2 3zm2-2a.5.5 0 0 0 .5.5h7a.5.5 0 0 0 0-1h-7A.5.5 0 0 0 4 1zm2.765 5.576A.5.5 0 0 0 6 7v5a.5.5 0 0 0 .765.424l4-2.5a.5.5 0 0 0 0-.848l-4-2.5z"/>
<path d="M1.5 14.5A1.5 1.5 0 0 1 0 13V6a1.5 1.5 0 0 1 1.5-1.5h13A1.5 1.5 0 0 1 16 6v7a1.5 1.5 0 0 1-1.5 1.5h-13zm13-1a.5.5 0 0 0 .5-.5V6a.5.5 0 0 0-.5-.5h-13A.5.5 0 0 0 1 6v7a.5.5 0 0 0 .5.5h13z"/>
</svg>

Before

Width:  |  Height:  |  Size: 550 B

After

Width:  |  Height:  |  Size: 507 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-filetype-pdf" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2h-1v-1h1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5L14 4.5ZM1.6 11.85H0v3.999h.791v-1.342h.803c.287 0 .531-.057.732-.173.203-.117.358-.275.463-.474a1.42 1.42 0 0 0 .161-.677c0-.25-.053-.476-.158-.677a1.176 1.176 0 0 0-.46-.477c-.2-.12-.443-.179-.732-.179Zm.545 1.333a.795.795 0 0 1-.085.38.574.574 0 0 1-.238.241.794.794 0 0 1-.375.082H.788V12.48h.66c.218 0 .389.06.512.181.123.122.185.296.185.522Zm1.217-1.333v3.999h1.46c.401 0 .734-.08.998-.237a1.45 1.45 0 0 0 .595-.689c.13-.3.196-.662.196-1.084 0-.42-.065-.778-.196-1.075a1.426 1.426 0 0 0-.589-.68c-.264-.156-.599-.234-1.005-.234H3.362Zm.791.645h.563c.248 0 .45.05.609.152a.89.89 0 0 1 .354.454c.079.201.118.452.118.753a2.3 2.3 0 0 1-.068.592 1.14 1.14 0 0 1-.196.422.8.8 0 0 1-.334.252 1.298 1.298 0 0 1-.483.082h-.563v-2.707Zm3.743 1.763v1.591h-.79V11.85h2.548v.653H7.896v1.117h1.606v.638H7.896Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person" viewBox="0 0 16 16">
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-person" viewBox="0 0 16 16">
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 387 B

After

Width:  |  Height:  |  Size: 344 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-record2" viewBox="0 0 16 16">
<path d="M8 12a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0 1A5 5 0 1 0 8 3a5 5 0 0 0 0 10z"/>
<path d="M10 8a2 2 0 1 1-4 0 2 2 0 0 1 4 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 221 B

View file

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-slack" viewBox="0 0 16 16">
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-slack" viewBox="0 0 16 16">
<path d="M3.362 10.11c0 .926-.756 1.681-1.681 1.681S0 11.036 0 10.111C0 9.186.756 8.43 1.68 8.43h1.682v1.68zm.846 0c0-.924.756-1.68 1.681-1.68s1.681.756 1.681 1.68v4.21c0 .924-.756 1.68-1.68 1.68a1.685 1.685 0 0 1-1.682-1.68v-4.21zM5.89 3.362c-.926 0-1.682-.756-1.682-1.681S4.964 0 5.89 0s1.68.756 1.68 1.68v1.682H5.89zm0 .846c.924 0 1.68.756 1.68 1.681S6.814 7.57 5.89 7.57H1.68C.757 7.57 0 6.814 0 5.89c0-.926.756-1.682 1.68-1.682h4.21zm6.749 1.682c0-.926.755-1.682 1.68-1.682.925 0 1.681.756 1.681 1.681s-.756 1.681-1.68 1.681h-1.681V5.89zm-.848 0c0 .924-.755 1.68-1.68 1.68A1.685 1.685 0 0 1 8.43 5.89V1.68C8.43.757 9.186 0 10.11 0c.926 0 1.681.756 1.681 1.68v4.21zm-1.681 6.748c.926 0 1.682.756 1.682 1.681S11.036 16 10.11 16s-1.681-.756-1.681-1.68v-1.682h1.68zm0-.847c-.924 0-1.68-.755-1.68-1.68 0-.925.756-1.681 1.68-1.681h4.21c.924 0 1.68.756 1.68 1.68 0 .926-.756 1.681-1.68 1.681h-4.21z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 990 B

View file

@ -32,6 +32,7 @@ module.exports = {
blue: "#366CD9",
blue2: "#0076FF",
"active-blue": "#F6F7FF",
'active-dark-blue': "#E2E4F6",
"bg-blue": "#e3e6ff",
"active-blue-border": "#D0D4F2",
pink: "#ffb9b9",

View file

@ -14,6 +14,7 @@ export interface IAccount extends IMember {
license: string
expirationDate?: DateTime
permissions: string[]
modules: string[]
iceServers: string
hasPassword: boolean
apiKey: string
@ -33,6 +34,7 @@ export default Member.extend({
license: '',
expirationDate: undefined,
permissions: [],
modules: ['notes'],
iceServers: undefined,
hasPassword: false, // to check if it's SSO
apiKey: undefined,

View file

@ -23,6 +23,7 @@
"cy:test-edge": "cypress run --browser edge"
},
"dependencies": {
"@ant-design/icons": "^5.2.5",
"@floating-ui/react-dom-interactions": "^0.10.3",
"@sentry/browser": "^5.21.1",
"@svg-maps/world": "^1.0.1",

View file

@ -45,6 +45,6 @@ module.exports = {
},
plugins: [],
corePlugins: {
// preflight: false
preflight: false
}
};

View file

@ -49,6 +49,13 @@ __metadata:
languageName: node
linkType: hard
"@ant-design/icons-svg@npm:^4.3.0":
version: 4.3.0
resolution: "@ant-design/icons-svg@npm:4.3.0"
checksum: 09067374027ffbc971a5e351e102553ac4d1319148ee9f4247a1f657561a46d70229b7cffaa65bc0e4d3540a4f230a4402bb1d6ab81c2c9803c8888373c0ff21
languageName: node
linkType: hard
"@ant-design/icons@npm:^5.0.0":
version: 5.0.1
resolution: "@ant-design/icons@npm:5.0.1"
@ -65,6 +72,23 @@ __metadata:
languageName: node
linkType: hard
"@ant-design/icons@npm:^5.2.5":
version: 5.2.5
resolution: "@ant-design/icons@npm:5.2.5"
dependencies:
"@ant-design/colors": ^7.0.0
"@ant-design/icons-svg": ^4.3.0
"@babel/runtime": ^7.11.2
classnames: ^2.2.6
lodash.camelcase: ^4.3.0
rc-util: ^5.31.1
peerDependencies:
react: ">=16.0.0"
react-dom: ">=16.0.0"
checksum: 085087ebaa181de62323c706cf86c8304967340dfcbd5881117f01a20eaedefad6e42b6998090e7416d1378315f4ee19738a6f1ab0049b0b156cfd7e473c7544
languageName: node
linkType: hard
"@ant-design/react-slick@npm:~1.0.0":
version: 1.0.1
resolution: "@ant-design/react-slick@npm:1.0.1"
@ -15648,6 +15672,13 @@ __metadata:
languageName: node
linkType: hard
"lodash.camelcase@npm:^4.3.0":
version: 4.3.0
resolution: "lodash.camelcase@npm:4.3.0"
checksum: fcba15d21a458076dd309fce6b1b4bf611d84a0ec252cb92447c948c533ac250b95d2e00955801ebc367e5af5ed288b996d75d37d2035260a937008e14eaf432
languageName: node
linkType: hard
"lodash.clonedeep@npm:^4.5.0, lodash.clonedeep@npm:~4.5.0":
version: 4.5.0
resolution: "lodash.clonedeep@npm:4.5.0"
@ -17558,6 +17589,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "openreplay@workspace:."
dependencies:
"@ant-design/icons": ^5.2.5
"@babel/cli": ^7.10.5
"@babel/core": ^7.17.12
"@babel/node": ^7.16.8
@ -20113,6 +20145,19 @@ __metadata:
languageName: node
linkType: hard
"rc-util@npm:^5.31.1":
version: 5.36.0
resolution: "rc-util@npm:5.36.0"
dependencies:
"@babel/runtime": ^7.18.3
react-is: ^16.12.0
peerDependencies:
react: ">=16.9.0"
react-dom: ">=16.9.0"
checksum: 262e26e8be16c0ad0ccbfeeeb0b6b3913cebe3be83f7760f73b93e33bb04392b53fcfb05f01d6064c05a588603cb9c73a4c997b296739fc357772976b972bb29
languageName: node
linkType: hard
"rc-virtual-list@npm:^3.4.13, rc-virtual-list@npm:^3.4.8":
version: 3.4.13
resolution: "rc-virtual-list@npm:3.4.13"