feat: update sidebar and base theme styles (#4272)

* feat: update sidebar and base theme styles

* feat: update sidebar items and styles

* feat: wire up logs navigation and update user settings page

* feat: update styles to handle light mode, add full view header

* feat: update onboarding header and styles

* feat: remove unused routes

* feat: handle sidebar collapse

* feat: show pointer on logo hover

* feat: fix logs module navigations

* feat: update logo click route

* feat: update entity name color to primary in application and dashboard tables

* feat: update sidebar item styles

* feat: update collapse icon and styles

* fix: name not updated in menu on change

* fix: show invite members nav item

* fix: open invite members modal on invite team member nav item click
This commit is contained in:
Yunus M 2024-01-05 11:15:31 +05:30 committed by GitHub
parent bdd7778e58
commit 7d960b79dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 1347 additions and 374 deletions

View File

@ -36,6 +36,7 @@
"@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0",
"@monaco-editor/react": "^4.3.1",
"@signozhq/design-tokens": "0.0.6",
"@uiw/react-md-editor": "3.23.5",
"@xstate/react": "^3.0.0",
"ansi-to-html": "0.7.2",

View File

@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2048_2251)">
<path opacity="0.9" d="M8.02226 15.9866C3.56539 15.9866 -6.10352e-05 12.4896 -6.10352e-05 8.11832C-6.10352e-05 3.79075 3.56539 0.25 8.02226 0.25H13.0584C14.7075 0.25 15.9999 1.56139 15.9999 3.13506V8.11832C15.9999 12.4896 12.4345 15.9866 8.02226 15.9866Z" fill="#F25733"/>
<path d="M7.95919 4.71207C4.63025 4.71207 2.75514 7.46868 2.67693 7.58603C2.48413 7.87508 2.48413 8.24888 2.67707 8.53816C2.75514 8.65528 4.63025 11.4119 7.95919 11.4119C11.2881 11.4119 13.1633 8.65528 13.2414 8.53792C13.4342 8.24888 13.4342 7.87508 13.2413 7.58582C13.1632 7.46868 11.2881 4.71207 7.95919 4.71207ZM3.13771 8.23088C3.06925 8.12832 3.06925 7.99571 3.13771 7.89307C3.20059 7.79867 4.53564 5.83764 6.92256 5.36723C5.84092 5.78476 5.07127 6.83485 5.07127 8.062C5.07127 9.28912 5.84092 10.3392 6.92256 10.7567C4.53564 10.2863 3.20059 8.32528 3.13771 8.23088ZM6.62838 8.062C6.62838 8.21488 6.50443 8.3388 6.35151 8.3388C6.19859 8.3388 6.07465 8.21488 6.07465 8.062C6.07465 7.02287 6.92003 6.17748 7.95916 6.17748C8.11207 6.17748 8.23599 6.30141 8.23599 6.45434C8.23599 6.60727 8.11207 6.73119 7.95916 6.73119C7.22535 6.73119 6.62838 7.32815 6.62838 8.062ZM7.95919 8.73504C7.58803 8.73504 7.2861 8.43312 7.2861 8.062C7.2861 7.69085 7.58803 7.3889 7.95919 7.3889C8.33039 7.3889 8.63231 7.69083 8.63231 8.062C8.63231 8.43312 8.33039 8.73504 7.95919 8.73504ZM12.7806 8.23088C12.7178 8.32528 11.3827 10.2863 8.99583 10.7567C10.0775 10.3392 10.8471 9.28912 10.8471 8.062C10.8471 6.83487 10.0775 5.78477 8.99583 5.36724C11.3827 5.83768 12.7178 7.7987 12.7806 7.89307C12.8491 7.99571 12.8491 8.12832 12.7806 8.23088Z" fill="#F9F2F9"/>
</g>
<defs>
<clipPath id="clip0_2048_2251">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -31,6 +31,7 @@
"NOT_FOUND": "SigNoz | Page Not Found",
"LOGS": "SigNoz | Logs",
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
"OLD_LOGS_EXPLORER": "SigNoz | Old Logs Explorer",
"LIVE_LOGS": "SigNoz | Live Logs",
"LOGS_PIPELINES": "SigNoz | Logs Pipelines",
"HOME_PAGE": "Open source Observability Platform | SigNoz",

View File

@ -112,17 +112,25 @@ export const MySettings = Loadable(
);
export const Logs = Loadable(
() => import(/* webpackChunkName: "Logs" */ 'pages/Logs'),
() => import(/* webpackChunkName: "Logs" */ 'pages/LogsModulePage'),
);
export const LogsExplorer = Loadable(
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/LogsExplorer'),
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/LogsModulePage'),
);
export const OldLogsExplorer = Loadable(
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/Logs'),
);
export const LiveLogs = Loadable(
() => import(/* webpackChunkName: "Live Logs" */ 'pages/LiveLogs'),
);
export const PipelinePage = Loadable(
() => import(/* webpackChunkName: "Pipelines" */ 'pages/LogsModulePage'),
);
export const Login = Loadable(
() => import(/* webpackChunkName: "Login" */ 'pages/Login'),
);
@ -151,10 +159,6 @@ export const LogsIndexToFields = Loadable(
import(/* webpackChunkName: "LogsIndexToFields Page" */ 'pages/LogsSettings'),
);
export const PipelinePage = Loadable(
() => import(/* webpackChunkName: "Pipelines" */ 'pages/Pipelines'),
);
export const BillingPage = Loadable(
() => import(/* webpackChunkName: "BillingPage" */ 'pages/Billing'),
);

View File

@ -23,6 +23,7 @@ import {
LogsIndexToFields,
MySettings,
NewDashboardPage,
OldLogsExplorer,
Onboarding,
OrganizationSettings,
PasswordReset,
@ -246,6 +247,13 @@ const routes: AppRoutes[] = [
key: 'LOGS_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.OLD_LOGS_EXPLORER,
exact: true,
component: OldLogsExplorer,
key: 'OLD_LOGS_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.LIVE_LOGS,
exact: true,

View File

@ -48,8 +48,9 @@ export const RawLogContent = styled.div<RawLogContentProps>`
line-clamp: ${linesPerRow};
-webkit-box-orient: vertical;`};
font-size: 1rem;
line-height: 2rem;
font-size: 12px;
line-height: 24px;
padding: 4px;
cursor: ${({ $isActiveLog, $isReadOnly }): string =>
$isActiveLog || $isReadOnly ? 'initial' : 'pointer'};

View File

@ -6,7 +6,6 @@ const ROUTES = {
TRACE: '/trace',
TRACE_DETAIL: '/trace/:id',
TRACES_EXPLORER: '/traces-explorer',
SETTINGS: '/settings',
GET_STARTED: '/get-started',
USAGE_EXPLORER: '/usage-explorer',
APPLICATION: '/services',
@ -23,15 +22,18 @@ const ROUTES = {
ERROR_DETAIL: '/error-detail',
VERSION: '/status',
MY_SETTINGS: '/my-settings',
SETTINGS: '/settings',
ORG_SETTINGS: '/settings/org-settings',
INGESTION_SETTINGS: '/settings/ingestion-settings',
SOMETHING_WENT_WRONG: '/something-went-wrong',
UN_AUTHORIZED: '/un-authorized',
NOT_FOUND: '/not-found',
LOGS: '/logs',
LOGS_EXPLORER: '/logs-explorer',
LIVE_LOGS: '/logs-explorer/live',
LOGS_PIPELINES: '/pipelines',
LOGS_BASE: '/logs',
LOGS: '/logs/logs-explorer',
OLD_LOGS_EXPLORER: '/logs/old-logs-explorer',
LOGS_EXPLORER: '/logs/logs-explorer',
LIVE_LOGS: '/logs/logs-explorer/live',
LOGS_PIPELINES: '/logs/pipelines',
HOME_PAGE: '/',
PASSWORD_RESET: '/password-reset',
LIST_LICENSES: '/licenses',

View File

@ -1,5 +1,6 @@
const themeColors = {
chartcolors: {
robin: '#3F5ECC',
dodgerBlue: '#2F80ED',
mediumOrchid: '#BB6BD9',
seaBuckthorn: '#F2994A',

View File

@ -15,6 +15,7 @@ export const ButtonContainer = styled.div`
align-items: center;
margin-top: 1rem;
margin-bottom: 1rem;
padding-right: 1rem;
}
`;

View File

@ -0,0 +1,53 @@
@import '@signozhq/design-tokens';
.app-layout {
height: 100%;
width: 100%;
.app-content {
width: 100%;
overflow: auto;
}
}
.isDarkMode {
.app-layout {
.app-content {
background: #0b0c0e;
}
}
}
.isLightMode {
.app-layout {
.app-content {
background: #ffffff;
}
}
}
.trial-expiry-banner {
padding: 8px;
background-color: #f25733;
color: white;
text-align: center;
}
.upgrade-link {
padding: 0px;
padding-right: 4px;
display: inline !important;
color: white;
text-decoration: underline;
text-decoration-color: white;
text-decoration-thickness: 2px;
text-underline-offset: 2px;
&:hover {
color: white;
text-decoration: underline;
text-decoration-color: white;
text-decoration-thickness: 2px;
text-underline-offset: 2px;
}
}

View File

@ -1,13 +1,22 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/anchor-is-valid */
import './AppLayout.styles.scss';
import { Flex } from 'antd';
import getDynamicConfigs from 'api/dynamicConfigs/getDynamicConfigs';
import getUserLatestVersion from 'api/user/getLatestVersion';
import getUserVersion from 'api/user/getVersion';
import cx from 'classnames';
import ROUTES from 'constants/routes';
import Header from 'container/Header';
import SideNav from 'container/SideNav';
import TopNav from 'container/TopNav';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { ReactNode, useEffect, useMemo, useRef } from 'react';
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
@ -25,15 +34,20 @@ import {
UPDATE_LATEST_VERSION_ERROR,
} from 'types/actions/app';
import AppReducer from 'types/reducer/app';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
import { ChildrenContainer, Layout, LayoutContent } from './styles';
import { getRouteKey } from './utils';
function AppLayout(props: AppLayoutProps): JSX.Element {
const { isLoggedIn, user } = useSelector<AppState, AppReducer>(
const { isLoggedIn, user, role } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const isDarkMode = useIsDarkMode();
const { data: licenseData, isFetching } = useLicense();
const { pathname } = useLocation();
const { t } = useTranslation(['titles']);
@ -196,16 +210,58 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const renderFullScreen =
pathname === ROUTES.GET_STARTED || pathname === ROUTES.WORKSPACE_LOCKED;
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);
useEffect(() => {
if (
!isFetching &&
licenseData?.payload?.onTrial &&
!licenseData?.payload?.trialConvertedToSubscription &&
!licenseData?.payload?.workSpaceBlock &&
getRemainingDays(licenseData?.payload.trialEnd) < 7
) {
setShowTrialExpiryBanner(true);
}
}, [licenseData, isFetching]);
const handleUpgrade = (): void => {
if (role === 'ADMIN') {
history.push(ROUTES.BILLING);
}
};
return (
<Layout>
<Layout className={isDarkMode ? 'darkMode' : 'lightMode'}>
<Helmet>
<title>{pageTitle}</title>
</Helmet>
{isToDisplayLayout && <Header />}
<Layout>
{isToDisplayLayout && !renderFullScreen && <SideNav />}
{showTrialExpiryBanner && (
<div className="trial-expiry-banner">
You are in free trial period. Your free trial will end on{' '}
<span>
{getFormattedDate(licenseData?.payload?.trialEnd || Date.now())}.
</span>
{role === 'ADMIN' ? (
<span>
{' '}
Please{' '}
<a className="upgrade-link" onClick={handleUpgrade}>
upgrade
</a>
to continue using SigNoz features.
</span>
) : (
'Please contact your administrator for upgrading to a paid plan.'
)}
</div>
)}
<Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}>
{isToDisplayLayout && !renderFullScreen && (
<SideNav licenseData={licenseData} isFetching={isFetching} />
)}
<div className="app-content">
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
<LayoutContent>
<ChildrenContainer>
@ -214,7 +270,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</ChildrenContainer>
</LayoutContent>
</ErrorBoundary>
</Layout>
</div>
</Flex>
</Layout>
);
}

View File

@ -13,6 +13,7 @@ export const Layout = styled(LayoutComponent)`
export const LayoutContent = styled(LayoutComponent.Content)`
overflow-y: auto;
height: 100%;
`;
export const ChildrenContainer = styled.div`

View File

@ -0,0 +1,37 @@
.full-view-header-container {
display: flex;
justify-content: center;
align-items: center;
padding: 24px 0;
.brand-logo {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
cursor: pointer;
img {
height: 32px;
width: 32px;
}
.brand-logo-name {
font-family: 'Work Sans', sans-serif;
font-size: 24px;
font-style: normal;
font-weight: 500;
line-height: 18px;
color: #fff;
}
}
}
.lightMode {
.brand-logo {
.brand-logo-name {
color: black;
}
}
}

View File

@ -0,0 +1,28 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './FullViewHeader.styles.scss';
import history from 'lib/history';
export default function FullViewHeader({
overrideRoute,
}: {
overrideRoute?: string;
}): React.ReactElement {
const handleLogoClick = (): void => {
history.push(overrideRoute || '/');
};
return (
<div className="full-view-header-container">
<div className="brand-logo" onClick={handleLogoClick}>
<img src="/Logos/signoz-brand-logo.svg" alt="SigNoz" />
<div className="brand-logo-name">SigNoz</div>
</div>
</div>
);
}
FullViewHeader.defaultProps = {
overrideRoute: '/',
};

View File

@ -332,7 +332,7 @@ function DashboardsList(): JSX.Element {
);
return (
<Card>
<Card style={{ margin: '16px 0' }}>
{GetHeader}
<TableContainer>

View File

@ -1,8 +1,7 @@
import { blue } from '@ant-design/colors';
import { Typography } from 'antd';
import styled from 'styled-components';
export const TableLinkText = styled(Typography.Text)`
color: ${blue.primary} !important;
color: #4e74f8 !important;
cursor: pointer;
`;

View File

@ -1,19 +1,18 @@
import { Button, ButtonProps } from 'antd';
import { themeColors } from 'constants/theme';
import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
export const LiveButtonStyled = styled(Button)<ButtonProps>`
background-color: rgba(${themeColors.buttonSuccessRgb}, 0.9);
background-color: #1eb475;
${({ danger }): FlattenSimpleInterpolation =>
!danger
? css`
&:hover {
background-color: rgba(${themeColors.buttonSuccessRgb}, 1) !important;
background-color: #1eb475 !important;
}
&:active {
background-color: rgba(${themeColors.buttonSuccessRgb}, 0.7) !important;
background-color: #1eb475 !important;
}
`
: css``}

View File

@ -1,7 +1,9 @@
import { Col, Row, Space } from 'antd';
import { Col, Row, Space, Typography } from 'antd';
import ROUTES from 'constants/routes';
import NewExplorerCTA from 'container/NewExplorerCTA';
import { FileText } from 'lucide-react';
import { useLocation } from 'react-use';
import ShowBreadcrumbs from '../TopNav/Breadcrumbs';
import DateTimeSelector from '../TopNav/DateTimeSelection';
import { Container } from './styles';
import { LocalTopNavProps } from './types';
@ -10,13 +12,25 @@ function LocalTopNav({
actions,
renderPermissions,
}: LocalTopNavProps): JSX.Element | null {
const { pathname } = useLocation();
const isLiveLogsPage = pathname === ROUTES.LIVE_LOGS;
return (
<Container>
{isLiveLogsPage && (
<Col span={16}>
<ShowBreadcrumbs />
</Col>
<Space>
<FileText color="#fff" size={16} />
<Col span={8}>
<Typography.Title level={4} style={{ marginTop: 0, marginBottom: 0 }}>
Live Logs
</Typography.Title>
</Space>
</Col>
)}
<Col span={isLiveLogsPage ? 8 : 24}>
<Row justify="end">
<Space align="start" size={30} direction="horizontal">
<NewExplorerCTA />

View File

@ -3,7 +3,7 @@ import styled from 'styled-components';
export const Container = styled(Row)`
&&& {
margin-top: 2rem;
margin-top: 1rem;
min-height: 8vh;
}
`;

View File

@ -63,7 +63,7 @@ function LogDetailedView({
queryString,
);
history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`);
history.replace(`${ROUTES.OLD_LOGS_EXPLORER}?q=${updatedQueryString}`);
},
[history, queryString],
);

View File

@ -74,6 +74,7 @@ function LogsTopNav(): JSX.Element {
icon={<PlayCircleFilled />}
onClick={handleGoLive}
type="primary"
size="small"
>
Go Live
</LiveButtonStyled>

View File

@ -1,19 +1,18 @@
import { Button, ButtonProps } from 'antd';
import { themeColors } from 'constants/theme';
import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
export const LiveButtonStyled = styled(Button)<ButtonProps>`
background-color: rgba(${themeColors.buttonSuccessRgb}, 0.9);
background-color: #1eb475;
${({ danger }): FlattenSimpleInterpolation =>
!danger
? css`
&:hover {
background-color: rgba(${themeColors.buttonSuccessRgb}, 1) !important;
background-color: #1eb475 !important;
}
&:active {
background-color: rgba(${themeColors.buttonSuccessRgb}, 0.7) !important;
background-color: #1eb475 !important;
}
`
: css``}

View File

@ -0,0 +1,5 @@
.flexBtn {
display: flex;
align-items: center;
gap: 8px;
}

View File

@ -1,6 +1,7 @@
import { Button, Space, Typography } from 'antd';
import { Button, Card, Space, Typography } from 'antd';
import changeMyPassword from 'api/user/changeMyPassword';
import { useNotifications } from 'hooks/useNotifications';
import { Save } from 'lucide-react';
import { isPasswordNotValidMessage, isPasswordValid } from 'pages/SignUp/utils';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -20,9 +21,7 @@ function PasswordContainer(): JSX.Element {
false,
);
const defaultPlaceHolder = t('input_password', {
ns: 'settings',
});
const defaultPlaceHolder = '*************';
const { notifications } = useNotifications();
@ -89,8 +88,9 @@ function PasswordContainer(): JSX.Element {
currentPassword === updatePassword;
return (
<Space direction="vertical" size="large">
<Typography.Title level={3}>
<Card>
<Space direction="vertical" size="small">
<Typography.Title level={4} style={{ marginTop: 0 }}>
{t('change_password', {
ns: 'settings',
})}
@ -144,11 +144,13 @@ function PasswordContainer(): JSX.Element {
onClick={onChangePasswordClickHandler}
type="primary"
>
<Save size={12} style={{ marginRight: '8px' }} />{' '}
{t('change_password', {
ns: 'settings',
})}
</Button>
</Space>
</Card>
);
}

View File

@ -0,0 +1,7 @@
.userInfo-label {
min-width: 150px;
}
.userInfo-value {
min-width: 20rem;
}

View File

@ -1,6 +1,10 @@
import { Button, Space, Typography } from 'antd';
import '../MySettings.styles.scss';
import './UserInfo.styles.scss';
import { Button, Card, Flex, Input, Space, Typography } from 'antd';
import editUser from 'api/user/editUser';
import { useNotifications } from 'hooks/useNotifications';
import { PencilIcon, UserSquare } from 'lucide-react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
@ -12,7 +16,7 @@ import AppReducer from 'types/reducer/app';
import { NameInput } from '../styles';
function UpdateName(): JSX.Element {
function UserInfo(): JSX.Element {
const { user, role, org, userFlags } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
@ -72,9 +76,18 @@ function UpdateName(): JSX.Element {
};
return (
<div>
<Card>
<Space direction="vertical" size="middle">
<Typography>Name</Typography>
<Flex gap={8}>
<UserSquare />{' '}
<Typography.Title level={4} style={{ marginTop: 0 }}>
User Details
</Typography.Title>
</Flex>
<Flex gap={16}>
<Space>
<Typography className="userInfo-label">Name</Typography>
<NameInput
placeholder="Your Name"
onChange={(event): void => {
@ -83,17 +96,31 @@ function UpdateName(): JSX.Element {
value={changedName}
disabled={loading}
/>
</Space>
<Button
className="flexBtn"
loading={loading}
disabled={loading}
onClick={onClickUpdateHandler}
type="primary"
>
Update Name
<PencilIcon size={12} /> Update
</Button>
</Flex>
<Space>
<Typography className="userInfo-label"> Email </Typography>
<Input className="userInfo-value" value={user.email} disabled />
</Space>
</div>
<Space>
<Typography className="userInfo-label"> Role </Typography>
<Input className="userInfo-value" value={role || ''} disabled />
</Space>
</Space>
</Card>
);
}
export default UpdateName;
export default UserInfo;

View File

@ -1,16 +1,28 @@
import { Space, Typography } from 'antd';
import { useTranslation } from 'react-i18next';
import './MySettings.styles.scss';
import { Button, Space } from 'antd';
import { Logout } from 'api/utils';
import { LogOut } from 'lucide-react';
import Password from './Password';
import UpdateName from './UpdateName';
import UserInfo from './UserInfo';
function MySettings(): JSX.Element {
const { t } = useTranslation(['routes']);
return (
<Space direction="vertical" size="large">
<Typography.Title level={2}>{t('my_settings')}</Typography.Title>
<UpdateName />
<Space
direction="vertical"
size="large"
style={{
margin: '16px 0',
}}
>
<UserInfo />
<Password />
<Button className="flexBtn" onClick={(): void => Logout()} type="primary">
<LogOut size={12} /> Logout
</Button>
</Space>
);
}

View File

@ -7,5 +7,5 @@ export const RIBBON_STYLES = {
export const buttonText = {
[ROUTES.LOGS_EXPLORER]: 'Switch to Old Logs Explorer',
[ROUTES.TRACE]: 'Try new Traces Explorer',
[ROUTES.LOGS]: 'Switch to New Logs Explorer',
[ROUTES.OLD_LOGS_EXPLORER]: 'Switch to New Logs Explorer',
};

View File

@ -14,16 +14,16 @@ function NewExplorerCTA(): JSX.Element | null {
() =>
location.pathname === ROUTES.LOGS_EXPLORER ||
location.pathname === ROUTES.TRACE ||
location.pathname === ROUTES.LOGS,
location.pathname === ROUTES.OLD_LOGS_EXPLORER,
[location.pathname],
);
const onClickHandler = useCallback((): void => {
if (location.pathname === ROUTES.LOGS_EXPLORER) {
history.push(ROUTES.LOGS);
history.push(ROUTES.OLD_LOGS_EXPLORER);
} else if (location.pathname === ROUTES.TRACE) {
history.push(ROUTES.TRACES_EXPLORER);
} else if (location.pathname === ROUTES.LOGS) {
} else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
history.push(ROUTES.LOGS_EXPLORER);
}
}, [location.pathname]);
@ -36,6 +36,7 @@ function NewExplorerCTA(): JSX.Element | null {
danger
data-testid="newExplorerCTA"
type="primary"
size="small"
>
{buttonText[location.pathname]}
</Button>

View File

@ -32,12 +32,12 @@
.onboardingHeader {
text-align: center;
margin-top: 48px;
margin-bottom: 24px;
}
.onboardingHeader h1 {
font-size: 24px;
font-weight: 500;
margin: 0;
}
.modulesContainer {

View File

@ -6,6 +6,7 @@ import { ArrowRightOutlined } from '@ant-design/icons';
import { Button, Card, Typography } from 'antd';
import getIngestionData from 'api/settings/getIngestionData';
import cx from 'classnames';
import FullViewHeader from 'container/FullViewHeader/FullViewHeader';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useEffect, useState } from 'react';
@ -218,11 +219,10 @@ export default function Onboarding(): JSX.Element {
<div className={cx('container', isDarkMode ? 'darkMode' : 'lightMode')}>
{activeStep === 1 && (
<>
<FullViewHeader />
<div className="onboardingHeader">
<h1>Get Started with SigNoz</h1>
<div> Select a use-case to get started </div>
<h1> Select a use-case to get started</h1>
</div>
<div className="modulesContainer">
<div className="moduleContainerRowStyles">
{Object.keys(ModulesMap).map((module) => {
@ -261,7 +261,6 @@ export default function Onboarding(): JSX.Element {
})}
</div>
</div>
<div className="continue-to-next-step">
<Button type="primary" icon={<ArrowRightOutlined />} onClick={handleNext}>
Get Started

View File

@ -39,6 +39,36 @@
.steps-container {
width: 20%;
height: 100%;
.steps-container-header {
display: flex;
align-items: center;
padding: 16px 0;
margin-bottom: 24px;
.brand-logo {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
cursor: pointer;
img {
height: 24px;
width: 24px;
}
.brand-logo-name {
font-family: 'Work Sans', sans-serif;
font-size: 18px;
font-style: normal;
font-weight: 500;
line-height: 18px;
color: #fff;
}
}
}
}
.selected-step-content {
@ -153,3 +183,18 @@
.error-container {
margin: 8px 0;
}
.lightMode {
.steps-container {
width: 20%;
height: 100%;
.steps-container-header {
.brand-logo {
.brand-logo-name {
color: black;
}
}
}
}
}

View File

@ -1,3 +1,6 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable react/jsx-no-comment-textnodes */
/* eslint-disable sonarjs/prefer-single-boolean-return */
import './ModuleStepsContainer.styles.scss';
@ -135,7 +138,7 @@ export default function ModuleStepsContainer({
if (selectedModule.id === ModulesMap.APM) {
history.push(ROUTES.APPLICATION);
} else if (selectedModule.id === ModulesMap.LogsManagement) {
history.push(ROUTES.LOGS);
history.push(ROUTES.LOGS_EXPLORER);
} else if (selectedModule.id === ModulesMap.InfrastructureMonitoring) {
history.push(ROUTES.APPLICATION);
}
@ -197,9 +200,21 @@ export default function ModuleStepsContainer({
}
};
const handleLogoClick = (): void => {
history.push('/');
};
return (
<div className="onboarding-module-steps">
<div className="steps-container">
<div className="steps-container-header">
<div className="brand-logo" onClick={handleLogoClick}>
<img src="/Logos/signoz-brand-logo.svg" alt="SigNoz" />
<div className="brand-logo-name">SigNoz</div>
</div>
</div>
<Space style={{ marginBottom: '24px' }}>
<Button
style={{ display: 'flex', alignItems: 'center' }}

View File

@ -14,11 +14,11 @@ function OptionRenderer({
const optionType = getOptionType(label);
return (
<span>
<span className="option">
{optionType ? (
<SelectOptionContainer>
<div>{value}</div>
<div>
<div className="option-value">{value}</div>
<div className="option-meta-data-container">
<TagContainer>
<TagLabel>Type: </TagLabel>
<TagValue>{optionType}</TagValue>

View File

@ -18,17 +18,18 @@ export const StyledCheckOutlined = styled(CheckOutlined)`
export const SelectOptionContainer = styled.div`
display: flex;
gap: 8px;
justify-content: space-between;
align-items: center;
overflow-x: auto;
`;
export const TagContainer = styled(Tag)`
&&& {
border-radius: 0.25rem;
padding: 0.063rem 0.5rem;
font-weight: 600;
font-size: 0.75rem;
line-height: 1.25rem;
border-radius: 3px;
padding: 0.3rem 0.3rem;
font-weight: 400;
font-size: 0.6rem;
}
`;

View File

@ -1,5 +1,4 @@
import { Typography } from 'antd';
import { themeColors } from 'constants/theme';
import styled from 'styled-components';
export const Container = styled.div`
@ -9,7 +8,7 @@ export const Container = styled.div`
export const Name = styled(Typography)`
&&& {
font-weight: 600;
color: ${themeColors.lightBlue};
color: #4e74f8;
cursor: pointer;
}
`;

View File

@ -8,7 +8,7 @@ export const Container = styled.div`
export const Name = styled(Typography)`
&&& {
font-weight: 600;
color: #177ddc;
color: #4e74f8;
cursor: pointer;
}
`;

View File

@ -0,0 +1,112 @@
.nav-item {
border-radius: 2px;
display: flex;
flex-direction: row;
align-items: center;
height: 36px;
margin-bottom: 4px;
&.active {
.nav-item-active-marker {
background: #3f5ecc;
}
}
&:hover {
cursor: pointer;
.nav-item-data {
color: white;
background: #121317;
}
}
&.active {
.nav-item-data {
color: white;
background: #121317;
// color: #3f5ecc;
}
}
.nav-item-active-marker {
margin: 8px 0;
width: 8px;
height: 24px;
background: transparent;
border-radius: 3px;
margin-left: -5px;
}
.nav-item-data {
flex-grow: 1;
max-width: calc(100% - 24px);
display: flex;
margin: 0px 8px;
padding: 4px 12px;
flex-direction: row;
align-items: center;
gap: 8px;
align-self: stretch;
color: #c0c1c3;
border-radius: 3px;
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px;
background: transparent;
border-left: 2px solid transparent;
transition: 0.2s all linear;
.nav-item-icon {
height: 16px;
}
.nav-item-label {
// width: 220px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.lightMode {
.nav-item {
&.active {
.nav-item-active-marker {
background: #3f5ecc;
}
}
&:hover {
cursor: pointer;
.nav-item-data {
color: #121317;
background: white;
}
}
&.active {
.nav-item-data {
color: #121317;
background: white;
// color: #4e74f8;
}
}
.nav-item-data {
color: #121317;
}
}
}

View File

@ -0,0 +1,31 @@
import './NavItem.styles.scss';
import cx from 'classnames';
import { SidebarItem } from '../sideNav.types';
export default function NavItem({
isCollapsed,
item,
isActive,
onClick,
}: {
isCollapsed: boolean;
item: SidebarItem;
isActive: boolean;
onClick: () => void;
}): JSX.Element {
const { label, icon } = item;
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div className={cx('nav-item', isActive ? 'active' : '')} onClick={onClick}>
<div className="nav-item-active-marker" />
<div className="nav-item-data">
<div className="nav-item-icon">{icon}</div>
{!isCollapsed && <div className="nav-item-label">{label}</div>}
</div>
</div>
);
}

View File

@ -0,0 +1,172 @@
@import '@signozhq/design-tokens';
.sideNav {
flex: 0 0 240px;
max-width: 240px;
min-width: 240px;
width: 240px;
border-right: 1px solid $bg-slate-400;
padding-bottom: 48px;
transition: all 0.3s, background 0s, border 0s;
position: relative;
.brand {
display: flex;
align-items: center;
gap: 12px;
padding: $padding-4;
.brand-logo {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
img {
height: $font-size-xl;
}
.brand-logo-name {
font-family: 'Work Sans', sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 18px;
color: #fff;
}
}
.license {
&.tag {
box-sizing: border-box;
margin: 0;
padding: 0;
color: rgba(255, 255, 255, 0.85);
font-size: 8px;
font-weight: $font-weight-medium;
letter-spacing: 0.6px;
padding: 4px 8px;
text-transform: uppercase;
white-space: nowrap;
background: $bg-slate-400;
border: 1px solid $bg-slate-400;
border-radius: 20px;
opacity: 1;
transition: all 0.2s;
}
}
}
.get-started-nav-items {
display: flex;
margin: 4px 13px 4px 10px;
.get-started-btn {
display: flex;
align-items: center;
padding: 8px;
margin-left: 2px;
gap: 8px;
width: 100%;
height: 36px;
border: 1px solid $bg-slate-400;
border-radius: 2px;
box-shadow: none !important;
}
}
.secondary-nav-items {
border-top: 1px solid $bg-slate-400;
padding: 8px 0;
max-width: 100%;
position: fixed;
bottom: 0;
left: 0;
width: 240px;
transition: all 0.3s, background 0s, border 0s;
// position: relative;
.collapse-expand-handlers {
position: absolute;
top: -9px;
right: -9px;
cursor: pointer;
display: none;
transition: display 0.3s;
svg {
fill: $bg-vanilla-300;
color: $bg-slate-300;
}
}
}
&.collapsed {
flex: 0 0 64px;
max-width: 64px;
min-width: 64px;
width: 64px;
.secondary-nav-items {
width: 64px;
}
.brand {
justify-content: center;
}
.get-started-nav-items {
.get-started-btn {
justify-content: center;
}
}
}
&:hover {
.collapse-expand-handlers {
display: block;
}
}
}
.lightMode {
.sideNav {
background: $bg-vanilla-300;
border-right: 1px solid $bg-vanilla-400;
.get-started-nav-items {
.get-started-btn {
border: 1px solid $bg-vanilla-400;
}
}
.brand {
.brand-logo {
.brand-logo-name {
color: $bg-slate-400;
}
}
}
.secondary-nav-items {
border-top: 1px solid $bg-vanilla-400;
.collapse-expand-handlers {
svg {
color: $bg-slate-300;
fill: $bg-vanilla-300;
}
}
}
}
}

View File

@ -1,12 +1,26 @@
import { CheckCircleTwoTone, WarningOutlined } from '@ant-design/icons';
import { MenuProps } from 'antd';
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './SideNav.styles.scss';
import { Button } from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get';
import cx from 'classnames';
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
import { FeatureKeys } from 'constants/features';
import ROUTES from 'constants/routes';
import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense';
import { ToggleButton } from 'container/Header/styles';
import useComponentPermission from 'hooks/useComponentPermission';
import useThemeMode, { useIsDarkMode } from 'hooks/useDarkMode';
import { LICENSE_PLAN_KEY, LICENSE_PLAN_STATUS } from 'hooks/useLicense';
import history from 'lib/history';
import { LifeBuoy } from 'lucide-react';
import {
AlertTriangle,
CheckSquare,
ChevronLeftCircle,
ChevronRightCircle,
RocketIcon,
UserCircle,
} from 'lucide-react';
import {
useCallback,
useEffect,
@ -17,44 +31,82 @@ import {
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { sideBarCollapse } from 'store/actions/app';
import { sideBarCollapse } from 'store/actions';
import { AppState } from 'store/reducers';
import { License } from 'types/api/licenses/def';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { checkVersionState, isCloudUser, isEECloudUser } from 'utils/app';
import { routeConfig, styles } from './config';
import { routeConfig } from './config';
import { getQueryString } from './helper';
import defaultMenuItems from './menuItems';
import { MenuItem, SecondaryMenuItemKey } from './sideNav.types';
import defaultMenuItems, {
helpSupportMenuItem,
inviteMemberMenuItem,
manageLicenseMenuItem,
slackSupportMenuItem,
trySignozCloudMenuItem,
} from './menuItems';
import NavItem from './NavItem/NavItem';
import { SecondaryMenuItemKey } from './sideNav.types';
import { getActiveMenuKeyFromPath } from './sideNav.utils';
import Slack from './Slack';
import {
MenuLabelContainer,
RedDot,
Sider,
StyledPrimaryMenu,
StyledSecondaryMenu,
StyledText,
} from './styles';
function SideNav(): JSX.Element {
function SideNav({
licenseData,
isFetching,
}: {
licenseData: any;
isFetching: boolean;
}): JSX.Element {
const dispatch = useDispatch();
const [menuItems, setMenuItems] = useState(defaultMenuItems);
const [collapsed, setCollapsed] = useState<boolean>(
getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
);
const { pathname, search } = useLocation();
const {
user,
role,
featureResponse,
currentVersion,
latestVersion,
isCurrentVersionError,
featureResponse,
} = useSelector<AppState, AppReducer>((state) => state.app);
const { data, isFetching } = useLicense();
const userSettingsMenuItem = {
key: ROUTES.MY_SETTINGS,
label: user?.name || 'User',
icon: <UserCircle size={16} />,
};
let secondaryMenuItems: MenuItem[] = [];
const [userManagementMenuItems, setUserManagementMenuItems] = useState([
manageLicenseMenuItem,
]);
const onClickSlackHandler = (): void => {
window.open('https://signoz.io/slack', '_blank');
};
const onClickVersionHandler = (): void => {
history.push(ROUTES.VERSION);
};
const isLatestVersion = checkVersionState(currentVersion, latestVersion);
const [inviteMembers] = useComponentPermission(['invite_members'], role);
useEffect(() => {
if (inviteMembers) {
const updatedUserManagementMenuItems = [
inviteMemberMenuItem,
manageLicenseMenuItem,
];
setUserManagementMenuItems(updatedUserManagementMenuItems);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inviteMembers]);
useEffect((): void => {
const isOnboardingEnabled =
@ -78,10 +130,10 @@ function SideNav(): JSX.Element {
let items = [...menuItems];
const isOnBasicPlan =
data?.payload?.licenses?.some(
(license) =>
licenseData?.payload?.licenses?.some(
(license: License) =>
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
) || data?.payload?.licenses === null;
) || licenseData?.payload?.licenses === null;
if (role !== USER_ROLES.ADMIN || isOnBasicPlan) {
items = items.filter((item) => item.key !== ROUTES.BILLING);
@ -90,9 +142,7 @@ function SideNav(): JSX.Element {
setMenuItems(items);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data?.payload?.licenses, isFetching, role]);
const { pathname, search } = useLocation();
}, [licenseData?.payload?.licenses, isFetching, role]);
const { t } = useTranslation('');
@ -104,6 +154,26 @@ function SideNav(): JSX.Element {
dispatch(sideBarCollapse(collapsed));
}, [collapsed, dispatch]);
const isLicenseActive =
licenseData?.payload?.licenses?.find((e: License) => e.isCurrent)?.status ===
LICENSE_PLAN_STATUS.VALID;
const isEnterprise = licenseData?.payload?.licenses?.some(
(license: License) =>
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.ENTERPRISE_PLAN,
);
const onClickSignozCloud = (): void => {
window.open(
'https://signoz.io/oss-to-cloud/?utm_source=product_navbar&utm_medium=frontend&utm_campaign=oss_users',
'_blank',
);
};
const onClickGetStarted = (): void => {
history.push(`/get-started`);
};
const onClickHandler = useCallback(
(key: string) => {
const params = new URLSearchParams(search);
@ -118,80 +188,175 @@ function SideNav(): JSX.Element {
[pathname, search],
);
const onClickMenuHandler: MenuProps['onClick'] = (e) => {
onClickHandler(e.key);
};
const onClickSlackHandler = (): void => {
window.open('https://signoz.io/slack', '_blank');
};
const onClickVersionHandler = (): void => {
history.push(ROUTES.VERSION);
};
const isLatestVersion = checkVersionState(currentVersion, latestVersion);
if (isCloudUser() || isEECloudUser()) {
secondaryMenuItems = [
{
key: SecondaryMenuItemKey.Support,
label: 'Support',
icon: <LifeBuoy />,
onClick: onClickMenuHandler,
},
];
} else {
secondaryMenuItems = [
{
key: SecondaryMenuItemKey.Version,
icon: !isLatestVersion ? (
<WarningOutlined style={{ color: '#E87040' }} />
) : (
<CheckCircleTwoTone twoToneColor={['#D5F2BB', '#1f1f1f']} />
),
label: (
<MenuLabelContainer>
<StyledText ellipsis>
{!isCurrentVersionError ? currentVersion : t('n_a')}
</StyledText>
{!isLatestVersion && <RedDot />}
</MenuLabelContainer>
),
onClick: onClickVersionHandler,
},
{
key: SecondaryMenuItemKey.Slack,
icon: <Slack />,
label: <StyledText>Support</StyledText>,
onClick: onClickSlackHandler,
},
];
}
const activeMenuKey = useMemo(() => getActiveMenuKeyFromPath(pathname), [
pathname,
]);
const isDarkMode = useIsDarkMode();
const { toggleTheme } = useThemeMode();
const isCloudUserVal = isCloudUser();
useEffect(() => {
if (isCloudUser() || isEECloudUser()) {
const updatedUserManagementMenuItems = [
helpSupportMenuItem,
manageLicenseMenuItem,
];
setUserManagementMenuItems(updatedUserManagementMenuItems);
} else if (currentVersion && latestVersion) {
const versionMenuItem = {
key: SecondaryMenuItemKey.Version,
label: !isCurrentVersionError ? currentVersion : t('n_a'),
icon: !isLatestVersion ? (
<AlertTriangle color="#E87040" size={16} />
) : (
<CheckSquare color="#D5F2BB" size={16} />
),
onClick: onClickVersionHandler,
};
const updatedUserManagementMenuItems = [
versionMenuItem,
slackSupportMenuItem,
manageLicenseMenuItem,
];
setUserManagementMenuItems(updatedUserManagementMenuItems);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentVersion, latestVersion]);
const handleUserManagentMenuItemClick = (key: string): void => {
switch (key) {
case SecondaryMenuItemKey.Slack:
onClickSlackHandler();
break;
case SecondaryMenuItemKey.Version:
onClickVersionHandler();
break;
default:
onClickHandler(key);
break;
}
};
return (
<Sider collapsible collapsed={collapsed} onCollapse={onCollapse} width={200}>
<StyledPrimaryMenu
theme="dark"
defaultSelectedKeys={[ROUTES.APPLICATION]}
selectedKeys={activeMenuKey ? [activeMenuKey] : []}
mode="vertical"
style={styles}
items={menuItems}
onClick={onClickMenuHandler}
<div className={cx('sideNav', collapsed ? 'collapsed' : '')}>
<div className="brand">
<div
className="brand-logo"
// eslint-disable-next-line react/no-unknown-property
onClick={(): void => {
// Current home page
onClickHandler(ROUTES.APPLICATION);
}}
>
<img src="/Logos/signoz-brand-logo.svg" alt="SigNoz" />
{!collapsed && <span className="brand-logo-name"> SigNoz </span>}
</div>
{!collapsed && (
<>
<div className="license tag">{!isEnterprise ? 'Free' : 'Enterprise'}</div>
<ToggleButton
checked={isDarkMode}
onChange={toggleTheme}
defaultChecked={isDarkMode}
checkedChildren="🌜"
unCheckedChildren="🌞"
/>
<StyledSecondaryMenu
theme="dark"
selectedKeys={activeMenuKey ? [activeMenuKey] : []}
mode="vertical"
style={styles}
items={secondaryMenuItems}
</>
)}
</div>
{isCloudUserVal && (
<div className="get-started-nav-items">
<Button className="get-started-btn" onClick={onClickGetStarted}>
<RocketIcon size={16} />
{!collapsed && <> Get Started </>}
</Button>
</div>
)}
<div className="primary-nav-items">
{menuItems.map((item, index) => (
<NavItem
isCollapsed={collapsed}
key={item.key || index}
item={item}
isActive={activeMenuKey === item.key}
onClick={(): void => {
if (item) {
onClickHandler(item?.key as string);
}
}}
/>
</Sider>
))}
</div>
<div className="secondary-nav-items">
{licenseData && !isLicenseActive && (
<NavItem
isCollapsed={collapsed}
key="trySignozCloud"
item={trySignozCloudMenuItem}
isActive={false}
onClick={onClickSignozCloud}
/>
)}
{userManagementMenuItems.map(
(item, index): JSX.Element => (
<NavItem
isCollapsed={collapsed}
key={item?.key || index}
item={item}
isActive={activeMenuKey === item?.key}
onClick={(): void => {
handleUserManagentMenuItemClick(item?.key as string);
}}
/>
),
)}
{inviteMembers && (
<NavItem
isCollapsed={collapsed}
key={inviteMemberMenuItem.key}
item={inviteMemberMenuItem}
isActive={activeMenuKey === inviteMemberMenuItem?.key}
onClick={(): void => {
history.push(`${inviteMemberMenuItem.key}`);
}}
/>
)}
{user && (
<NavItem
isCollapsed={collapsed}
key={ROUTES.MY_SETTINGS}
item={userSettingsMenuItem}
isActive={activeMenuKey === userSettingsMenuItem?.key}
onClick={(): void => {
handleUserManagentMenuItemClick(userSettingsMenuItem?.key as string);
}}
/>
)}
<div className="collapse-expand-handlers" onClick={onCollapse}>
{collapsed ? (
<ChevronRightCircle size={18} />
) : (
<ChevronLeftCircle size={18} />
)}
</div>
</div>
</div>
);
}

View File

@ -31,6 +31,7 @@ export const routeConfig: Record<string, QueryParams[]> = {
[ROUTES.LIST_LICENSES]: [QueryParams.resourceAttributes],
[ROUTES.LOGIN]: [QueryParams.resourceAttributes],
[ROUTES.LOGS]: [QueryParams.resourceAttributes],
[ROUTES.LOGS_BASE]: [QueryParams.resourceAttributes],
[ROUTES.MY_SETTINGS]: [QueryParams.resourceAttributes],
[ROUTES.NOT_FOUND]: [QueryParams.resourceAttributes],
[ROUTES.ORG_SETTINGS]: [QueryParams.resourceAttributes],

View File

@ -1,88 +1,111 @@
import {
AlertOutlined,
AlignLeftOutlined,
BarChartOutlined,
BugOutlined,
DashboardFilled,
DeploymentUnitOutlined,
FileDoneOutlined,
LineChartOutlined,
MenuOutlined,
RocketOutlined,
SearchOutlined,
SettingOutlined,
} from '@ant-design/icons';
import { RocketOutlined } from '@ant-design/icons';
import ROUTES from 'constants/routes';
import {
AreaChart,
BarChart2,
BellDot,
BugIcon,
Cloudy,
DraftingCompass,
FileKey2,
LayoutGrid,
MessageSquare,
Receipt,
Route,
ScrollText,
Settings,
Slack,
UserPlus,
} from 'lucide-react';
import { SidebarMenu } from './sideNav.types';
import { SecondaryMenuItemKey, SidebarItem } from './sideNav.types';
const menuItems: SidebarMenu[] = [
{
export const getStartedMenuItem = {
key: ROUTES.GET_STARTED,
label: 'Get Started',
icon: <RocketOutlined rotate={45} />,
},
};
export const inviteMemberMenuItem = {
key: `${ROUTES.ORG_SETTINGS}#invite-team-members`,
label: 'Invite Team Member',
icon: <UserPlus size={16} />,
};
export const manageLicenseMenuItem = {
key: ROUTES.LIST_LICENSES,
label: 'Manage Licenses',
icon: <FileKey2 size={16} />,
};
export const helpSupportMenuItem = {
key: ROUTES.SUPPORT,
label: 'Help & Support',
icon: <MessageSquare size={16} />,
};
export const slackSupportMenuItem = {
key: SecondaryMenuItemKey.Slack,
label: 'Slack Support',
icon: <Slack size={16} />,
};
export const trySignozCloudMenuItem: SidebarItem = {
key: 'trySignozCloud',
label: 'Try Signoz Cloud',
icon: <Cloudy size={16} />,
};
const menuItems: SidebarItem[] = [
{
key: ROUTES.APPLICATION,
label: 'Services',
icon: <BarChartOutlined />,
icon: <BarChart2 size={16} />,
},
{
key: ROUTES.TRACE,
label: 'Traces',
icon: <MenuOutlined />,
icon: <DraftingCompass size={16} />,
},
{
key: ROUTES.LOGS_EXPLORER,
key: ROUTES.LOGS,
label: 'Logs',
icon: <AlignLeftOutlined />,
children: [
{
key: ROUTES.LOGS_EXPLORER,
icon: <SearchOutlined />,
label: 'Logs Explorer',
},
{
key: ROUTES.LOGS_PIPELINES,
icon: <DeploymentUnitOutlined />,
label: 'Logs Pipelines',
},
],
icon: <ScrollText size={16} />,
},
{
key: ROUTES.ALL_DASHBOARD,
label: 'Dashboards',
icon: <DashboardFilled />,
icon: <LayoutGrid size={16} />,
},
{
key: ROUTES.LIST_ALL_ALERT,
label: 'Alerts',
icon: <AlertOutlined />,
icon: <BellDot size={16} />,
},
{
key: ROUTES.ALL_ERROR,
label: 'Exceptions',
icon: <BugOutlined />,
icon: <BugIcon size={16} />,
},
{
key: ROUTES.SERVICE_MAP,
label: 'Service Map',
icon: <DeploymentUnitOutlined />,
icon: <Route size={16} />,
},
{
key: ROUTES.USAGE_EXPLORER,
label: 'Usage Explorer',
icon: <LineChartOutlined />,
icon: <AreaChart size={16} />,
},
{
key: ROUTES.BILLING,
label: 'Billing',
icon: <FileDoneOutlined />,
icon: <Receipt size={16} />,
},
{
key: ROUTES.SETTINGS,
label: 'Settings',
icon: <SettingOutlined />,
icon: <Settings size={16} />,
},
];
@ -90,7 +113,7 @@ const menuItems: SidebarMenu[] = [
export const NEW_ROUTES_MENU_ITEM_KEY_MAP = {
[ROUTES.TRACES_EXPLORER]: ROUTES.TRACE,
[ROUTES.TRACE_EXPLORER]: ROUTES.TRACE,
[ROUTES.LOGS_EXPLORER]: ROUTES.LOGS_EXPLORER,
[ROUTES.LOGS_BASE]: ROUTES.LOGS_EXPLORER,
};
export default menuItems;

View File

@ -8,10 +8,9 @@ export type SidebarMenu = MenuItem & {
};
export interface SidebarItem {
onClick: VoidFunction;
icon?: ReactNode;
text?: ReactNode;
key: string;
key: string | number;
label?: ReactNode;
}

View File

@ -22,6 +22,7 @@ const breadcrumbNameMap = {
[ROUTES.ALL_DASHBOARD]: 'Dashboard',
[ROUTES.LOGS]: 'Logs',
[ROUTES.LOGS_EXPLORER]: 'Logs Explorer',
[ROUTES.OLD_LOGS_EXPLORER]: 'Old Logs Explorer',
[ROUTES.LIVE_LOGS]: 'Live View',
[ROUTES.LOGS_PIPELINES]: 'Logs Pipelines',
[ROUTES.BILLING]: 'Billing',

View File

@ -90,6 +90,9 @@ export const routesToSkip = [
ROUTES.BILLING,
ROUTES.SUPPORT,
ROUTES.WORKSPACE_LOCKED,
ROUTES.LOGS,
ROUTES.MY_SETTINGS,
ROUTES.LIST_LICENSES,
];
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];

View File

@ -4,14 +4,8 @@ import { useMemo } from 'react';
import { matchPath, useHistory } from 'react-router-dom';
import NewExplorerCTA from '../NewExplorerCTA';
import ShowBreadcrumbs from './Breadcrumbs';
import DateTimeSelector from './DateTimeSelection';
import {
routesToDisable,
routesToHideBreadCrumbs,
routesToSkip,
} from './DateTimeSelection/config';
import { Container } from './styles';
import { routesToDisable, routesToSkip } from './DateTimeSelection/config';
function TopNav(): JSX.Element | null {
const { location } = useHistory();
@ -24,14 +18,6 @@ function TopNav(): JSX.Element | null {
[location.pathname],
);
const isRouteToHideBreadCrumbs = useMemo(
() =>
routesToHideBreadCrumbs.some((route) =>
matchPath(location.pathname, { path: route, exact: true }),
),
[location.pathname],
);
const isDisabled = useMemo(
() =>
routesToDisable.some((route) =>
@ -50,15 +36,9 @@ function TopNav(): JSX.Element | null {
}
return (
<Container>
{!isRouteToHideBreadCrumbs && (
<Col span={16}>
<ShowBreadcrumbs />
</Col>
)}
<Row>
{!isRouteToSkip && (
<Col span={isRouteToHideBreadCrumbs ? 24 : 8}>
<Col span={24} style={{ marginTop: '1rem' }}>
<Row justify="end">
<Space align="start" size={60} direction="horizontal">
<NewExplorerCTA />
@ -69,7 +49,7 @@ function TopNav(): JSX.Element | null {
</Row>
</Col>
)}
</Container>
</Row>
);
}

View File

@ -3,6 +3,6 @@ import styled from 'styled-components';
export const Container = styled(Row)`
&&& {
margin-top: 2rem;
margin-top: 1rem;
}
`;

View File

@ -36,7 +36,9 @@ export const useActiveLog = (): UseActiveLog => {
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const { notifications } = useNotifications();
const isLogsPage = useMemo(() => pathname === ROUTES.LOGS, [pathname]);
const isLogsPage = useMemo(() => pathname === ROUTES.OLD_LOGS_EXPLORER, [
pathname,
]);
const [activeLog, setActiveLog] = useState<ILog | null>(null);
@ -135,7 +137,7 @@ export const useActiveLog = (): UseActiveLog => {
queryString,
);
history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`);
history.replace(`${ROUTES.OLD_LOGS_EXPLORER}?q=${updatedQueryString}`);
},
[history, queryString],
);

View File

@ -76,6 +76,11 @@ export const useThemeConfig = (): ThemeConfig => {
borderRadiusXS: 2,
fontFamily: 'Inter',
fontSize: 13,
colorPrimary: '#4E74F8',
colorBgBase: isDarkMode ? '#0B0C0E' : '#fff',
colorBgContainer: isDarkMode ? '#121317' : '#fff',
colorLink: '#4E74F8',
colorPrimaryText: '#3F5ECC',
},
};
};

View File

@ -67,7 +67,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap"
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&family=Work+Sans:wght@500&display=swap"
rel="stylesheet"
/>
</head>

View File

@ -8,7 +8,6 @@ import { createRoot } from 'react-dom/client';
import { ErrorBoundary } from 'react-error-boundary';
import { HelmetProvider } from 'react-helmet-async';
import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
import { Provider } from 'react-redux';
import store from 'store';
@ -33,7 +32,6 @@ if (container) {
<Provider store={store}>
<AppRoutes />
</Provider>
{process.env.NODE_ENV === 'development' && <ReactQueryDevtools />}
</QueryClientProvider>
</ThemeProvider>
</HelmetProvider>

View File

@ -30,7 +30,7 @@ import { useSelectedLogView } from './hooks';
import PopoverContent from './PopoverContent';
import SpaceContainer from './styles';
function Logs(): JSX.Element {
function OldLogsExplorer(): JSX.Element {
const dispatch = useDispatch<Dispatch<AppActions>>();
const { order } = useSelector<AppState, ILogsReducer>((store) => store.logs);
const location = useLocation();
@ -148,4 +148,4 @@ function Logs(): JSX.Element {
);
}
export default Logs;
export default OldLogsExplorer;

View File

@ -0,0 +1,28 @@
import RouteTab from 'components/RouteTab';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import LogsExplorer from 'pages/LogsExplorer';
import Pipelines from 'pages/Pipelines';
import { useLocation } from 'react-use';
export const logsExplorer = {
Component: LogsExplorer,
name: 'Explorer',
route: ROUTES.LOGS,
key: ROUTES.LOGS,
};
export const logsPipelines = {
Component: Pipelines,
name: 'Pipelines',
route: ROUTES.LOGS_PIPELINES,
key: ROUTES.LOGS_PIPELINES,
};
export default function LogsModulePage(): JSX.Element {
const { pathname } = useLocation();
const routes = [logsExplorer, logsPipelines];
return <RouteTab routes={routes} activeKey={pathname} history={history} />;
}

View File

@ -0,0 +1,3 @@
import LogsModulePage from './LogsModulePage';
export default LogsModulePage;

View File

@ -81,7 +81,7 @@ function Pipelines(): JSX.Element {
return (
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
<Tabs defaultActiveKey="pipelines" items={tabItems} />;
<Tabs defaultActiveKey="pipelines" items={tabItems} />
</ErrorBoundary>
);
}

View File

@ -1,7 +1,7 @@
.workspace-locked-container {
text-align: center;
padding: 48px;
margin: 48px;
margin: 24px;
}
.workpace-locked-details {

View File

@ -6,6 +6,7 @@ import { Button, Card, Skeleton, Typography } from 'antd';
import updateCreditCardApi from 'api/billing/checkout';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import ROUTES from 'constants/routes';
import FullViewHeader from 'container/FullViewHeader/FullViewHeader';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
@ -75,6 +76,9 @@ export default function WorkspaceBlocked(): JSX.Element {
}, [activeLicense?.key, updateCreditCard]);
return (
<>
<FullViewHeader overrideRoute={ROUTES.WORKSPACE_LOCKED} />
<Card className="workspace-locked-container">
{isLoadingLicenseData || !licensesData?.payload?.workSpaceBlock ? (
<Skeleton />
@ -112,5 +116,6 @@ export default function WorkspaceBlocked(): JSX.Element {
</>
)}
</Card>
</>
);
}

View File

@ -1,3 +1,5 @@
@import '@signozhq/design-tokens';
#root,
html,
body {

View File

@ -86,4 +86,6 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
BILLING: ['ADMIN', 'EDITOR', 'VIEWER'],
SUPPORT: ['ADMIN', 'EDITOR', 'VIEWER'],
SOMETHING_WENT_WRONG: ['ADMIN', 'EDITOR', 'VIEWER'],
LOGS_BASE: [],
OLD_LOGS_EXPLORER: [],
};

View File

@ -3082,6 +3082,13 @@
resolved "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz"
integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==
"@signozhq/design-tokens@0.0.6":
version "0.0.6"
resolved "https://registry.yarnpkg.com/@signozhq/design-tokens/-/design-tokens-0.0.6.tgz#42449052dca644c4d52448f9c2c521d39e535720"
integrity sha512-i+aG0YCuYL2KVUtRFj3qgAVDU6GbKmTdFXpqCqLUQp8diKMWH5Svzzxj4B14Q6+yE79+wbm1iZ0Nr6nYgkBA8Q==
dependencies:
style-dictionary "3.8.0"
"@sinclair/typebox@^0.25.16":
version "0.25.24"
resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz"
@ -5422,6 +5429,15 @@ canvas-color-tracker@1:
dependencies:
tinycolor2 "^1.6.0"
capital-case@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669"
integrity sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==
dependencies:
no-case "^3.0.4"
tslib "^2.0.3"
upper-case-first "^2.0.2"
cardboard-vr-display@^1.0.19:
version "1.0.19"
resolved "https://registry.npmjs.org/cardboard-vr-display/-/cardboard-vr-display-1.0.19.tgz"
@ -5461,6 +5477,24 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
change-case@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/change-case/-/change-case-4.1.2.tgz#fedfc5f136045e2398c0410ee441f95704641e12"
integrity sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==
dependencies:
camel-case "^4.1.2"
capital-case "^1.0.4"
constant-case "^3.0.4"
dot-case "^3.0.4"
header-case "^2.0.4"
no-case "^3.0.4"
param-case "^3.0.4"
pascal-case "^3.1.2"
path-case "^3.0.4"
sentence-case "^3.0.4"
snake-case "^3.0.4"
tslib "^2.0.3"
char-regex@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz"
@ -5842,6 +5876,15 @@ connect-history-api-fallback@^2.0.0:
resolved "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz"
integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==
constant-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-3.0.4.tgz#3b84a9aeaf4cf31ec45e6bf5de91bdfb0589faf1"
integrity sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==
dependencies:
no-case "^3.0.4"
tslib "^2.0.3"
upper-case "^2.0.2"
content-disposition@0.5.4:
version "0.5.4"
resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz"
@ -7993,7 +8036,7 @@ glob-to-regexp@^0.4.1:
resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0:
version "7.2.3"
resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
@ -8329,6 +8372,14 @@ he@^1.2.0:
resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
header-case@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/header-case/-/header-case-2.0.4.tgz#5a42e63b55177349cf405beb8d775acabb92c063"
integrity sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==
dependencies:
capital-case "^1.0.4"
tslib "^2.0.3"
headers-polyfill@3.2.5:
version "3.2.5"
resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-3.2.5.tgz#6e67d392c9d113d37448fe45014e0afdd168faed"
@ -9871,6 +9922,11 @@ json5@^1.0.2:
dependencies:
minimist "^1.2.0"
jsonc-parser@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76"
integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz"
@ -11789,6 +11845,14 @@ pascal-case@^3.1.2:
no-case "^3.0.4"
tslib "^2.0.3"
path-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/path-case/-/path-case-3.0.4.tgz#9168645334eb942658375c56f80b4c0cb5f82c6f"
integrity sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==
dependencies:
dot-case "^3.0.4"
tslib "^2.0.3"
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
@ -13841,6 +13905,15 @@ send@0.18.0:
range-parser "~1.2.1"
statuses "2.0.1"
sentence-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-3.0.4.tgz#3645a7b8c117c787fde8702056225bb62a45131f"
integrity sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==
dependencies:
no-case "^3.0.4"
tslib "^2.0.3"
upper-case-first "^2.0.2"
serialize-javascript@^5.0.1:
version "5.0.1"
resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz"
@ -14017,6 +14090,14 @@ slice-ansi@^5.0.0:
ansi-styles "^6.0.0"
is-fullwidth-code-point "^4.0.0"
snake-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"
integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==
dependencies:
dot-case "^3.0.4"
tslib "^2.0.3"
sockjs@^0.3.24:
version "0.3.24"
resolved "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz"
@ -14397,6 +14478,21 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
style-dictionary@3.8.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/style-dictionary/-/style-dictionary-3.8.0.tgz#7cb8d64360c53431f768d44def665f61e971a73e"
integrity sha512-wHlB/f5eO3mDcYv6WtOz6gvQC477jBKrwuIXe+PtHskTCBsJdAOvL8hCquczJxDui2TnwpeNE+2msK91JJomZg==
dependencies:
chalk "^4.0.0"
change-case "^4.1.2"
commander "^8.3.0"
fs-extra "^10.0.0"
glob "^7.2.0"
json5 "^2.2.2"
jsonc-parser "^3.0.0"
lodash "^4.17.15"
tinycolor2 "^1.4.1"
style-loader@1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/style-loader/-/style-loader-1.3.0.tgz"
@ -14698,7 +14794,7 @@ tiny-warning@^1.0.0:
resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tinycolor2@1, tinycolor2@1.6.0, tinycolor2@^1.6.0:
tinycolor2@1, tinycolor2@1.6.0, tinycolor2@^1.4.1, tinycolor2@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e"
integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==
@ -15156,6 +15252,20 @@ uplot@1.6.26:
resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.6.26.tgz#a6012fd141ad4a71741c75af0c71283d0ade45a7"
integrity sha512-qN0mveL6UsP40TnHzHAJkUQvpfA3y8zSLXtXKVlJo/sLfj2+vjan/Z3g81MCZjy/hEDUFNtnLftPmETDA4s7Rg==
upper-case-first@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-2.0.2.tgz#992c3273f882abd19d1e02894cc147117f844324"
integrity sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==
dependencies:
tslib "^2.0.3"
upper-case@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-2.0.2.tgz#d89810823faab1df1549b7d97a76f8662bae6f7a"
integrity sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==
dependencies:
tslib "^2.0.3"
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz"