mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 03:29:02 +08:00
feat: show release note in alerts dashboards and services pages (#1840)
* feat: show release note in alerts dashboards and services pages * fix: made code changes as per review and changed message in release note * fix: solved build pipeline issue * fix: solved lint issue Co-authored-by: mindhash <mindhash@mindhashs-MacBook-Pro.local> Co-authored-by: Pranay Prateek <pranay@signoz.io> Co-authored-by: Ankit Nayan <ankit@signoz.io>
This commit is contained in:
parent
4d02603aed
commit
0abae1c09c
@ -57,6 +57,7 @@ const afterLogin = async (
|
|||||||
profilePictureURL: payload.profilePictureURL,
|
profilePictureURL: payload.profilePictureURL,
|
||||||
userId: payload.id,
|
userId: payload.id,
|
||||||
orgId: payload.orgId,
|
orgId: payload.orgId,
|
||||||
|
userFlags: payload.flags,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
26
frontend/src/api/user/setFlags.ts
Normal file
26
frontend/src/api/user/setFlags.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { PayloadProps, Props } from 'types/api/user/setFlags';
|
||||||
|
|
||||||
|
const setFlags = async (
|
||||||
|
props: Props,
|
||||||
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.patch(`/user/${props.userId}/flags`, {
|
||||||
|
...props.flags,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data?.status,
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default setFlags;
|
@ -41,6 +41,7 @@ export const Logout = (): void => {
|
|||||||
orgName: '',
|
orgName: '',
|
||||||
profilePictureURL: '',
|
profilePictureURL: '',
|
||||||
userId: '',
|
userId: '',
|
||||||
|
userFlags: {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
27
frontend/src/components/MessageTip/index.tsx
Normal file
27
frontend/src/components/MessageTip/index.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { StyledAlert } from './styles';
|
||||||
|
|
||||||
|
interface MessageTipProps {
|
||||||
|
show?: boolean;
|
||||||
|
message: React.ReactNode | string;
|
||||||
|
action: React.ReactNode | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MessageTip({
|
||||||
|
show,
|
||||||
|
message,
|
||||||
|
action,
|
||||||
|
}: MessageTipProps): JSX.Element | null {
|
||||||
|
if (!show) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledAlert showIcon description={message} type="info" action={action} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageTip.defaultProps = {
|
||||||
|
show: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MessageTip;
|
6
frontend/src/components/MessageTip/styles.ts
Normal file
6
frontend/src/components/MessageTip/styles.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { Alert } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const StyledAlert = styled(Alert)`
|
||||||
|
align-items: center;
|
||||||
|
`;
|
4
frontend/src/components/ReleaseNote/ReleaseNoteProps.ts
Normal file
4
frontend/src/components/ReleaseNote/ReleaseNoteProps.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export default interface ReleaseNoteProps {
|
||||||
|
path?: string;
|
||||||
|
release?: string;
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
import { Button, Space } from 'antd';
|
||||||
|
import setFlags from 'api/user/setFlags';
|
||||||
|
import MessageTip from 'components/MessageTip';
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { UPDATE_USER_FLAG } from 'types/actions/app';
|
||||||
|
import { UserFlags } from 'types/api/user/setFlags';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
|
import ReleaseNoteProps from '../ReleaseNoteProps';
|
||||||
|
|
||||||
|
export default function ReleaseNote0120({
|
||||||
|
release,
|
||||||
|
}: ReleaseNoteProps): JSX.Element | null {
|
||||||
|
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
|
|
||||||
|
const handleDontShow = useCallback(async (): Promise<void> => {
|
||||||
|
const flags: UserFlags = { ReleaseNote0120Hide: 'Y' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
dispatch({
|
||||||
|
type: UPDATE_USER_FLAG,
|
||||||
|
payload: {
|
||||||
|
flags,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
// no user is set, so escape the routine
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await setFlags({ userId: user?.userId, flags });
|
||||||
|
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
console.log('failed to complete do not show status', response.error);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// here we do not nothing as the cost of error is minor,
|
||||||
|
// the user can switch the do no show option again in the further.
|
||||||
|
console.log('unexpected error: failed to complete do not show status', e);
|
||||||
|
}
|
||||||
|
}, [dispatch, user]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MessageTip
|
||||||
|
show
|
||||||
|
message={
|
||||||
|
<div>
|
||||||
|
You are using {release} of SigNoz. We have introduced distributed setup in
|
||||||
|
v0.12.0 release. If you use or plan to use clickhouse queries in dashboard
|
||||||
|
or alerts, you might want to read about querying the new distributed tables{' '}
|
||||||
|
<a
|
||||||
|
href="https://signoz.io/docs/operate/migration/upgrade-0.12/#querying-distributed-tables"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
action={
|
||||||
|
<Space>
|
||||||
|
<Button onClick={handleDontShow}>Do not show again</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
66
frontend/src/components/ReleaseNote/index.tsx
Normal file
66
frontend/src/components/ReleaseNote/index.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import ReleaseNoteProps from 'components/ReleaseNote/ReleaseNoteProps';
|
||||||
|
import ReleaseNote0120 from 'components/ReleaseNote/Releases/ReleaseNote0120';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { UserFlags } from 'types/api/user/setFlags';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
|
interface ComponentMapType {
|
||||||
|
match: (
|
||||||
|
path: string | undefined,
|
||||||
|
version: string,
|
||||||
|
userFlags: UserFlags | null,
|
||||||
|
) => boolean;
|
||||||
|
component: ({ path, release }: ReleaseNoteProps) => JSX.Element | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allComponentMap: ComponentMapType[] = [
|
||||||
|
{
|
||||||
|
match: (
|
||||||
|
path: string | undefined,
|
||||||
|
version: string,
|
||||||
|
userFlags: UserFlags | null,
|
||||||
|
): boolean => {
|
||||||
|
if (!path) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const allowedPaths = [
|
||||||
|
ROUTES.LIST_ALL_ALERT,
|
||||||
|
ROUTES.APPLICATION,
|
||||||
|
ROUTES.ALL_DASHBOARD,
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
userFlags?.ReleaseNote0120Hide !== 'Y' &&
|
||||||
|
allowedPaths.includes(path) &&
|
||||||
|
version.startsWith('v0.12')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
component: ReleaseNote0120,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ReleaseNote prints release specific warnings and notes that
|
||||||
|
// user needs to be aware of before using the upgraded version.
|
||||||
|
function ReleaseNote({ path }: ReleaseNoteProps): JSX.Element | null {
|
||||||
|
const { userFlags, currentVersion } = useSelector<AppState, AppReducer>(
|
||||||
|
(state) => state.app,
|
||||||
|
);
|
||||||
|
|
||||||
|
const c = allComponentMap.find((item) => {
|
||||||
|
return item.match(path, currentVersion, userFlags);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!c) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <c.component path={path} release={currentVersion} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReleaseNote.defaultProps = {
|
||||||
|
path: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReleaseNote;
|
@ -1,14 +1,17 @@
|
|||||||
import { notification } from 'antd';
|
import { notification, Space } from 'antd';
|
||||||
import getAll from 'api/alerts/getAll';
|
import getAll from 'api/alerts/getAll';
|
||||||
|
import ReleaseNote from 'components/ReleaseNote';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import ListAlert from './ListAlert';
|
import ListAlert from './ListAlert';
|
||||||
|
|
||||||
function ListAlertRules(): JSX.Element {
|
function ListAlertRules(): JSX.Element {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
|
const location = useLocation();
|
||||||
const { data, isError, isLoading, refetch, status } = useQuery('allAlerts', {
|
const { data, isError, isLoading, refetch, status } = useQuery('allAlerts', {
|
||||||
queryFn: getAll,
|
queryFn: getAll,
|
||||||
cacheTime: 0,
|
cacheTime: 0,
|
||||||
@ -45,12 +48,15 @@ function ListAlertRules(): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListAlert
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||||
{...{
|
<ReleaseNote path={location.pathname} />
|
||||||
allAlertRules: data.payload,
|
<ListAlert
|
||||||
refetch,
|
{...{
|
||||||
}}
|
allAlertRules: data.payload,
|
||||||
/>
|
refetch,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ import AppReducer from 'types/reducer/app';
|
|||||||
import { NameInput } from '../styles';
|
import { NameInput } from '../styles';
|
||||||
|
|
||||||
function UpdateName(): JSX.Element {
|
function UpdateName(): JSX.Element {
|
||||||
const { user, role, org } = useSelector<AppState, AppReducer>(
|
const { user, role, org, userFlags } = useSelector<AppState, AppReducer>(
|
||||||
(state) => state.app,
|
(state) => state.app,
|
||||||
);
|
);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -47,6 +47,7 @@ function UpdateName(): JSX.Element {
|
|||||||
ROLE: role || 'ADMIN',
|
ROLE: role || 'ADMIN',
|
||||||
orgId: org[0].id,
|
orgId: org[0].id,
|
||||||
orgName: org[0].name,
|
orgName: org[0].name,
|
||||||
|
userFlags: userFlags || {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,17 +1,26 @@
|
|||||||
|
import { Space } from 'antd';
|
||||||
|
import ReleaseNote from 'components/ReleaseNote';
|
||||||
import ListOfAllDashboard from 'container/ListOfDashboard';
|
import ListOfAllDashboard from 'container/ListOfDashboard';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { ThunkDispatch } from 'redux-thunk';
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
import { GetAllDashboards } from 'store/actions';
|
import { GetAllDashboards } from 'store/actions';
|
||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
|
|
||||||
function Dashboard({ getAllDashboards }: DashboardProps): JSX.Element {
|
function Dashboard({ getAllDashboards }: DashboardProps): JSX.Element {
|
||||||
|
const location = useLocation();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getAllDashboards();
|
getAllDashboards();
|
||||||
}, [getAllDashboards]);
|
}, [getAllDashboards]);
|
||||||
|
|
||||||
return <ListOfAllDashboard />;
|
return (
|
||||||
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
|
<ReleaseNote path={location.pathname} />
|
||||||
|
<ListOfAllDashboard />
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DispatchProps {
|
interface DispatchProps {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { notification } from 'antd';
|
import { notification, Space } from 'antd';
|
||||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||||
|
import ReleaseNote from 'components/ReleaseNote';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { SKIP_ONBOARDING } from 'constants/onboarding';
|
import { SKIP_ONBOARDING } from 'constants/onboarding';
|
||||||
import ResourceAttributesFilter from 'container/MetricsApplication/ResourceAttributesFilter';
|
import ResourceAttributesFilter from 'container/MetricsApplication/ResourceAttributesFilter';
|
||||||
@ -7,6 +8,7 @@ import MetricTable from 'container/MetricsTable';
|
|||||||
import { convertRawQueriesToTraceSelectedTags } from 'lib/resourceAttributes';
|
import { convertRawQueriesToTraceSelectedTags } from 'lib/resourceAttributes';
|
||||||
import React, { useEffect, useMemo } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { connect, useSelector } from 'react-redux';
|
import { connect, useSelector } from 'react-redux';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
import { ThunkDispatch } from 'redux-thunk';
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
import { GetService, GetServiceProps } from 'store/actions/metrics';
|
import { GetService, GetServiceProps } from 'store/actions/metrics';
|
||||||
@ -21,6 +23,7 @@ function Metrics({ getService }: MetricsProps): JSX.Element {
|
|||||||
AppState,
|
AppState,
|
||||||
GlobalReducer
|
GlobalReducer
|
||||||
>((state) => state.globalTime);
|
>((state) => state.globalTime);
|
||||||
|
const location = useLocation();
|
||||||
const {
|
const {
|
||||||
services,
|
services,
|
||||||
resourceAttributeQueries,
|
resourceAttributeQueries,
|
||||||
@ -86,10 +89,12 @@ function Metrics({ getService }: MetricsProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<ReleaseNote path={location.pathname} />
|
||||||
|
|
||||||
<ResourceAttributesFilter />
|
<ResourceAttributesFilter />
|
||||||
<MetricTable />
|
<MetricTable />
|
||||||
</>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
UPDATE_ORG_NAME,
|
UPDATE_ORG_NAME,
|
||||||
UPDATE_USER,
|
UPDATE_USER,
|
||||||
UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN,
|
UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN,
|
||||||
|
UPDATE_USER_FLAG,
|
||||||
UPDATE_USER_IS_FETCH,
|
UPDATE_USER_IS_FETCH,
|
||||||
UPDATE_USER_ORG_ROLE,
|
UPDATE_USER_ORG_ROLE,
|
||||||
} from 'types/actions/app';
|
} from 'types/actions/app';
|
||||||
@ -58,6 +59,7 @@ const InitialValue: InitialValueTypes = {
|
|||||||
org: null,
|
org: null,
|
||||||
role: null,
|
role: null,
|
||||||
configs: {},
|
configs: {},
|
||||||
|
userFlags: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const appReducer = (
|
const appReducer = (
|
||||||
@ -153,6 +155,7 @@ const appReducer = (
|
|||||||
ROLE,
|
ROLE,
|
||||||
orgId,
|
orgId,
|
||||||
orgName,
|
orgName,
|
||||||
|
userFlags,
|
||||||
} = action.payload;
|
} = action.payload;
|
||||||
const orgIndex = org.findIndex((e) => e.id === orgId);
|
const orgIndex = org.findIndex((e) => e.id === orgId);
|
||||||
|
|
||||||
@ -179,6 +182,7 @@ const appReducer = (
|
|||||||
},
|
},
|
||||||
org: [...updatedOrg],
|
org: [...updatedOrg],
|
||||||
role: ROLE,
|
role: ROLE,
|
||||||
|
userFlags,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,6 +223,14 @@ const appReducer = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case UPDATE_USER_FLAG: {
|
||||||
|
console.log('herei n update user flag');
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
userFlags: { ...state.userFlags, ...action.payload.flags },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
Organization,
|
Organization,
|
||||||
PayloadProps as OrgPayload,
|
PayloadProps as OrgPayload,
|
||||||
} from 'types/api/user/getOrganization';
|
} from 'types/api/user/getOrganization';
|
||||||
|
import { UserFlags } from 'types/api/user/setFlags';
|
||||||
import AppReducer, { User } from 'types/reducer/app';
|
import AppReducer, { User } from 'types/reducer/app';
|
||||||
import { ROLES } from 'types/roles';
|
import { ROLES } from 'types/roles';
|
||||||
|
|
||||||
@ -24,6 +25,7 @@ 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 const UPDATE_CONFIGS = 'UPDATE_CONFIGS';
|
||||||
|
export const UPDATE_USER_FLAG = 'UPDATE_USER_FLAG';
|
||||||
|
|
||||||
export interface SwitchDarkMode {
|
export interface SwitchDarkMode {
|
||||||
type: typeof SWITCH_DARK_MODE;
|
type: typeof SWITCH_DARK_MODE;
|
||||||
@ -92,6 +94,7 @@ export interface UpdateUser {
|
|||||||
orgName: Organization['name'];
|
orgName: Organization['name'];
|
||||||
ROLE: ROLES;
|
ROLE: ROLES;
|
||||||
orgId: Organization['id'];
|
orgId: Organization['id'];
|
||||||
|
userFlags: UserFlags;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,6 +113,13 @@ export interface UpdateOrgName {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateUserFlag {
|
||||||
|
type: typeof UPDATE_USER_FLAG;
|
||||||
|
payload: {
|
||||||
|
flags: UserFlags | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface UpdateOrg {
|
export interface UpdateOrg {
|
||||||
type: typeof UPDATE_ORG;
|
type: typeof UPDATE_ORG;
|
||||||
payload: {
|
payload: {
|
||||||
@ -137,4 +147,5 @@ export type AppAction =
|
|||||||
| UpdateOrgName
|
| UpdateOrgName
|
||||||
| UpdateOrg
|
| UpdateOrg
|
||||||
| UpdateFeatureFlags
|
| UpdateFeatureFlags
|
||||||
| UpdateConfigs;
|
| UpdateConfigs
|
||||||
|
| UpdateUserFlag;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { UserFlags } from 'types/api/user/setFlags';
|
||||||
import { User } from 'types/reducer/app';
|
import { User } from 'types/reducer/app';
|
||||||
import { ROLES } from 'types/roles';
|
import { ROLES } from 'types/roles';
|
||||||
|
|
||||||
@ -15,4 +16,5 @@ export interface PayloadProps {
|
|||||||
profilePictureURL: string;
|
profilePictureURL: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
role: ROLES;
|
role: ROLES;
|
||||||
|
flags: UserFlags;
|
||||||
}
|
}
|
||||||
|
12
frontend/src/types/api/user/setFlags.ts
Normal file
12
frontend/src/types/api/user/setFlags.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { User } from 'types/reducer/app';
|
||||||
|
|
||||||
|
export interface UserFlags {
|
||||||
|
ReleaseNote0120Hide?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PayloadProps = UserFlags;
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
userId: User['userId'];
|
||||||
|
flags: UserFlags;
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { PayloadProps as ConfigPayload } from 'types/api/dynamicConfigs/getDynam
|
|||||||
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';
|
||||||
|
import { UserFlags } from 'types/api/user/setFlags';
|
||||||
import { ROLES } from 'types/roles';
|
import { ROLES } from 'types/roles';
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
@ -28,4 +29,5 @@ export default interface AppReducer {
|
|||||||
org: OrgPayload | null;
|
org: OrgPayload | null;
|
||||||
featureFlags: null | FeatureFlagPayload;
|
featureFlags: null | FeatureFlagPayload;
|
||||||
configs: ConfigPayload;
|
configs: ConfigPayload;
|
||||||
|
userFlags: null | UserFlags;
|
||||||
}
|
}
|
||||||
|
@ -392,6 +392,8 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router) {
|
|||||||
router.HandleFunc("/api/v1/user/{id}", SelfAccess(aH.editUser)).Methods(http.MethodPut)
|
router.HandleFunc("/api/v1/user/{id}", SelfAccess(aH.editUser)).Methods(http.MethodPut)
|
||||||
router.HandleFunc("/api/v1/user/{id}", AdminAccess(aH.deleteUser)).Methods(http.MethodDelete)
|
router.HandleFunc("/api/v1/user/{id}", AdminAccess(aH.deleteUser)).Methods(http.MethodDelete)
|
||||||
|
|
||||||
|
router.HandleFunc("/api/v1/user/{id}/flags", SelfAccess(aH.patchUserFlag)).Methods(http.MethodPatch)
|
||||||
|
|
||||||
router.HandleFunc("/api/v1/rbac/role/{id}", SelfAccess(aH.getRole)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v1/rbac/role/{id}", SelfAccess(aH.getRole)).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/api/v1/rbac/role/{id}", AdminAccess(aH.editRole)).Methods(http.MethodPut)
|
router.HandleFunc("/api/v1/rbac/role/{id}", AdminAccess(aH.editRole)).Methods(http.MethodPut)
|
||||||
|
|
||||||
@ -1854,6 +1856,37 @@ func (aH *APIHandler) deleteUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
aH.WriteJSON(w, r, map[string]string{"data": "user deleted successfully"})
|
aH.WriteJSON(w, r, map[string]string{"data": "user deleted successfully"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addUserFlag patches a user flags with the changes
|
||||||
|
func (aH *APIHandler) patchUserFlag(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// read user id from path var
|
||||||
|
userId := mux.Vars(r)["id"]
|
||||||
|
|
||||||
|
// read input into user flag
|
||||||
|
defer r.Body.Close()
|
||||||
|
b, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
zap.S().Errorf("failed read user flags from http request for userId ", userId, "with error: ", err)
|
||||||
|
RespondError(w, model.BadRequestStr("received user flags in invalid format"), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
flags := make(map[string]string, 0)
|
||||||
|
|
||||||
|
err = json.Unmarshal(b, &flags)
|
||||||
|
if err != nil {
|
||||||
|
zap.S().Errorf("failed parsing user flags for userId ", userId, "with error: ", err)
|
||||||
|
RespondError(w, model.BadRequestStr("received user flags in invalid format"), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newflags, apiError := dao.DB().UpdateUserFlags(r.Context(), userId, flags)
|
||||||
|
if !apiError.IsNil() {
|
||||||
|
RespondError(w, apiError, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
aH.Respond(w, newflags)
|
||||||
|
}
|
||||||
|
|
||||||
func (aH *APIHandler) getRole(w http.ResponseWriter, r *http.Request) {
|
func (aH *APIHandler) getRole(w http.ResponseWriter, r *http.Request) {
|
||||||
id := mux.Vars(r)["id"]
|
id := mux.Vars(r)["id"]
|
||||||
|
|
||||||
|
@ -41,6 +41,8 @@ type Mutations interface {
|
|||||||
EditUser(ctx context.Context, update *model.User) (*model.User, *model.ApiError)
|
EditUser(ctx context.Context, update *model.User) (*model.User, *model.ApiError)
|
||||||
DeleteUser(ctx context.Context, id string) *model.ApiError
|
DeleteUser(ctx context.Context, id string) *model.ApiError
|
||||||
|
|
||||||
|
UpdateUserFlags(ctx context.Context, userId string, flags map[string]string) (model.UserFlag, *model.ApiError)
|
||||||
|
|
||||||
CreateGroup(ctx context.Context, group *model.Group) (*model.Group, *model.ApiError)
|
CreateGroup(ctx context.Context, group *model.Group) (*model.Group, *model.ApiError)
|
||||||
DeleteGroup(ctx context.Context, id string) *model.ApiError
|
DeleteGroup(ctx context.Context, id string) *model.ApiError
|
||||||
|
|
||||||
|
@ -68,6 +68,11 @@ func InitDB(dataSourceName string) (*ModelDaoSqlite, error) {
|
|||||||
token TEXT NOT NULL,
|
token TEXT NOT NULL,
|
||||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||||
);
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS user_flags (
|
||||||
|
user_id TEXT PRIMARY KEY,
|
||||||
|
flags TEXT,
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
`
|
`
|
||||||
|
|
||||||
_, err = db.Exec(table_schema)
|
_, err = db.Exec(table_schema)
|
||||||
|
@ -2,6 +2,7 @@ package sqlite
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -271,7 +272,10 @@ func (mds *ModelDaoSqlite) GetUser(ctx context.Context,
|
|||||||
u.org_id,
|
u.org_id,
|
||||||
u.group_id,
|
u.group_id,
|
||||||
g.name as role,
|
g.name as role,
|
||||||
o.name as organization
|
o.name as organization,
|
||||||
|
COALESCE((select uf.flags
|
||||||
|
from user_flags uf
|
||||||
|
where u.id = uf.user_id), '') as flags
|
||||||
from users u, groups g, organizations o
|
from users u, groups g, organizations o
|
||||||
where
|
where
|
||||||
g.id=u.group_id and
|
g.id=u.group_id and
|
||||||
@ -291,6 +295,7 @@ func (mds *ModelDaoSqlite) GetUser(ctx context.Context,
|
|||||||
if len(users) == 0 {
|
if len(users) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &users[0], nil
|
return &users[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -531,3 +536,53 @@ func (mds *ModelDaoSqlite) GetResetPasswordEntry(ctx context.Context,
|
|||||||
}
|
}
|
||||||
return &entries[0], nil
|
return &entries[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateUserFlags inserts user specific flags
|
||||||
|
func (mds *ModelDaoSqlite) UpdateUserFlags(ctx context.Context, userId string, flags map[string]string) (model.UserFlag, *model.ApiError) {
|
||||||
|
|
||||||
|
if len(flags) == 0 {
|
||||||
|
// nothing to do as flags are empty. In this method, we only append the flags
|
||||||
|
// but not set them to empty
|
||||||
|
return flags, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch existing flags
|
||||||
|
userPayload, apiError := mds.GetUser(ctx, userId)
|
||||||
|
if apiError != nil {
|
||||||
|
return nil, apiError
|
||||||
|
}
|
||||||
|
|
||||||
|
if userPayload.Flags != nil {
|
||||||
|
for k, v := range userPayload.Flags {
|
||||||
|
if _, ok := flags[k]; !ok {
|
||||||
|
// insert only missing keys as we want to retain the
|
||||||
|
// flags in the db that are not part of this request
|
||||||
|
flags[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// append existing flags with new ones
|
||||||
|
|
||||||
|
// write the updated flags
|
||||||
|
flagsBytes, err := json.Marshal(flags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.InternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(userPayload.Flags) == 0 {
|
||||||
|
q := `INSERT INTO user_flags (user_id, flags) VALUES (?, ?);`
|
||||||
|
|
||||||
|
if _, err := mds.db.ExecContext(ctx, q, userId, string(flagsBytes)); err != nil {
|
||||||
|
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
q := `UPDATE user_flags SET flags = ? WHERE user_id = ?;`
|
||||||
|
|
||||||
|
if _, err := mds.db.ExecContext(ctx, q, userId, string(flagsBytes)); err != nil {
|
||||||
|
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return flags, nil
|
||||||
|
}
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
type Organization struct {
|
type Organization struct {
|
||||||
Id string `json:"id" db:"id"`
|
Id string `json:"id" db:"id"`
|
||||||
Name string `json:"name" db:"name"`
|
Name string `json:"name" db:"name"`
|
||||||
@ -30,10 +36,42 @@ type User struct {
|
|||||||
GroupId string `json:"groupId,omitempty" db:"group_id"`
|
GroupId string `json:"groupId,omitempty" db:"group_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserFlag map[string]string
|
||||||
|
|
||||||
|
func (uf UserFlag) Value() (driver.Value, error) {
|
||||||
|
f := make(map[string]string, 0)
|
||||||
|
for k, v := range uf {
|
||||||
|
f[k] = v
|
||||||
|
}
|
||||||
|
return json.Marshal(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uf *UserFlag) Scan(value interface{}) error {
|
||||||
|
fmt.Println(" value:", value)
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b, ok := value.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("type assertion to []byte failed while scanning user flag")
|
||||||
|
}
|
||||||
|
f := make(map[string]string, 0)
|
||||||
|
if err := json.Unmarshal([]byte(b), &f); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*uf = make(UserFlag, len(f))
|
||||||
|
for k, v := range f {
|
||||||
|
(*uf)[k] = v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type UserPayload struct {
|
type UserPayload struct {
|
||||||
User
|
User
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Organization string `json:"organization"`
|
Organization string `json:"organization"`
|
||||||
|
Flags UserFlag `json:"flags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Group struct {
|
type Group struct {
|
||||||
|
@ -72,6 +72,14 @@ func BadRequest(err error) *ApiError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BadRequestStr returns a ApiError object of bad request
|
||||||
|
func BadRequestStr(s string) *ApiError {
|
||||||
|
return &ApiError{
|
||||||
|
Typ: ErrorBadData,
|
||||||
|
Err: fmt.Errorf(s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// InternalError returns a ApiError object of internal type
|
// InternalError returns a ApiError object of internal type
|
||||||
func InternalError(err error) *ApiError {
|
func InternalError(err error) *ApiError {
|
||||||
return &ApiError{
|
return &ApiError{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user