mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-10 17:53:36 +02:00
Merge branch 'main' into fix/proxy_all_toggles
This commit is contained in:
commit
0f5b080b13
@ -3,16 +3,16 @@ import AccessContext from 'contexts/AccessContext';
|
|||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
import { AdminAlert } from 'component/common/AdminAlert/AdminAlert';
|
import { AdminAlert } from 'component/common/AdminAlert/AdminAlert';
|
||||||
import { SignOnLogTable } from './SignOnLogTable/SignOnLogTable';
|
import { LoginHistoryTable } from './LoginHistoryTable/LoginHistoryTable';
|
||||||
|
|
||||||
export const SignOnLog = () => {
|
export const LoginHistory = () => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasAccess(ADMIN)}
|
condition={hasAccess(ADMIN)}
|
||||||
show={<SignOnLogTable />}
|
show={<LoginHistoryTable />}
|
||||||
elseShow={<AdminAlert />}
|
elseShow={<AdminAlert />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
@ -8,13 +8,13 @@ const StyledBox = styled(Box)(() => ({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface ISignOnLogActionsCellProps {
|
interface ILoginHistoryActionsCellProps {
|
||||||
onDelete: (event: React.SyntheticEvent) => void;
|
onDelete: (event: React.SyntheticEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SignOnLogActionsCell = ({
|
export const LoginHistoryActionsCell = ({
|
||||||
onDelete,
|
onDelete,
|
||||||
}: ISignOnLogActionsCellProps) => {
|
}: ILoginHistoryActionsCellProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledBox>
|
<StyledBox>
|
||||||
<PermissionIconButton
|
<PermissionIconButton
|
@ -1,28 +1,28 @@
|
|||||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
|
|
||||||
interface IServiceAccountDeleteAllDialogProps {
|
interface ILoginHistoryDeleteAllDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SignOnLogDeleteAllDialog = ({
|
export const LoginHistoryDeleteAllDialog = ({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}: IServiceAccountDeleteAllDialogProps) => (
|
}: ILoginHistoryDeleteAllDialogProps) => (
|
||||||
<Dialogue
|
<Dialogue
|
||||||
title="Clear sign-on log?"
|
title="Clear login history?"
|
||||||
open={open}
|
open={open}
|
||||||
primaryButtonText="Clear sign-on log"
|
primaryButtonText="Clear login history"
|
||||||
secondaryButtonText="Cancel"
|
secondaryButtonText="Cancel"
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
You are about to clear the sign-on log.
|
You are about to clear the login history.
|
||||||
<br />
|
<br />
|
||||||
This will delete all the sign-on events.
|
This will delete all the login events.
|
||||||
</Dialogue>
|
</Dialogue>
|
||||||
);
|
);
|
@ -1,19 +1,19 @@
|
|||||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
import { ISignOnEvent } from 'interfaces/signOnEvent';
|
import { ILoginEvent } from 'interfaces/loginEvent';
|
||||||
|
|
||||||
interface IServiceAccountDeleteDialogProps {
|
interface ILoginHistoryDeleteDialogProps {
|
||||||
event?: ISignOnEvent;
|
event?: ILoginEvent;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
onConfirm: (event: ISignOnEvent) => void;
|
onConfirm: (event: ILoginEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SignOnLogDeleteDialog = ({
|
export const LoginHistoryDeleteDialog = ({
|
||||||
event,
|
event,
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}: IServiceAccountDeleteDialogProps) => (
|
}: ILoginHistoryDeleteDialogProps) => (
|
||||||
<Dialogue
|
<Dialogue
|
||||||
title="Delete event?"
|
title="Delete event?"
|
||||||
open={open}
|
open={open}
|
@ -2,7 +2,7 @@ import { VFC } from 'react';
|
|||||||
import { Box, styled } from '@mui/material';
|
import { Box, styled } from '@mui/material';
|
||||||
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
||||||
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
import { ISignOnEvent } from 'interfaces/signOnEvent';
|
import { ILoginEvent } from 'interfaces/loginEvent';
|
||||||
import { Badge } from 'component/common/Badge/Badge';
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
||||||
|
|
||||||
@ -11,17 +11,16 @@ const StyledBox = styled(Box)(() => ({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface ISignOnLogSuccessfulCellProps {
|
interface ILoginHistorySuccessfulCellProps {
|
||||||
row: {
|
row: {
|
||||||
original: ISignOnEvent;
|
original: ILoginEvent;
|
||||||
};
|
};
|
||||||
value: boolean;
|
value: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SignOnLogSuccessfulCell: VFC<ISignOnLogSuccessfulCellProps> = ({
|
export const LoginHistorySuccessfulCell: VFC<
|
||||||
row,
|
ILoginHistorySuccessfulCellProps
|
||||||
value,
|
> = ({ row, value }) => {
|
||||||
}) => {
|
|
||||||
const { searchQuery } = useSearchHighlightContext();
|
const { searchQuery } = useSearchHighlightContext();
|
||||||
|
|
||||||
if (value)
|
if (value)
|
@ -16,17 +16,17 @@ import { Search } from 'component/common/Search/Search';
|
|||||||
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
||||||
import { useSearch } from 'hooks/useSearch';
|
import { useSearch } from 'hooks/useSearch';
|
||||||
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
|
import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
|
||||||
import { useSignOnLog } from 'hooks/api/getters/useSignOnLog/useSignOnLog';
|
import { useLoginHistory } from 'hooks/api/getters/useLoginHistory/useLoginHistory';
|
||||||
import { SignOnLogSuccessfulCell } from './SignOnLogSuccessfulCell/SignOnLogSuccessfulCell';
|
import { LoginHistorySuccessfulCell } from './LoginHistorySuccessfulCell/LoginHistorySuccessfulCell';
|
||||||
import { ISignOnEvent } from 'interfaces/signOnEvent';
|
import { ILoginEvent } from 'interfaces/loginEvent';
|
||||||
import { SignOnLogActionsCell } from './SignOnLogActionsCell/SignOnLogActionsCell';
|
import { LoginHistoryActionsCell } from './LoginHistoryActionsCell/LoginHistoryActionsCell';
|
||||||
import { SignOnLogDeleteDialog } from './SignOnLogDeleteDialog/SignOnLogDeleteDialog';
|
import { LoginHistoryDeleteDialog } from './LoginHistoryDeleteDialog/LoginHistoryDeleteDialog';
|
||||||
import { useSignOnLogApi } from 'hooks/api/actions/useSignOnLogApi/useSignOnLogApi';
|
import { useLoginHistoryApi } from 'hooks/api/actions/useLoginHistoryApi/useLoginHistoryApi';
|
||||||
import { formatDateYMDHMS } from 'utils/formatDate';
|
import { formatDateYMDHMS } from 'utils/formatDate';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||||
import { Delete, Download } from '@mui/icons-material';
|
import { Delete, Download } from '@mui/icons-material';
|
||||||
import { SignOnLogDeleteAllDialog } from './SignOnLogDeleteAllDialog/SignOnLogDeleteAllDialog';
|
import { LoginHistoryDeleteAllDialog } from './LoginHistoryDeleteAllDialog/LoginHistoryDeleteAllDialog';
|
||||||
|
|
||||||
export type PageQueryType = Partial<
|
export type PageQueryType = Partial<
|
||||||
Record<'sort' | 'order' | 'search', string>
|
Record<'sort' | 'order' | 'search', string>
|
||||||
@ -35,7 +35,7 @@ export type PageQueryType = Partial<
|
|||||||
const defaultSort: SortingRule<string> = { id: 'created_at' };
|
const defaultSort: SortingRule<string> = { id: 'created_at' };
|
||||||
|
|
||||||
const { value: storedParams, setValue: setStoredParams } = createLocalStorage(
|
const { value: storedParams, setValue: setStoredParams } = createLocalStorage(
|
||||||
'SignOnLogTable:v1',
|
'LoginHistoryTable:v1',
|
||||||
defaultSort
|
defaultSort
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -46,11 +46,11 @@ const AUTH_TYPE_LABEL: { [key: string]: string } = {
|
|||||||
google: 'Google',
|
google: 'Google',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SignOnLogTable = () => {
|
export const LoginHistoryTable = () => {
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
|
||||||
const { events, loading, refetch } = useSignOnLog();
|
const { events, loading, refetch } = useLoginHistory();
|
||||||
const { removeEvent, removeAllEvents, downloadCSV } = useSignOnLogApi();
|
const { removeEvent, removeAllEvents, downloadCSV } = useLoginHistoryApi();
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [initialState] = useState(() => ({
|
const [initialState] = useState(() => ({
|
||||||
@ -67,11 +67,11 @@ export const SignOnLogTable = () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = useState(initialState.globalFilter);
|
const [searchValue, setSearchValue] = useState(initialState.globalFilter);
|
||||||
const [selectedEvent, setSelectedEvent] = useState<ISignOnEvent>();
|
const [selectedEvent, setSelectedEvent] = useState<ILoginEvent>();
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
const [deleteAllOpen, setDeleteAllOpen] = useState(false);
|
const [deleteAllOpen, setDeleteAllOpen] = useState(false);
|
||||||
|
|
||||||
const onDeleteConfirm = async (event: ISignOnEvent) => {
|
const onDeleteConfirm = async (event: ILoginEvent) => {
|
||||||
try {
|
try {
|
||||||
await removeEvent(event.id);
|
await removeEvent(event.id);
|
||||||
setToastData({
|
setToastData({
|
||||||
@ -89,7 +89,7 @@ export const SignOnLogTable = () => {
|
|||||||
try {
|
try {
|
||||||
await removeAllEvents();
|
await removeAllEvents();
|
||||||
setToastData({
|
setToastData({
|
||||||
title: `Log has been cleared`,
|
title: `History has been cleared`,
|
||||||
type: 'success',
|
type: 'success',
|
||||||
});
|
});
|
||||||
refetch();
|
refetch();
|
||||||
@ -122,7 +122,7 @@ export const SignOnLogTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Authentication',
|
Header: 'Authentication',
|
||||||
accessor: (event: ISignOnEvent) =>
|
accessor: (event: ILoginEvent) =>
|
||||||
AUTH_TYPE_LABEL[event.auth_type] || event.auth_type,
|
AUTH_TYPE_LABEL[event.auth_type] || event.auth_type,
|
||||||
width: 150,
|
width: 150,
|
||||||
maxWidth: 150,
|
maxWidth: 150,
|
||||||
@ -140,7 +140,7 @@ export const SignOnLogTable = () => {
|
|||||||
Header: 'Success',
|
Header: 'Success',
|
||||||
accessor: 'successful',
|
accessor: 'successful',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
Cell: SignOnLogSuccessfulCell,
|
Cell: LoginHistorySuccessfulCell,
|
||||||
filterName: 'success',
|
filterName: 'success',
|
||||||
filterParsing: (value: boolean) => value.toString(),
|
filterParsing: (value: boolean) => value.toString(),
|
||||||
},
|
},
|
||||||
@ -149,7 +149,7 @@ export const SignOnLogTable = () => {
|
|||||||
id: 'Actions',
|
id: 'Actions',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
Cell: ({ row: { original: event } }: any) => (
|
Cell: ({ row: { original: event } }: any) => (
|
||||||
<SignOnLogActionsCell
|
<LoginHistoryActionsCell
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
setSelectedEvent(event);
|
setSelectedEvent(event);
|
||||||
setDeleteOpen(true);
|
setDeleteOpen(true);
|
||||||
@ -238,7 +238,7 @@ export const SignOnLogTable = () => {
|
|||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
header={
|
header={
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={`Sign-on log (${rows.length})`}
|
title={`Login history (${rows.length})`}
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
@ -261,7 +261,7 @@ export const SignOnLogTable = () => {
|
|||||||
show={<PageHeader.Divider />}
|
show={<PageHeader.Divider />}
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title="Download sign-on log"
|
title="Download login history"
|
||||||
arrow
|
arrow
|
||||||
>
|
>
|
||||||
<IconButton onClick={downloadCSV}>
|
<IconButton onClick={downloadCSV}>
|
||||||
@ -269,7 +269,7 @@ export const SignOnLogTable = () => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title="Clear sign-on log"
|
title="Clear login history"
|
||||||
arrow
|
arrow
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
@ -314,26 +314,26 @@ export const SignOnLogTable = () => {
|
|||||||
condition={searchValue?.length > 0}
|
condition={searchValue?.length > 0}
|
||||||
show={
|
show={
|
||||||
<TablePlaceholder>
|
<TablePlaceholder>
|
||||||
No sign-on events found matching “
|
No login events found matching “
|
||||||
{searchValue}
|
{searchValue}
|
||||||
”
|
”
|
||||||
</TablePlaceholder>
|
</TablePlaceholder>
|
||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<TablePlaceholder>
|
<TablePlaceholder>
|
||||||
No sign-on events available.
|
No login events available.
|
||||||
</TablePlaceholder>
|
</TablePlaceholder>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<SignOnLogDeleteDialog
|
<LoginHistoryDeleteDialog
|
||||||
event={selectedEvent}
|
event={selectedEvent}
|
||||||
open={deleteOpen}
|
open={deleteOpen}
|
||||||
setOpen={setDeleteOpen}
|
setOpen={setDeleteOpen}
|
||||||
onConfirm={onDeleteConfirm}
|
onConfirm={onDeleteConfirm}
|
||||||
/>
|
/>
|
||||||
<SignOnLogDeleteAllDialog
|
<LoginHistoryDeleteAllDialog
|
||||||
open={deleteAllOpen}
|
open={deleteAllOpen}
|
||||||
setOpen={setDeleteAllOpen}
|
setOpen={setDeleteAllOpen}
|
||||||
onConfirm={onDeleteAllConfirm}
|
onConfirm={onDeleteAllConfirm}
|
@ -352,8 +352,8 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
"menu": {
|
"menu": {
|
||||||
"adminSettings": true,
|
"adminSettings": true,
|
||||||
},
|
},
|
||||||
"path": "/admin/signons",
|
"path": "/admin/logins",
|
||||||
"title": "Sign on log",
|
"title": "Login history",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -43,7 +43,7 @@ import { LazyFeatureView } from 'component/feature/FeatureView/LazyFeatureView';
|
|||||||
import { LazyAdmin } from 'component/admin/LazyAdmin';
|
import { LazyAdmin } from 'component/admin/LazyAdmin';
|
||||||
import { LazyProject } from 'component/project/Project/LazyProject';
|
import { LazyProject } from 'component/project/Project/LazyProject';
|
||||||
import { AdminRedirect } from 'component/admin/AdminRedirect';
|
import { AdminRedirect } from 'component/admin/AdminRedirect';
|
||||||
import { SignOnLog } from 'component/signOnLog/SignOnLog';
|
import { LoginHistory } from 'component/loginHistory/LoginHistory';
|
||||||
|
|
||||||
export const routes: IRoute[] = [
|
export const routes: IRoute[] = [
|
||||||
// Splash
|
// Splash
|
||||||
@ -357,9 +357,9 @@ export const routes: IRoute[] = [
|
|||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/admin/signons',
|
path: '/admin/logins',
|
||||||
title: 'Sign on log',
|
title: 'Login history',
|
||||||
component: SignOnLog,
|
component: LoginHistory,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
menu: { adminSettings: true },
|
menu: { adminSettings: true },
|
||||||
},
|
},
|
||||||
@ -448,10 +448,10 @@ export const adminMenuRoutes: INavigationMenuItem[] = [
|
|||||||
menu: { adminSettings: true },
|
menu: { adminSettings: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/signons',
|
path: '/admin/logins',
|
||||||
title: 'Sign-on log',
|
title: 'Login history',
|
||||||
menu: { adminSettings: true },
|
menu: { adminSettings: true },
|
||||||
flag: 'signOnLog',
|
flag: 'loginHistory',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/users',
|
path: '/admin/users',
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Alert } from '@mui/material';
|
import { Alert } from '@mui/material';
|
||||||
import { useSegmentLimits } from 'hooks/api/getters/useSegmentLimits/useSegmentLimits';
|
import { useSegmentLimits } from 'hooks/api/getters/useSegmentLimits/useSegmentLimits';
|
||||||
|
|
||||||
export const SegmentDocsValuesWarning = () => {
|
export const SegmentDocsValuesInfo = () => {
|
||||||
const { segmentValuesLimit } = useSegmentLimits();
|
const { segmentValuesLimit } = useSegmentLimits();
|
||||||
|
|
||||||
if (typeof segmentValuesLimit === 'undefined') {
|
if (typeof segmentValuesLimit === 'undefined') {
|
||||||
@ -9,9 +9,16 @@ export const SegmentDocsValuesWarning = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert severity="warning">
|
<Alert severity="info">
|
||||||
Segments is an experimental feature, currently limited to at most{' '}
|
A segment can have{' '}
|
||||||
{segmentValuesLimit} values. <SegmentLimitsLink />
|
<a
|
||||||
|
href="https://docs.getunleash.io/reference/segments#segment-limits"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
at most {segmentValuesLimit} across all of its contraints
|
||||||
|
</a>
|
||||||
|
. <SegmentLimitsLink />
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -25,8 +32,15 @@ export const SegmentDocsValuesError = (props: { values: number }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert severity="error">
|
<Alert severity="error">
|
||||||
Segments are limited to at most {segmentValuesLimit} values. This
|
A segment can have{' '}
|
||||||
segment currently has {props.values}{' '}
|
<a
|
||||||
|
href="https://docs.getunleash.io/reference/segments#segment-limits"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
at most {segmentValuesLimit} across all of its contraints
|
||||||
|
</a>
|
||||||
|
. This segment has {props.values}{' '}
|
||||||
{props.values === 1 ? 'value' : 'values'}.
|
{props.values === 1 ? 'value' : 'values'}.
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
@ -41,8 +55,8 @@ export const SegmentDocsStrategyWarning = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert severity="warning">
|
<Alert severity="warning">
|
||||||
Strategies are limited to {strategySegmentsLimit} segments.{' '}
|
You can't apply more than {strategySegmentsLimit} segments to a
|
||||||
<SegmentLimitsLink />
|
strategy. <SegmentLimitsLink />
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -50,16 +64,14 @@ export const SegmentDocsStrategyWarning = () => {
|
|||||||
const SegmentLimitsLink = () => {
|
const SegmentLimitsLink = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
Please{' '}
|
|
||||||
<a
|
<a
|
||||||
href="https://slack.unleash.run"
|
href="https://slack.unleash.run"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
style={{ color: 'inherit' }}
|
|
||||||
>
|
>
|
||||||
get in touch
|
Get in touch
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
if you would like this limit increased.
|
if you'd like to increase this limit.
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -23,7 +23,7 @@ import {
|
|||||||
IAutocompleteBoxOption,
|
IAutocompleteBoxOption,
|
||||||
} from 'component/common/AutocompleteBox/AutocompleteBox';
|
} from 'component/common/AutocompleteBox/AutocompleteBox';
|
||||||
import {
|
import {
|
||||||
SegmentDocsValuesWarning,
|
SegmentDocsValuesInfo,
|
||||||
SegmentDocsValuesError,
|
SegmentDocsValuesError,
|
||||||
} from 'component/segments/SegmentDocs';
|
} from 'component/segments/SegmentDocs';
|
||||||
import { useSegmentValuesCount } from 'component/segments/hooks/useSegmentValuesCount';
|
import { useSegmentValuesCount } from 'component/segments/hooks/useSegmentValuesCount';
|
||||||
@ -43,7 +43,7 @@ const StyledForm = styled('div')(({ theme }) => ({
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledWarning = styled('div')(({ theme }) => ({
|
const StyledInfo = styled('div')(({ theme }) => ({
|
||||||
marginBottom: '1.5rem',
|
marginBottom: '1.5rem',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -130,9 +130,9 @@ export const SegmentFormStepTwo: React.FC<ISegmentFormPartTwoProps> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledForm>
|
<StyledForm>
|
||||||
<StyledWarning>
|
<StyledInfo>
|
||||||
<SegmentDocsValuesWarning />
|
<SegmentDocsValuesInfo />
|
||||||
</StyledWarning>
|
</StyledInfo>
|
||||||
<div>
|
<div>
|
||||||
<StyledInputDescription>
|
<StyledInputDescription>
|
||||||
Select the context fields you want to include in the
|
Select the context fields you want to include in the
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import useAPI from '../useApi/useApi';
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
export const useSignOnLogApi = () => {
|
export const useLoginHistoryApi = () => {
|
||||||
const { loading, makeRequest, createRequest, errors } = useAPI({
|
const { loading, makeRequest, createRequest, errors } = useAPI({
|
||||||
propagateErrors: true,
|
propagateErrors: true,
|
||||||
});
|
});
|
||||||
@ -8,7 +8,7 @@ export const useSignOnLogApi = () => {
|
|||||||
const downloadCSV = async () => {
|
const downloadCSV = async () => {
|
||||||
const requestId = 'downloadCSV';
|
const requestId = 'downloadCSV';
|
||||||
const req = createRequest(
|
const req = createRequest(
|
||||||
'api/admin/signons',
|
'api/admin/logins',
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
@ -25,7 +25,7 @@ export const useSignOnLogApi = () => {
|
|||||||
const removeEvent = async (eventId: number) => {
|
const removeEvent = async (eventId: number) => {
|
||||||
const requestId = 'removeEvent';
|
const requestId = 'removeEvent';
|
||||||
const req = createRequest(
|
const req = createRequest(
|
||||||
`api/admin/signons/${eventId}`,
|
`api/admin/logins/${eventId}`,
|
||||||
{ method: 'DELETE' },
|
{ method: 'DELETE' },
|
||||||
requestId
|
requestId
|
||||||
);
|
);
|
||||||
@ -36,7 +36,7 @@ export const useSignOnLogApi = () => {
|
|||||||
const removeAllEvents = async () => {
|
const removeAllEvents = async () => {
|
||||||
const requestId = 'removeAllEvents';
|
const requestId = 'removeAllEvents';
|
||||||
const req = createRequest(
|
const req = createRequest(
|
||||||
'api/admin/signons',
|
'api/admin/logins',
|
||||||
{ method: 'DELETE' },
|
{ method: 'DELETE' },
|
||||||
requestId
|
requestId
|
||||||
);
|
);
|
@ -1,25 +1,25 @@
|
|||||||
import { ISignOnEvent } from 'interfaces/signOnEvent';
|
import { ILoginEvent } from 'interfaces/loginEvent';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { formatApiPath } from 'utils/formatPath';
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
|
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
|
||||||
import useUiConfig from '../useUiConfig/useUiConfig';
|
import useUiConfig from '../useUiConfig/useUiConfig';
|
||||||
|
|
||||||
export const useSignOnLog = () => {
|
export const useLoginHistory = () => {
|
||||||
const { uiConfig, isEnterprise } = useUiConfig();
|
const { uiConfig, isEnterprise } = useUiConfig();
|
||||||
|
|
||||||
const { signOnLog } = uiConfig.flags;
|
const { loginHistory } = uiConfig.flags;
|
||||||
|
|
||||||
const { data, error, mutate } = useConditionalSWR(
|
const { data, error, mutate } = useConditionalSWR(
|
||||||
signOnLog && isEnterprise(),
|
loginHistory && isEnterprise(),
|
||||||
{ events: [] },
|
{ events: [] },
|
||||||
formatApiPath(`api/admin/signons`),
|
formatApiPath(`api/admin/logins`),
|
||||||
fetcher
|
fetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
events: (data?.events ?? []) as ISignOnEvent[],
|
events: (data?.events ?? []) as ILoginEvent[],
|
||||||
loading: !error && !data,
|
loading: !error && !data,
|
||||||
refetch: () => mutate(),
|
refetch: () => mutate(),
|
||||||
error,
|
error,
|
||||||
@ -30,6 +30,6 @@ export const useSignOnLog = () => {
|
|||||||
|
|
||||||
const fetcher = (path: string) => {
|
const fetcher = (path: string) => {
|
||||||
return fetch(path)
|
return fetch(path)
|
||||||
.then(handleErrorResponses('Sign-On Log'))
|
.then(handleErrorResponses('Login History'))
|
||||||
.then(res => res.json());
|
.then(res => res.json());
|
||||||
};
|
};
|
@ -1,4 +1,4 @@
|
|||||||
export interface ISignOnEvent {
|
export interface ILoginEvent {
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
auth_type: string;
|
auth_type: string;
|
@ -47,7 +47,7 @@ export interface IFlags {
|
|||||||
showProjectApiAccess?: boolean;
|
showProjectApiAccess?: boolean;
|
||||||
proPlanAutoCharge?: boolean;
|
proPlanAutoCharge?: boolean;
|
||||||
notifications?: boolean;
|
notifications?: boolean;
|
||||||
signOnLog?: boolean;
|
loginHistory?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "unleash-server",
|
"name": "unleash-server",
|
||||||
"description": "Unleash is an enterprise ready feature toggles service. It provides different strategies for handling feature toggles.",
|
"description": "Unleash is an enterprise ready feature toggles service. It provides different strategies for handling feature toggles.",
|
||||||
"version": "4.22.0-beta.33",
|
"version": "4.22.0-beta.35",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"unleash",
|
"unleash",
|
||||||
"feature toggle",
|
"feature toggle",
|
||||||
|
@ -74,6 +74,7 @@ exports[`should create default config 1`] = `
|
|||||||
"embedProxy": true,
|
"embedProxy": true,
|
||||||
"embedProxyFrontend": true,
|
"embedProxyFrontend": true,
|
||||||
"featuresExportImport": false,
|
"featuresExportImport": false,
|
||||||
|
"loginHistory": false,
|
||||||
"maintenanceMode": false,
|
"maintenanceMode": false,
|
||||||
"messageBanner": false,
|
"messageBanner": false,
|
||||||
"newProjectOverview": false,
|
"newProjectOverview": false,
|
||||||
@ -83,7 +84,6 @@ exports[`should create default config 1`] = `
|
|||||||
"proxyReturnAllToggles": false,
|
"proxyReturnAllToggles": false,
|
||||||
"responseTimeWithAppNameKillSwitch": false,
|
"responseTimeWithAppNameKillSwitch": false,
|
||||||
"showProjectApiAccess": false,
|
"showProjectApiAccess": false,
|
||||||
"signOnLog": false,
|
|
||||||
"strictSchemaValidation": false,
|
"strictSchemaValidation": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -96,6 +96,7 @@ exports[`should create default config 1`] = `
|
|||||||
"embedProxy": true,
|
"embedProxy": true,
|
||||||
"embedProxyFrontend": true,
|
"embedProxyFrontend": true,
|
||||||
"featuresExportImport": false,
|
"featuresExportImport": false,
|
||||||
|
"loginHistory": false,
|
||||||
"maintenanceMode": false,
|
"maintenanceMode": false,
|
||||||
"messageBanner": false,
|
"messageBanner": false,
|
||||||
"newProjectOverview": false,
|
"newProjectOverview": false,
|
||||||
@ -105,7 +106,6 @@ exports[`should create default config 1`] = `
|
|||||||
"proxyReturnAllToggles": false,
|
"proxyReturnAllToggles": false,
|
||||||
"responseTimeWithAppNameKillSwitch": false,
|
"responseTimeWithAppNameKillSwitch": false,
|
||||||
"showProjectApiAccess": false,
|
"showProjectApiAccess": false,
|
||||||
"signOnLog": false,
|
|
||||||
"strictSchemaValidation": false,
|
"strictSchemaValidation": false,
|
||||||
},
|
},
|
||||||
"externalResolver": {
|
"externalResolver": {
|
||||||
|
@ -63,7 +63,7 @@ const flags = {
|
|||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
notifications: parseEnvVarBoolean(process.env.NOTIFICATIONS, false),
|
notifications: parseEnvVarBoolean(process.env.NOTIFICATIONS, false),
|
||||||
signOnLog: parseEnvVarBoolean(process.env.UNLEASH_SIGN_ON_LOG, false),
|
loginHistory: parseEnvVarBoolean(process.env.UNLEASH_LOGIN_HISTORY, false),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
exports.up = function (db, cb) {
|
||||||
|
db.runSql(`ALTER TABLE sign_on_log RENAME TO login_history`, cb);
|
||||||
|
db.runSql(`DELETE FROM settings WHERE name = 'sign_on_log_retention'`, cb);
|
||||||
|
db.runSql(
|
||||||
|
`INSERT INTO settings(name, content) VALUES ('login_history_retention', '{"hours": 336}')`,
|
||||||
|
cb,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (db, cb) {
|
||||||
|
db.runSql(`ALTER TABLE login_history RENAME TO sign_on_log`, cb);
|
||||||
|
db.runSql(
|
||||||
|
`DELETE FROM settings WHERE name = 'login_history_retention'`,
|
||||||
|
cb,
|
||||||
|
);
|
||||||
|
db.runSql(
|
||||||
|
`INSERT INTO settings(name, content) VALUES ('sign_on_log_retention', '{"hours": 336}')`,
|
||||||
|
cb,
|
||||||
|
);
|
||||||
|
};
|
@ -180,6 +180,20 @@ const start = async () => {
|
|||||||
start();
|
start();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Segment limits {#segments}
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
|
||||||
|
Changing segment limits could have a negative impact on the performance of Unleash SDKs and cause network congestion. Think twice before changing these values.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
Some facets of the [segments feature](../segments.mdx) can be customized via environment variables. This lets you change the [segment limits](../segments.mdx#segment-limits) that Unleash uses.
|
||||||
|
|
||||||
|
`UNLEASH_STRATEGY_SEGMENTS_LIMIT` controls the maximum number of segments that can be applied to a single strategy. The default is 5.
|
||||||
|
|
||||||
|
`UNLEASH_SEGMENT_VALUES_LIMIT` controls the maximum number of values that you can assign across a segment's constraints. The default is 100.
|
||||||
|
|
||||||
## Securing Unleash {#securing-unleash}
|
## Securing Unleash {#securing-unleash}
|
||||||
|
|
||||||
You can integrate Unleash with your authentication provider (OAuth 2.0). Read more about [securing unleash](./securing-unleash.md).
|
You can integrate Unleash with your authentication provider (OAuth 2.0). Read more about [securing unleash](./securing-unleash.md).
|
||||||
|
@ -38,6 +38,22 @@ Segments are collections of strategy constraints. To satisfy a segment, _all_ th
|
|||||||
|
|
||||||
If an activation strategy has a segment _and_ additional constraints applied, the segment _and_ the strategies must all be satisfied. Similarly, if an activation strategy has multiple segments, then they must _must all be satisfied_.
|
If an activation strategy has a segment _and_ additional constraints applied, the segment _and_ the strategies must all be satisfied. Similarly, if an activation strategy has multiple segments, then they must _must all be satisfied_.
|
||||||
|
|
||||||
|
## Segment limits
|
||||||
|
|
||||||
|
In theory, you could create segments with a thousand constraints, each with a million values. But this wouldn't scale well, so there are limitations in place to stop you from doing this. Unleash enforces the following limits on use of segments:
|
||||||
|
|
||||||
|
1. By default, a segment can have **at most 100 values** specified across all of its constraints. That means that if you add a constraint that uses 10 values, you will have 90 more values to use for any other constraints you add to the same segment.
|
||||||
|
|
||||||
|
2. By default, you can apply **at most 5 segments to any one strategy**. Separate strategies (even on the same feature) do not count towards the same total, so you can have two strategies with 5 segments each.
|
||||||
|
|
||||||
|
You **can** [configure segment limits](deploy/configuring-unleash.md#segments) with environment variables.
|
||||||
|
|
||||||
|
### A note on large segments {#large-segments}
|
||||||
|
|
||||||
|
Segments are just constraints, so any limitations that apply to constraints also apply to segments.
|
||||||
|
|
||||||
|
This means that if you want to add a hundred different user IDs to one of your constraints, you are most likely better off thinking about finding another way to solve this problem. That may be using a different abstraction or finding another pattern that you can use instead. Refer to the section on [constraint limitations](../reference/strategy-constraints.md#limitations) for a more thorough explanation.
|
||||||
|
|
||||||
## Creating, updating, and deleting segments
|
## Creating, updating, and deleting segments
|
||||||
|
|
||||||
Segments can be created, edited, and deleted from the segments page in the admin UI or via the API (see the [segments API documentation](/reference/api/legacy/unleash/admin/segments.mdx)).
|
Segments can be created, edited, and deleted from the segments page in the admin UI or via the API (see the [segments API documentation](/reference/api/legacy/unleash/admin/segments.mdx)).
|
||||||
@ -45,9 +61,3 @@ Segments can be created, edited, and deleted from the segments page in the admin
|
|||||||
A segment that is in use **cannot** be deleted. If you'd like to delete a segment that is in use, you must first remove the segment from all the activation strategies that are currently using it.
|
A segment that is in use **cannot** be deleted. If you'd like to delete a segment that is in use, you must first remove the segment from all the activation strategies that are currently using it.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### A note on large segments and limits {#large-segments}
|
|
||||||
|
|
||||||
In theory, you could you create segments with a thousand constraints, each with a million values. But this wouldn't scale well, so there are limitations in place to stop you from doing this. Segments are just constraints, so any limitations that apply to constraints also apply to segments.
|
|
||||||
|
|
||||||
This means that if you want to add a hundred different user IDs to one of your constraints, you are most likely better off thinking about finding another way to solve this problem. That may be using a different abstraction or finding another pattern that you can use instead. Refer to the section on [constraint limitations](../reference/strategy-constraints.md#limitations) for a more thorough explanation.
|
|
||||||
|
Loading…
Reference in New Issue
Block a user