mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 00:38:59 +08:00
Feat: dynamic tooltip (#1705)
* feat: integrate config service with query service * feat: add tooltip checkpoint * feat: add support for dark and light mode icons Co-authored-by: Palash Gupta <palashgdev@gmail.com>
This commit is contained in:
parent
73706d872f
commit
a50d7f227c
24
frontend/src/api/dynamicConfigs/getDynamicConfigs.ts
Normal file
24
frontend/src/api/dynamicConfigs/getDynamicConfigs.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { PayloadProps } from 'types/api/dynamicConfigs/getDynamicConfigs';
|
||||||
|
|
||||||
|
const getDynamicConfigs = async (): Promise<
|
||||||
|
SuccessResponse<PayloadProps> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/configs`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getDynamicConfigs;
|
@ -1,4 +1,5 @@
|
|||||||
import { notification } from 'antd';
|
import { notification } from 'antd';
|
||||||
|
import getDynamicConfigs from 'api/dynamicConfigs/getDynamicConfigs';
|
||||||
import getFeaturesFlags from 'api/features/getFeatureFlags';
|
import getFeaturesFlags from 'api/features/getFeatureFlags';
|
||||||
import getUserLatestVersion from 'api/user/getLatestVersion';
|
import getUserLatestVersion from 'api/user/getLatestVersion';
|
||||||
import getUserVersion from 'api/user/getVersion';
|
import getUserVersion from 'api/user/getVersion';
|
||||||
@ -14,6 +15,7 @@ import { Dispatch } from 'redux';
|
|||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import {
|
import {
|
||||||
|
UPDATE_CONFIGS,
|
||||||
UPDATE_CURRENT_ERROR,
|
UPDATE_CURRENT_ERROR,
|
||||||
UPDATE_CURRENT_VERSION,
|
UPDATE_CURRENT_VERSION,
|
||||||
UPDATE_FEATURE_FLAGS,
|
UPDATE_FEATURE_FLAGS,
|
||||||
@ -33,6 +35,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
getUserVersionResponse,
|
getUserVersionResponse,
|
||||||
getUserLatestVersionResponse,
|
getUserLatestVersionResponse,
|
||||||
getFeaturesResponse,
|
getFeaturesResponse,
|
||||||
|
getDynamicConfigsResponse,
|
||||||
] = useQueries([
|
] = useQueries([
|
||||||
{
|
{
|
||||||
queryFn: getUserVersion,
|
queryFn: getUserVersion,
|
||||||
@ -48,6 +51,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
queryFn: getFeaturesFlags,
|
queryFn: getFeaturesFlags,
|
||||||
queryKey: 'getFeatureFlags',
|
queryKey: 'getFeatureFlags',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
queryFn: getDynamicConfigs,
|
||||||
|
queryKey: 'getDynamicConfigs',
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -65,11 +72,15 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
if (getFeaturesResponse.status === 'idle') {
|
if (getFeaturesResponse.status === 'idle') {
|
||||||
getFeaturesResponse.refetch();
|
getFeaturesResponse.refetch();
|
||||||
}
|
}
|
||||||
|
if (getDynamicConfigsResponse.status === 'idle') {
|
||||||
|
getDynamicConfigsResponse.refetch();
|
||||||
|
}
|
||||||
}, [
|
}, [
|
||||||
getFeaturesResponse,
|
getFeaturesResponse,
|
||||||
getUserLatestVersionResponse,
|
getUserLatestVersionResponse,
|
||||||
getUserVersionResponse,
|
getUserVersionResponse,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
|
getDynamicConfigsResponse,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
@ -78,6 +89,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
const latestCurrentCounter = useRef(0);
|
const latestCurrentCounter = useRef(0);
|
||||||
const latestVersionCounter = useRef(0);
|
const latestVersionCounter = useRef(0);
|
||||||
|
const latestConfigCounter = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@ -170,6 +182,23 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
getDynamicConfigsResponse.isFetched &&
|
||||||
|
getDynamicConfigsResponse.isSuccess &&
|
||||||
|
getDynamicConfigsResponse.data &&
|
||||||
|
getDynamicConfigsResponse.data.payload &&
|
||||||
|
latestConfigCounter.current === 0
|
||||||
|
) {
|
||||||
|
latestConfigCounter.current = 1;
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: UPDATE_CONFIGS,
|
||||||
|
payload: {
|
||||||
|
configs: getDynamicConfigsResponse.data.payload,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}, [
|
}, [
|
||||||
dispatch,
|
dispatch,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
@ -187,6 +216,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
getFeaturesResponse.isFetched,
|
getFeaturesResponse.isFetched,
|
||||||
getFeaturesResponse.isSuccess,
|
getFeaturesResponse.isSuccess,
|
||||||
getFeaturesResponse.data,
|
getFeaturesResponse.data,
|
||||||
|
getDynamicConfigsResponse.data,
|
||||||
|
getDynamicConfigsResponse.isFetched,
|
||||||
|
getDynamicConfigsResponse.isSuccess,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isToDisplayLayout = isLoggedIn;
|
const isToDisplayLayout = isLoggedIn;
|
||||||
|
33
frontend/src/container/ConfigDropdown/Config/ErrorLink.tsx
Normal file
33
frontend/src/container/ConfigDropdown/Config/ErrorLink.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorLink extends PureComponent<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(): State {
|
||||||
|
return { hasError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): JSX.Element {
|
||||||
|
const { children } = this.props;
|
||||||
|
const { hasError } = this.state;
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorLink;
|
23
frontend/src/container/ConfigDropdown/Config/Link.tsx
Normal file
23
frontend/src/container/ConfigDropdown/Config/Link.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
function LinkContainer({ children, href }: LinkContainerProps): JSX.Element {
|
||||||
|
const isInternalLink = href.startsWith('/');
|
||||||
|
|
||||||
|
if (isInternalLink) {
|
||||||
|
return <Link to={href}>{children}</Link>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a rel="noreferrer" target="_blank" href={href}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LinkContainerProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LinkContainer;
|
51
frontend/src/container/ConfigDropdown/Config/index.tsx
Normal file
51
frontend/src/container/ConfigDropdown/Config/index.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { Menu, Space } from 'antd';
|
||||||
|
import Spinner from 'components/Spinner';
|
||||||
|
import React, { Suspense, useMemo } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { ConfigProps } from 'types/api/dynamicConfigs/getDynamicConfigs';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
|
import ErrorLink from './ErrorLink';
|
||||||
|
import LinkContainer from './Link';
|
||||||
|
|
||||||
|
function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
|
||||||
|
const sortedConfig = useMemo(
|
||||||
|
() => config.components.sort((a, b) => a.position - b.position),
|
||||||
|
[config.components],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.ItemGroup>
|
||||||
|
{sortedConfig.map((item) => {
|
||||||
|
const iconName = `${isDarkMode ? item.darkIcon : item.lightIcon}`;
|
||||||
|
|
||||||
|
const Component = React.lazy(
|
||||||
|
() => import(`@ant-design/icons/es/icons/${iconName}.js`),
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<ErrorLink key={item.text + item.href}>
|
||||||
|
<Suspense fallback={<Spinner height="5vh" />}>
|
||||||
|
<Menu.Item>
|
||||||
|
<LinkContainer href={item.href}>
|
||||||
|
<Space size="small" align="start">
|
||||||
|
<Component />
|
||||||
|
{item.text}
|
||||||
|
</Space>
|
||||||
|
</LinkContainer>
|
||||||
|
</Menu.Item>
|
||||||
|
</Suspense>
|
||||||
|
</ErrorLink>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Menu.ItemGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HelpToolTipProps {
|
||||||
|
config: ConfigProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HelpToolTip;
|
67
frontend/src/container/ConfigDropdown/index.tsx
Normal file
67
frontend/src/container/ConfigDropdown/index.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
CaretDownFilled,
|
||||||
|
CaretUpFilled,
|
||||||
|
QuestionCircleFilled,
|
||||||
|
QuestionCircleOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Dropdown, Menu, Space } from 'antd';
|
||||||
|
import React, { useMemo, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
|
import HelpToolTip from './Config';
|
||||||
|
|
||||||
|
function DynamicConfigDropdown({
|
||||||
|
frontendId,
|
||||||
|
}: DynamicConfigDropdownProps): JSX.Element {
|
||||||
|
const { configs, isDarkMode } = useSelector<AppState, AppReducer>(
|
||||||
|
(state) => state.app,
|
||||||
|
);
|
||||||
|
const [isHelpDropDownOpen, setIsHelpDropDownOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const config = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.values(configs).find(
|
||||||
|
(config) => config.frontendPositionId === frontendId,
|
||||||
|
),
|
||||||
|
[frontendId, configs],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onToggleHandler = (): void => {
|
||||||
|
setIsHelpDropDownOpen(!isHelpDropDownOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Icon = isDarkMode ? QuestionCircleOutlined : QuestionCircleFilled;
|
||||||
|
const DropDownIcon = isHelpDropDownOpen ? CaretUpFilled : CaretDownFilled;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
onVisibleChange={onToggleHandler}
|
||||||
|
trigger={['click']}
|
||||||
|
overlay={
|
||||||
|
<Menu>
|
||||||
|
<HelpToolTip config={config} />
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
|
visible={isHelpDropDownOpen}
|
||||||
|
>
|
||||||
|
<Space align="center">
|
||||||
|
<Icon
|
||||||
|
style={{ fontSize: 26, color: 'white', paddingTop: 20, cursor: 'pointer' }}
|
||||||
|
/>
|
||||||
|
<DropDownIcon style={{ color: 'white' }} />
|
||||||
|
</Space>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DynamicConfigDropdownProps {
|
||||||
|
frontendId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DynamicConfigDropdown;
|
@ -14,8 +14,9 @@ import {
|
|||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { Logout } from 'api/utils';
|
import { Logout } from 'api/utils';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
|
import Config from 'container/ConfigDropdown';
|
||||||
import setTheme, { AppMode } from 'lib/theme/setTheme';
|
import setTheme, { AppMode } from 'lib/theme/setTheme';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { Dispatch, SetStateAction, useCallback, useState } from 'react';
|
||||||
import { connect, useSelector } from 'react-redux';
|
import { connect, useSelector } from 'react-redux';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
@ -34,7 +35,8 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
|
|||||||
const { isDarkMode, user, currentVersion } = useSelector<AppState, AppReducer>(
|
const { isDarkMode, user, currentVersion } = useSelector<AppState, AppReducer>(
|
||||||
(state) => state.app,
|
(state) => state.app,
|
||||||
);
|
);
|
||||||
const [isUserDropDownOpen, setIsUserDropDownOpen] = useState<boolean>();
|
|
||||||
|
const [isUserDropDownOpen, setIsUserDropDownOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const onToggleThemeHandler = useCallback(() => {
|
const onToggleThemeHandler = useCallback(() => {
|
||||||
const preMode: AppMode = isDarkMode ? 'lightMode' : 'darkMode';
|
const preMode: AppMode = isDarkMode ? 'lightMode' : 'darkMode';
|
||||||
@ -57,22 +59,21 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
|
|||||||
};
|
};
|
||||||
}, [toggleDarkMode, isDarkMode]);
|
}, [toggleDarkMode, isDarkMode]);
|
||||||
|
|
||||||
const onArrowClickHandler: VoidFunction = () => {
|
const onToggleHandler = useCallback(
|
||||||
setIsUserDropDownOpen((state) => !state);
|
(functionToExecute: Dispatch<SetStateAction<boolean>>) => (): void => {
|
||||||
};
|
functionToExecute((state) => !state);
|
||||||
|
},
|
||||||
const onClickLogoutHandler = (): void => {
|
[],
|
||||||
Logout();
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const menu = (
|
const menu = (
|
||||||
<Menu style={{ padding: '1rem' }}>
|
<Menu style={{ padding: '1rem' }}>
|
||||||
<Menu.ItemGroup>
|
<Menu.ItemGroup>
|
||||||
<SignedInAS />
|
<SignedInAS />
|
||||||
<Divider />
|
<Divider />
|
||||||
<CurrentOrganization onToggle={onArrowClickHandler} />
|
<CurrentOrganization onToggle={onToggleHandler(setIsUserDropDownOpen)} />
|
||||||
<Divider />
|
<Divider />
|
||||||
<ManageLicense onToggle={onArrowClickHandler} />
|
<ManageLicense onToggle={onToggleHandler(setIsUserDropDownOpen)} />
|
||||||
<Divider />
|
<Divider />
|
||||||
<LogoutContainer>
|
<LogoutContainer>
|
||||||
<LogoutOutlined />
|
<LogoutOutlined />
|
||||||
@ -80,11 +81,11 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyDown={(e): void => {
|
onKeyDown={(e): void => {
|
||||||
if (e.key === 'Enter' || e.key === 'Space') {
|
if (e.key === 'Enter' || e.key === 'Space') {
|
||||||
onClickLogoutHandler();
|
Logout();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
role="button"
|
role="button"
|
||||||
onClick={onClickLogoutHandler}
|
onClick={Logout}
|
||||||
>
|
>
|
||||||
<Typography.Link>Logout</Typography.Link>
|
<Typography.Link>Logout</Typography.Link>
|
||||||
</div>
|
</div>
|
||||||
@ -94,23 +95,18 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Header
|
<Layout.Header>
|
||||||
style={{
|
|
||||||
paddingLeft: '1.125rem',
|
|
||||||
paddingRight: '1.125rem',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Container>
|
<Container>
|
||||||
<NavLink
|
<NavLink to={ROUTES.APPLICATION}>
|
||||||
style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
|
|
||||||
to={ROUTES.APPLICATION}
|
|
||||||
>
|
|
||||||
<img src={`/signoz.svg?currentVersion=${currentVersion}`} alt="SigNoz" />
|
<img src={`/signoz.svg?currentVersion=${currentVersion}`} alt="SigNoz" />
|
||||||
<Typography.Title style={{ margin: 0, color: '#DBDBDB' }} level={4}>
|
<Typography.Title style={{ margin: 0, color: '#DBDBDB' }} level={4}>
|
||||||
SigNoz
|
SigNoz
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<Space align="center">
|
|
||||||
|
<Space style={{ height: '100%' }} align="center">
|
||||||
|
<Config frontendId="tooltip" />
|
||||||
|
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
checked={isDarkMode}
|
checked={isDarkMode}
|
||||||
onChange={onToggleThemeHandler}
|
onChange={onToggleThemeHandler}
|
||||||
@ -120,26 +116,14 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
onVisibleChange={onArrowClickHandler}
|
onVisibleChange={onToggleHandler(setIsUserDropDownOpen)}
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
overlay={menu}
|
overlay={menu}
|
||||||
visible={isUserDropDownOpen}
|
visible={isUserDropDownOpen}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space>
|
||||||
<Avatar shape="circle">{user?.name[0]}</Avatar>
|
<Avatar shape="circle">{user?.name[0]}</Avatar>
|
||||||
{!isUserDropDownOpen ? (
|
{!isUserDropDownOpen ? <CaretDownFilled /> : <CaretUpFilled />}
|
||||||
<CaretDownFilled
|
|
||||||
style={{
|
|
||||||
color: '#DBDBDB',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<CaretUpFilled
|
|
||||||
style={{
|
|
||||||
color: '#DBDBDB',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Space>
|
</Space>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</Space>
|
</Space>
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
LOGGED_IN,
|
LOGGED_IN,
|
||||||
SIDEBAR_COLLAPSE,
|
SIDEBAR_COLLAPSE,
|
||||||
SWITCH_DARK_MODE,
|
SWITCH_DARK_MODE,
|
||||||
|
UPDATE_CONFIGS,
|
||||||
UPDATE_CURRENT_ERROR,
|
UPDATE_CURRENT_ERROR,
|
||||||
UPDATE_CURRENT_VERSION,
|
UPDATE_CURRENT_VERSION,
|
||||||
UPDATE_FEATURE_FLAGS,
|
UPDATE_FEATURE_FLAGS,
|
||||||
@ -56,6 +57,7 @@ const InitialValue: InitialValueTypes = {
|
|||||||
isUserFetchingError: false,
|
isUserFetchingError: false,
|
||||||
org: null,
|
org: null,
|
||||||
role: null,
|
role: null,
|
||||||
|
configs: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const appReducer = (
|
const appReducer = (
|
||||||
@ -210,6 +212,13 @@ const appReducer = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case UPDATE_CONFIGS: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
configs: action.payload.configs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ export const UPDATE_USER = 'UPDATE_USER';
|
|||||||
export const UPDATE_ORG_NAME = 'UPDATE_ORG_NAME';
|
export const UPDATE_ORG_NAME = 'UPDATE_ORG_NAME';
|
||||||
export const UPDATE_ORG = 'UPDATE_ORG';
|
export const UPDATE_ORG = 'UPDATE_ORG';
|
||||||
export const UPDATE_FEATURE_FLAGS = 'UPDATE_FEATURE_FLAGS';
|
export const UPDATE_FEATURE_FLAGS = 'UPDATE_FEATURE_FLAGS';
|
||||||
|
export const UPDATE_CONFIGS = 'UPDATE_CONFIGS';
|
||||||
|
|
||||||
export interface SwitchDarkMode {
|
export interface SwitchDarkMode {
|
||||||
type: typeof SWITCH_DARK_MODE;
|
type: typeof SWITCH_DARK_MODE;
|
||||||
@ -115,6 +116,12 @@ export interface UpdateOrg {
|
|||||||
org: AppReducer['org'];
|
org: AppReducer['org'];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
export interface UpdateConfigs {
|
||||||
|
type: typeof UPDATE_CONFIGS;
|
||||||
|
payload: {
|
||||||
|
configs: AppReducer['configs'];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type AppAction =
|
export type AppAction =
|
||||||
| SwitchDarkMode
|
| SwitchDarkMode
|
||||||
@ -129,4 +136,5 @@ export type AppAction =
|
|||||||
| UpdateUser
|
| UpdateUser
|
||||||
| UpdateOrgName
|
| UpdateOrgName
|
||||||
| UpdateOrg
|
| UpdateOrg
|
||||||
| UpdateFeatureFlags;
|
| UpdateFeatureFlags
|
||||||
|
| UpdateConfigs;
|
||||||
|
14
frontend/src/types/api/dynamicConfigs/getDynamicConfigs.ts
Normal file
14
frontend/src/types/api/dynamicConfigs/getDynamicConfigs.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export interface ConfigProps {
|
||||||
|
enabled: boolean;
|
||||||
|
frontendPositionId: string;
|
||||||
|
components: Array<{
|
||||||
|
href: string;
|
||||||
|
darkIcon: string;
|
||||||
|
lightIcon: string;
|
||||||
|
position: 1;
|
||||||
|
text: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
export interface PayloadProps {
|
||||||
|
[key: string]: ConfigProps;
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import { PayloadProps as ConfigPayload } from 'types/api/dynamicConfigs/getDynamicConfigs';
|
||||||
import { PayloadProps as FeatureFlagPayload } from 'types/api/features/getFeaturesFlags';
|
import { PayloadProps as FeatureFlagPayload } from 'types/api/features/getFeaturesFlags';
|
||||||
import { PayloadProps as OrgPayload } from 'types/api/user/getOrganization';
|
import { PayloadProps as OrgPayload } from 'types/api/user/getOrganization';
|
||||||
import { PayloadProps as UserPayload } from 'types/api/user/getUser';
|
import { PayloadProps as UserPayload } from 'types/api/user/getUser';
|
||||||
@ -26,4 +27,5 @@ export default interface AppReducer {
|
|||||||
role: ROLES | null;
|
role: ROLES | null;
|
||||||
org: OrgPayload | null;
|
org: OrgPayload | null;
|
||||||
featureFlags: null | FeatureFlagPayload;
|
featureFlags: null | FeatureFlagPayload;
|
||||||
|
configs: ConfigPayload;
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ import (
|
|||||||
|
|
||||||
"go.signoz.io/signoz/pkg/query-service/dao"
|
"go.signoz.io/signoz/pkg/query-service/dao"
|
||||||
am "go.signoz.io/signoz/pkg/query-service/integrations/alertManager"
|
am "go.signoz.io/signoz/pkg/query-service/integrations/alertManager"
|
||||||
|
signozio "go.signoz.io/signoz/pkg/query-service/integrations/signozio"
|
||||||
"go.signoz.io/signoz/pkg/query-service/interfaces"
|
"go.signoz.io/signoz/pkg/query-service/interfaces"
|
||||||
"go.signoz.io/signoz/pkg/query-service/model"
|
"go.signoz.io/signoz/pkg/query-service/model"
|
||||||
"go.signoz.io/signoz/pkg/query-service/rules"
|
"go.signoz.io/signoz/pkg/query-service/rules"
|
||||||
@ -360,6 +361,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router) {
|
|||||||
|
|
||||||
router.HandleFunc("/api/v1/version", OpenAccess(aH.getVersion)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v1/version", OpenAccess(aH.getVersion)).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/api/v1/featureFlags", OpenAccess(aH.getFeatureFlags)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v1/featureFlags", OpenAccess(aH.getFeatureFlags)).Methods(http.MethodGet)
|
||||||
|
router.HandleFunc("/api/v1/configs", OpenAccess(aH.getConfigs)).Methods(http.MethodGet)
|
||||||
|
|
||||||
router.HandleFunc("/api/v1/getSpanFilters", ViewAccess(aH.getSpanFilters)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v1/getSpanFilters", ViewAccess(aH.getSpanFilters)).Methods(http.MethodPost)
|
||||||
router.HandleFunc("/api/v1/getTagFilters", ViewAccess(aH.getTagFilters)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v1/getTagFilters", ViewAccess(aH.getTagFilters)).Methods(http.MethodPost)
|
||||||
@ -1583,6 +1585,16 @@ func (aH *APIHandler) CheckFeature(f string) bool {
|
|||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (aH *APIHandler) getConfigs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
configs, err := signozio.FetchDynamicConfigs()
|
||||||
|
if err != nil {
|
||||||
|
aH.HandleError(w, err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
aH.Respond(w, configs)
|
||||||
|
}
|
||||||
|
|
||||||
// inviteUser is used to invite a user. It is used by an admin api.
|
// inviteUser is used to invite a user. It is used by an admin api.
|
||||||
func (aH *APIHandler) inviteUser(w http.ResponseWriter, r *http.Request) {
|
func (aH *APIHandler) inviteUser(w http.ResponseWriter, r *http.Request) {
|
||||||
req, err := parseInviteRequest(r)
|
req, err := parseInviteRequest(r)
|
||||||
|
@ -13,6 +13,8 @@ const (
|
|||||||
DebugHttpPort = "0.0.0.0:6060" // Address to serve http (pprof)
|
DebugHttpPort = "0.0.0.0:6060" // Address to serve http (pprof)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ConfigSignozIo = "https://config.signoz.io/api/v1"
|
||||||
|
|
||||||
var DEFAULT_TELEMETRY_ANONYMOUS = false
|
var DEFAULT_TELEMETRY_ANONYMOUS = false
|
||||||
|
|
||||||
func IsTelemetryEnabled() bool {
|
func IsTelemetryEnabled() bool {
|
||||||
|
75
pkg/query-service/integrations/signozio/dynamic_config.go
Normal file
75
pkg/query-service/integrations/signozio/dynamic_config.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package signozio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.signoz.io/signoz/ee/query-service/model"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||||
|
)
|
||||||
|
|
||||||
|
var C *Client
|
||||||
|
|
||||||
|
const (
|
||||||
|
POST = "POST"
|
||||||
|
APPLICATION_JSON = "application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
Prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Client {
|
||||||
|
return &Client{
|
||||||
|
Prefix: constants.ConfigSignozIo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
C = New()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchDynamicConfigs fetches configs from config server
|
||||||
|
func FetchDynamicConfigs() (map[string]Config, *model.ApiError) {
|
||||||
|
|
||||||
|
client := http.Client{Timeout: 5 * time.Second}
|
||||||
|
req, err := http.NewRequest(http.MethodGet, C.Prefix+"/configs", http.NoBody)
|
||||||
|
if err != nil {
|
||||||
|
return DefaultConfig, nil
|
||||||
|
}
|
||||||
|
req.SetBasicAuth("admin", "SigNoz@adm1n")
|
||||||
|
httpResponse, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return DefaultConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defer httpResponse.Body.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return DefaultConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
httpBody, err := ioutil.ReadAll(httpResponse.Body)
|
||||||
|
if err != nil {
|
||||||
|
return DefaultConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// read api request result
|
||||||
|
result := ConfigResult{}
|
||||||
|
err = json.Unmarshal(httpBody, &result)
|
||||||
|
if err != nil {
|
||||||
|
return DefaultConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch httpResponse.StatusCode {
|
||||||
|
case 200, 201:
|
||||||
|
return result.Data, nil
|
||||||
|
case 400, 401:
|
||||||
|
return DefaultConfig, nil
|
||||||
|
default:
|
||||||
|
return DefaultConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
54
pkg/query-service/integrations/signozio/response.go
Normal file
54
pkg/query-service/integrations/signozio/response.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package signozio
|
||||||
|
|
||||||
|
type status string
|
||||||
|
|
||||||
|
type ConfigResult struct {
|
||||||
|
Status status `json:"status"`
|
||||||
|
Data map[string]Config `json:"data,omitempty"`
|
||||||
|
ErrorType string `json:"errorType,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
FrontendPositionId string `json:"frontendPositionId"`
|
||||||
|
Components []ComponentProps `json:"components"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComponentProps struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Position int `json:"position"`
|
||||||
|
DarkIcon string `json:"darkIcon"`
|
||||||
|
LightIcon string `json:"lightIcon"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultConfig = map[string]Config{
|
||||||
|
"helpConfig": {
|
||||||
|
Enabled: true,
|
||||||
|
FrontendPositionId: "tooltip",
|
||||||
|
Components: []ComponentProps{
|
||||||
|
{
|
||||||
|
Text: "How to use SigNoz in production",
|
||||||
|
Position: 1,
|
||||||
|
LightIcon: "RiseOutlined",
|
||||||
|
DarkIcon: "RiseOutlined",
|
||||||
|
Href: "https://signoz.io/docs/production-readiness",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "Create an issue in GitHub",
|
||||||
|
Position: 2,
|
||||||
|
LightIcon: "GithubFilled",
|
||||||
|
DarkIcon: "GithubOutlined",
|
||||||
|
Href: "https://github.com/SigNoz/signoz/issues/new/choose",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "Read the docs",
|
||||||
|
Position: 3,
|
||||||
|
LightIcon: "FileTextFilled",
|
||||||
|
DarkIcon: "FileTextOutlined",
|
||||||
|
Href: "https://signoz.io/docs",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user