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
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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\-]+$')
|
||||
|
|
|
|||
|
|
@ -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 />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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);
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
32
frontend/app/components/Client/Modules/ModuleCard.tsx
Normal 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;
|
||||
42
frontend/app/components/Client/Modules/Modules.tsx
Normal 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));
|
||||
51
frontend/app/components/Client/Modules/index.ts
Normal 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'
|
||||
}
|
||||
];
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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>}
|
||||
|
|
|
|||
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
);
|
||||
1
frontend/app/components/shared/ProjectDropdown/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ProjectDropdown'
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
71
frontend/app/mstore/types/moduleSettings.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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('|')})`;
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 *!*/
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
@layout-header-background: #FFFFFF;
|
||||
3
frontend/app/svg/icons/bookmark.svg
Normal 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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
3
frontend/app/svg/icons/filetype-pdf.svg
Normal 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 |
|
|
@ -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 |
4
frontend/app/svg/icons/record2.svg
Normal 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 |
|
|
@ -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 |
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -45,6 +45,6 @@ module.exports = {
|
|||
},
|
||||
plugins: [],
|
||||
corePlugins: {
|
||||
// preflight: false
|
||||
preflight: false
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||