feat: version page is added (#924)

* feat👔 : getLatestVersion api is added

* chore: VERSION page is added

* feat:  version page is added

* chore: all string is grabbed from locale

* chore: warning is removed

* chore: translation json is added

* chore: feedback about version is added

* chore: made two different functions

* unused import is removed

* feat: version changes are updated

* chore: if current version is present then it is displayed
This commit is contained in:
palash-signoz 2022-04-05 18:21:25 +05:30 committed by GitHub
parent 5744193f50
commit 2a348e916c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 433 additions and 12 deletions

View File

@ -1,5 +1,14 @@
{
"monitor_signup": "Monitor your applications. Find what is causing issues.",
"version": "Version",
"latest_version": "Latest version",
"current_version": "Current version",
"release_notes": "Release Notes",
"read_how_to_upgrade": "Read instructions on how to upgrade",
"latest_version_signoz": "You are running the latest version of SigNoz.",
"stale_version": "You are on an older version and may be loosing on the latest features we have shipped. We recommend to upgrade to the latest version",
"oops_something_went_wrong_version": "Oops.. facing issues with fetching updated version information",
"n_a": "N/A",
"routes": {
"general": "General",
"alert_channels": "Alert Channels"

View File

@ -85,3 +85,7 @@ export const EditAlertChannelsAlerts = Loadable(
export const AllAlertChannels = Loadable(
() => import(/* webpackChunkName: "All Channels" */ 'pages/AllAlertChannels'),
);
export const StatusPage = Loadable(
() => import(/* webpackChunkName: "All Status" */ 'pages/Status'),
);

View File

@ -17,6 +17,7 @@ import {
ServicesTablePage,
SettingsPage,
SignupPage,
StatusPage,
TraceDetail,
TraceFilter,
UsageExplorerPage,
@ -113,6 +114,11 @@ const routes: AppRoutes[] = [
exact: true,
component: AllAlertChannels,
},
{
path: ROUTES.VERSION,
exact: true,
component: StatusPage,
},
];
interface AppRoutes {

View File

@ -0,0 +1,25 @@
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import axios, { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/user/getLatestVersion';
const getLatestVersion = async (): Promise<
SuccessResponse<PayloadProps> | ErrorResponse
> => {
try {
const response = await axios.get(
`https://api.github.com/repos/signoz/signoz/releases/latest`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getLatestVersion;

View File

@ -17,6 +17,7 @@ const ROUTES = {
ALL_CHANNELS: '/settings/channels',
CHANNELS_NEW: '/setting/channels/new',
CHANNELS_EDIT: '/setting/channels/edit/:id',
VERSION: '/status',
};
export default ROUTES;

View File

@ -1,11 +1,24 @@
import { notification } from 'antd';
import getLatestVersion from 'api/user/getLatestVersion';
import getVersion from 'api/user/getVersion';
import ROUTES from 'constants/routes';
import TopNav from 'container/Header';
import SideNav from 'container/SideNav';
import useFetch from 'hooks/useFetch';
import history from 'lib/history';
import React, { ReactNode, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import React, { ReactNode, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
UPDATE_CURRENT_ERROR,
UPDATE_CURRENT_VERSION,
UPDATE_LATEST_VERSION,
UPDATE_LATEST_VERSION_ERROR,
} from 'types/actions/app';
import AppReducer from 'types/reducer/app';
import { Content, Layout } from './styles';
@ -13,11 +26,24 @@ import { Content, Layout } from './styles';
function AppLayout(props: AppLayoutProps): JSX.Element {
const { isLoggedIn } = useSelector<AppState, AppReducer>((state) => state.app);
const { pathname } = useLocation();
const { t } = useTranslation();
const [isSignUpPage, setIsSignUpPage] = useState(ROUTES.SIGN_UP === pathname);
const { payload: versionPayload, loading, error: getVersionError } = useFetch(
getVersion,
);
const {
payload: latestVersionPayload,
loading: latestLoading,
error: latestError,
} = useFetch(getLatestVersion);
const { children } = props;
const dispatch = useDispatch<Dispatch<AppActions>>();
useEffect(() => {
if (!isLoggedIn) {
setIsSignUpPage(true);
@ -27,11 +53,71 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
}
}, [isLoggedIn, isSignUpPage]);
const latestCurrentCounter = useRef(0);
const latestVersionCounter = useRef(0);
useEffect(() => {
if (isLoggedIn && pathname === ROUTES.SIGN_UP) {
history.push(ROUTES.APPLICATION);
}
}, [isLoggedIn, pathname]);
if (!latestLoading && latestError && latestCurrentCounter.current === 0) {
latestCurrentCounter.current = 1;
dispatch({
type: UPDATE_LATEST_VERSION_ERROR,
payload: {
isError: true,
},
});
notification.error({
message: t('oops_something_went_wrong_version'),
});
}
if (!loading && getVersionError && latestVersionCounter.current === 0) {
latestVersionCounter.current = 1;
dispatch({
type: UPDATE_CURRENT_ERROR,
payload: {
isError: true,
},
});
notification.error({
message: t('oops_something_went_wrong_version'),
});
}
if (!latestLoading && versionPayload) {
dispatch({
type: UPDATE_CURRENT_VERSION,
payload: {
currentVersion: versionPayload.version,
},
});
}
if (!loading && latestVersionPayload) {
dispatch({
type: UPDATE_LATEST_VERSION,
payload: {
latestVersion: latestVersionPayload.name,
},
});
}
}, [
dispatch,
loading,
latestLoading,
versionPayload,
latestVersionPayload,
isLoggedIn,
pathname,
getVersionError,
latestError,
t,
]);
return (
<Layout>

View File

@ -11,6 +11,7 @@ const breadcrumbNameMap = {
[ROUTES.INSTRUMENTATION]: 'Add instrumentation',
[ROUTES.SETTINGS]: 'Settings',
[ROUTES.DASHBOARD]: 'Dashboard',
[ROUTES.VERSION]: 'Status',
};
function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element {

View File

@ -1,3 +1,4 @@
import { CheckCircleTwoTone, WarningOutlined } from '@ant-design/icons';
import { Menu, Typography } from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get';
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
@ -5,6 +6,7 @@ import ROUTES from 'constants/routes';
import history from 'lib/history';
import setTheme, { AppMode } from 'lib/theme/setTheme';
import React, { useCallback, useLayoutEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { connect, useDispatch, useSelector } from 'react-redux';
import { NavLink, useLocation } from 'react-router-dom';
import { bindActionCreators } from 'redux';
@ -19,11 +21,13 @@ import menus from './menuItems';
import Slack from './Slack';
import {
Logo,
RedDot,
Sider,
SlackButton,
SlackMenuItemContainer,
ThemeSwitcherWrapper,
ToggleButton,
VersionContainer,
} from './styles';
function SideNav({ toggleDarkMode }: Props): JSX.Element {
@ -31,9 +35,15 @@ function SideNav({ toggleDarkMode }: Props): JSX.Element {
const [collapsed, setCollapsed] = useState<boolean>(
getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const {
isDarkMode,
currentVersion,
latestVersion,
isCurrentVersionError,
} = useSelector<AppState, AppReducer>((state) => state.app);
const { pathname } = useLocation();
const { t } = useTranslation('');
const toggleTheme = useCallback(() => {
const preMode: AppMode = isDarkMode ? 'lightMode' : 'darkMode';
@ -77,6 +87,38 @@ function SideNav({ toggleDarkMode }: Props): JSX.Element {
window.open('https://signoz.io/slack', '_blank');
};
const onClickVersionHandler = (): void => {
history.push(ROUTES.VERSION);
};
const isNotCurrentVersion = currentVersion !== latestVersion;
const sidebar = [
{
onClick: onClickSlackHandler,
icon: <Slack />,
text: <SlackButton>Support</SlackButton>,
},
{
onClick: onClickVersionHandler,
icon: isNotCurrentVersion ? (
<WarningOutlined style={{ color: '#E87040' }} />
) : (
<CheckCircleTwoTone twoToneColor={['#D5F2BB', '#1f1f1f']} />
),
text: (
<VersionContainer>
{!isCurrentVersionError ? (
<SlackButton>{currentVersion}</SlackButton>
) : (
<SlackButton>{t('n_a')}</SlackButton>
)}
{isNotCurrentVersion && <RedDot />}
</VersionContainer>
),
},
];
return (
<Sider collapsible collapsed={collapsed} onCollapse={onCollapse} width={200}>
<ThemeSwitcherWrapper>
@ -87,7 +129,7 @@ function SideNav({ toggleDarkMode }: Props): JSX.Element {
/>
</ThemeSwitcherWrapper>
<NavLink to={ROUTES.APPLICATION}>
<Logo src="/signoz.svg" alt="SigNoz" collapsed={collapsed} />
<Logo index={0} src="/signoz.svg" alt="SigNoz" collapsed={collapsed} />
</NavLink>
<Menu
@ -105,11 +147,21 @@ function SideNav({ toggleDarkMode }: Props): JSX.Element {
<Typography>{name}</Typography>
</Menu.Item>
))}
<SlackMenuItemContainer collapsed={collapsed}>
<Menu.Item onClick={onClickSlackHandler} icon={<Slack />}>
<SlackButton>Support</SlackButton>
</Menu.Item>
</SlackMenuItemContainer>
{sidebar.map((props, index) => (
<SlackMenuItemContainer
index={index + 1}
key={`${index + 1}`}
collapsed={collapsed}
>
<Menu.Item
eventKey={index.toString()}
onClick={props.onClick}
icon={props.icon}
>
{props.text}
</Menu.Item>
</SlackMenuItemContainer>
))}
</Menu>
</Sider>
);

View File

@ -19,6 +19,7 @@ export const Logo = styled.img<LogoProps>`
interface LogoProps {
collapsed: boolean;
index: number;
}
export const Sider = styled(SiderComponent)`
@ -50,9 +51,10 @@ export const SlackButton = styled(Typography)`
export const SlackMenuItemContainer = styled.div<LogoProps>`
position: fixed;
bottom: 48px;
bottom: ${({ index }): string => `${index * 48 + (index + 16)}px`};
background: #262626;
width: ${({ collapsed }): string => (!collapsed ? '200px' : '80px')};
transition: inherit;
&&& {
li {
@ -60,11 +62,14 @@ export const SlackMenuItemContainer = styled.div<LogoProps>`
collapsed &&
css`
padding-left: 24px;
padding-top: 6px;
`}
}
svg {
margin-left: ${({ collapsed }): string => (collapsed ? '0' : '24px')};
width: 28px;
height: 28px;
${({ collapsed }): StyledCSS =>
collapsed &&
@ -73,5 +78,24 @@ export const SlackMenuItemContainer = styled.div<LogoProps>`
margin: 0 auto;
`}
}
.ant-menu-title-content {
margin: 0;
}
}
`;
export const RedDot = styled.div`
width: 12px;
height: 12px;
background: #d32029;
border-radius: 50%;
margin-left: 1rem;
margin-top: 0.5rem;
`;
export const VersionContainer = styled.div`
&&& {
display: flex;
}
`;

View File

@ -0,0 +1,110 @@
import { WarningFilled } from '@ant-design/icons';
import { Button, Card, Form, Space, Typography } from 'antd';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import { InputComponent } from './styles';
const { Title } = Typography;
function Version(): JSX.Element {
const [form] = Form.useForm();
const { t } = useTranslation();
const onClickUpgradeHandler = useCallback((link: string) => {
window.open(link, '_blank');
}, []);
const {
currentVersion,
latestVersion,
isCurrentVersionError,
isLatestVersionError,
} = useSelector<AppState, AppReducer>((state) => state.app);
const isLatestVersion = currentVersion === latestVersion;
const isError = isCurrentVersionError || isLatestVersionError;
return (
<Card>
<Title ellipsis level={4}>
{t('version')}
</Title>
<Form
wrapperCol={{
span: 14,
}}
labelCol={{
span: 3,
}}
layout="horizontal"
form={form}
labelAlign="left"
>
<Form.Item label={t('current_version')}>
<InputComponent
readOnly
value={isCurrentVersionError ? t('n_a').toString() : currentVersion}
placeholder={t('current_version')}
/>
</Form.Item>
<Form.Item label={t('latest_version')}>
<InputComponent
readOnly
value={isLatestVersionError ? t('n_a').toString() : latestVersion}
placeholder={t('latest_version')}
/>
<Button
onClick={(): void =>
onClickUpgradeHandler('https://github.com/SigNoz/signoz/releases')
}
type="link"
>
{t('release_notes')}
</Button>
</Form.Item>
</Form>
{!isError && isLatestVersion && (
<div>
<Space align="start">
<span></span>
<Typography.Paragraph italic>
{t('latest_version_signoz')}
</Typography.Paragraph>
</Space>
</div>
)}
{!isError && !isLatestVersion && (
<div>
<Space align="start">
<span>
<WarningFilled style={{ color: '#E87040' }} />
</span>
<Typography.Paragraph italic>{t('stale_version')}</Typography.Paragraph>
</Space>
</div>
)}
{!isError && !isLatestVersion && (
<Button
onClick={(): void =>
onClickUpgradeHandler(
'https://signoz.io/docs/operate/docker-standalone/#upgrade',
)
}
>
{t('read_how_to_upgrade')}
</Button>
)}
</Card>
);
}
export default Version;

View File

@ -0,0 +1,8 @@
import { Input } from 'antd';
import styled from 'styled-components';
export const InputComponent = styled(Input)`
&&& {
max-width: 183px;
}
`;

View File

@ -0,0 +1,8 @@
import Version from 'container/Version';
import React from 'react';
function Status(): JSX.Element {
return <Version />;
}
export default Status;

View File

@ -7,6 +7,10 @@ import {
LOGGED_IN,
SIDEBAR_COLLAPSE,
SWITCH_DARK_MODE,
UPDATE_CURRENT_ERROR,
UPDATE_CURRENT_VERSION,
UPDATE_LATEST_VERSION,
UPDATE_LATEST_VERSION_ERROR,
} from 'types/actions/app';
import InitialValueTypes from 'types/reducer/app';
@ -14,6 +18,10 @@ const InitialValue: InitialValueTypes = {
isDarkMode: getTheme() === 'darkMode',
isLoggedIn: getLocalStorageKey(IS_LOGGED_IN) === 'yes',
isSideBarCollapsed: getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
currentVersion: '',
latestVersion: '',
isCurrentVersionError: false,
isLatestVersionError: false,
};
const appReducer = (
@ -42,6 +50,28 @@ const appReducer = (
};
}
case UPDATE_CURRENT_VERSION: {
return {
...state,
currentVersion: action.payload.currentVersion,
};
}
case UPDATE_LATEST_VERSION: {
return { ...state, latestVersion: action.payload.latestVersion };
}
case UPDATE_CURRENT_ERROR: {
return { ...state, isCurrentVersionError: true };
}
case UPDATE_LATEST_VERSION_ERROR: {
return {
...state,
isLatestVersionError: true,
};
}
default:
return state;
}

View File

@ -1,7 +1,15 @@
import AppReducer from 'types/reducer/app';
export const SWITCH_DARK_MODE = 'SWITCH_DARK_MODE';
export const LOGGED_IN = 'LOGGED_IN';
export const SIDEBAR_COLLAPSE = 'SIDEBAR_COLLAPSE';
export const UPDATE_CURRENT_VERSION = 'UPDATE_CURRENT_VERSION';
export const UPDATE_LATEST_VERSION = 'UPDATE_LATEST_VERSION';
export const UPDATE_CURRENT_ERROR = 'UPDATE_CURRENT_ERROR';
export const UPDATE_LATEST_VERSION_ERROR = 'UPDATE_LATEST_VERSION_ERROR';
export interface SwitchDarkMode {
type: typeof SWITCH_DARK_MODE;
}
@ -15,4 +23,31 @@ export interface SideBarCollapse {
payload: boolean;
}
export type AppAction = SwitchDarkMode | LoggedInUser | SideBarCollapse;
export interface UpdateAppVersion {
type: typeof UPDATE_CURRENT_VERSION;
payload: {
currentVersion: AppReducer['currentVersion'];
};
}
export interface UpdateLatestVersion {
type: typeof UPDATE_LATEST_VERSION;
payload: {
latestVersion: AppReducer['latestVersion'];
};
}
export interface UpdateVersionError {
type: typeof UPDATE_CURRENT_ERROR | typeof UPDATE_LATEST_VERSION_ERROR;
payload: {
isError: boolean;
};
}
export type AppAction =
| SwitchDarkMode
| LoggedInUser
| SideBarCollapse
| UpdateAppVersion
| UpdateLatestVersion
| UpdateVersionError;

View File

@ -0,0 +1,18 @@
export interface PayloadProps {
body: string;
created_at: string;
draft: boolean;
html_url: string;
id: number;
mentions_count: number;
name: string;
node_id: number;
prerelease: boolean;
published_at: string;
tag_name: number;
tarball_url: string;
target_commitish: string;
upload_url: string;
url: string;
zipball_url: string;
}

View File

@ -2,4 +2,8 @@ export default interface AppReducer {
isDarkMode: boolean;
isLoggedIn: boolean;
isSideBarCollapsed: boolean;
currentVersion: string;
latestVersion: string;
isCurrentVersionError: boolean;
isLatestVersionError: boolean;
}