mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-27 13:49:10 +02:00
Merge branch 'main' into feat/impact-metrics-frontend
This commit is contained in:
commit
071c869433
2
.github/workflows/build_prs_jest_report.yaml
vendored
2
.github/workflows/build_prs_jest_report.yaml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull_requests: write
|
||||
pull-requests: write
|
||||
name: build # temporary solution to trick branch protection rules
|
||||
|
||||
services:
|
||||
|
2
.github/workflows/hypermod.yml
vendored
2
.github/workflows/hypermod.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
permissions: write-all
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run Hypermod CLI
|
||||
uses: hypermod-io/action@v1
|
||||
with:
|
||||
|
@ -37,7 +37,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@codemirror/lang-json": "6.0.1",
|
||||
"@codemirror/lang-json": "6.0.2",
|
||||
"@emotion/react": "11.11.4",
|
||||
"@emotion/styled": "11.11.5",
|
||||
"@mui/icons-material": "5.15.3",
|
||||
@ -59,8 +59,8 @@
|
||||
"@types/lodash.mapvalues": "^4.6.9",
|
||||
"@types/lodash.omit": "4.5.9",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react": "18.3.23",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
"@types/react-table": "7.7.20",
|
||||
"@types/react-test-renderer": "18.3.1",
|
||||
@ -116,7 +116,7 @@
|
||||
"sass": "1.85.1",
|
||||
"semver": "7.7.2",
|
||||
"swr": "2.3.3",
|
||||
"tss-react": "4.9.15",
|
||||
"tss-react": "4.9.18",
|
||||
"typescript": "5.8.3",
|
||||
"unleash-proxy-client": "^3.7.3",
|
||||
"use-query-params": "^2.2.1",
|
||||
@ -136,7 +136,7 @@
|
||||
"vite": "5.4.19",
|
||||
"semver": "7.7.2",
|
||||
"ws": "^8.18.0",
|
||||
"@types/react": "18.3.18"
|
||||
"@types/react": "18.3.23"
|
||||
},
|
||||
"jest": {
|
||||
"moduleNameMapper": {
|
||||
|
@ -0,0 +1,182 @@
|
||||
import type { FC } from 'react';
|
||||
import {
|
||||
styled,
|
||||
Button,
|
||||
Checkbox,
|
||||
TextField,
|
||||
useTheme,
|
||||
type AutocompleteChangeReason,
|
||||
type FilterOptionsState,
|
||||
} from '@mui/material';
|
||||
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
|
||||
import CheckBoxIcon from '@mui/icons-material/CheckBox';
|
||||
import AutocompleteVirtual from 'component/common/AutocompleteVirtual/AutcompleteVirtual';
|
||||
import { caseInsensitiveSearch } from 'utils/search';
|
||||
import type { ChangeRequestType } from 'component/changeRequest/changeRequest.types';
|
||||
import { changesCount } from '../../changesCount.js';
|
||||
import {
|
||||
type AvailableReviewerSchema,
|
||||
useAvailableChangeRequestReviewers,
|
||||
} from 'hooks/api/getters/useAvailableChangeRequestReviewers/useAvailableChangeRequestReviewers.js';
|
||||
|
||||
const SubmitChangeRequestButton: FC<{
|
||||
onClick: () => void;
|
||||
count: number;
|
||||
disabled?: boolean;
|
||||
}> = ({ onClick, count, disabled = false }) => (
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
variant='contained'
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
Submit change request ({count})
|
||||
</Button>
|
||||
);
|
||||
|
||||
const StyledTags = styled('div')(({ theme }) => ({
|
||||
paddingLeft: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StrechedLi = styled('li')({ width: '100%' });
|
||||
|
||||
const StyledOption = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
'& > span:first-of-type': {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}));
|
||||
|
||||
const renderOption = (
|
||||
props: React.HTMLAttributes<HTMLLIElement>,
|
||||
option: AvailableReviewerSchema,
|
||||
{ selected }: { selected: boolean },
|
||||
) => (
|
||||
<StrechedLi {...props} key={option.id}>
|
||||
<Checkbox
|
||||
icon={<CheckBoxOutlineBlankIcon fontSize='small' />}
|
||||
checkedIcon={<CheckBoxIcon fontSize='small' />}
|
||||
style={{ marginRight: 8 }}
|
||||
checked={selected}
|
||||
/>
|
||||
<StyledOption>
|
||||
<span>{option.name || option.username}</span>
|
||||
<span>
|
||||
{option.name && option.username
|
||||
? option.username
|
||||
: option.email}
|
||||
</span>
|
||||
</StyledOption>
|
||||
</StrechedLi>
|
||||
);
|
||||
|
||||
const renderTags = (value: AvailableReviewerSchema[]) => (
|
||||
<StyledTags>
|
||||
{value.length > 1
|
||||
? `${value.length} users selected`
|
||||
: value[0].name || value[0].username || value[0].email}
|
||||
</StyledTags>
|
||||
);
|
||||
|
||||
export const DraftChangeRequestActions: FC<{
|
||||
environmentChangeRequest: ChangeRequestType;
|
||||
reviewers: AvailableReviewerSchema[];
|
||||
setReviewers: React.Dispatch<
|
||||
React.SetStateAction<AvailableReviewerSchema[]>
|
||||
>;
|
||||
onReview: (changeState: (project: string) => Promise<void>) => void;
|
||||
onDiscard: (id: number) => void;
|
||||
sendToReview: (project: string) => Promise<void>;
|
||||
disabled?: boolean;
|
||||
setDisabled: (disabled: boolean) => void;
|
||||
}> = ({
|
||||
environmentChangeRequest,
|
||||
reviewers,
|
||||
setReviewers,
|
||||
onReview,
|
||||
onDiscard,
|
||||
sendToReview,
|
||||
disabled,
|
||||
setDisabled,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { reviewers: availableReviewers, loading: isLoading } =
|
||||
useAvailableChangeRequestReviewers(
|
||||
environmentChangeRequest.project,
|
||||
environmentChangeRequest.environment,
|
||||
);
|
||||
|
||||
const autoCompleteChange = (
|
||||
event: React.SyntheticEvent,
|
||||
newValue: AvailableReviewerSchema[],
|
||||
reason: AutocompleteChangeReason,
|
||||
) => {
|
||||
if (
|
||||
event.type === 'keydown' &&
|
||||
(event as React.KeyboardEvent).key === 'Backspace' &&
|
||||
reason === 'removeOption'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setReviewers(newValue);
|
||||
};
|
||||
|
||||
const filterOptions = (
|
||||
options: AvailableReviewerSchema[],
|
||||
{ inputValue }: FilterOptionsState<AvailableReviewerSchema>,
|
||||
) =>
|
||||
options.filter(
|
||||
({ name, username, email }) =>
|
||||
caseInsensitiveSearch(inputValue, email) ||
|
||||
caseInsensitiveSearch(inputValue, name) ||
|
||||
caseInsensitiveSearch(inputValue, username),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutocompleteVirtual
|
||||
sx={{ ml: 'auto', width: theme.spacing(40) }}
|
||||
size='small'
|
||||
limitTags={3}
|
||||
openOnFocus
|
||||
multiple
|
||||
disableCloseOnSelect
|
||||
value={reviewers as AvailableReviewerSchema[]}
|
||||
onChange={autoCompleteChange}
|
||||
options={availableReviewers}
|
||||
renderOption={renderOption}
|
||||
filterOptions={filterOptions}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
getOptionLabel={(option: AvailableReviewerSchema) =>
|
||||
option.email || option.name || option.username || ''
|
||||
}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={`Reviewers (${reviewers.length})`}
|
||||
/>
|
||||
)}
|
||||
renderTags={(value) => renderTags(value)}
|
||||
noOptionsText={isLoading ? 'Loading…' : 'No options'}
|
||||
/>
|
||||
<SubmitChangeRequestButton
|
||||
onClick={() => onReview(sendToReview)}
|
||||
count={changesCount(environmentChangeRequest)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
variant='outlined'
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
setDisabled(true);
|
||||
onDiscard(environmentChangeRequest.id);
|
||||
}}
|
||||
>
|
||||
Discard changes
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
@ -24,6 +24,9 @@ import Input from 'component/common/Input/Input';
|
||||
import { ChangeRequestTitle } from './ChangeRequestTitle.tsx';
|
||||
import { UpdateCount } from 'component/changeRequest/UpdateCount';
|
||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { DraftChangeRequestActions } from '../DraftChangeRequestActions/DraftChangeRequestActions.tsx';
|
||||
import type { AvailableReviewerSchema } from 'hooks/api/getters/useAvailableChangeRequestReviewers/useAvailableChangeRequestReviewers.ts';
|
||||
|
||||
const SubmitChangeRequestButton: FC<{
|
||||
onClick: () => void;
|
||||
@ -71,11 +74,22 @@ export const EnvironmentChangeRequest: FC<{
|
||||
const [commentText, setCommentText] = useState('');
|
||||
const { user } = useAuthUser();
|
||||
const [title, setTitle] = useState(environmentChangeRequest.title);
|
||||
const { changeState } = useChangeRequestApi();
|
||||
const { changeState, updateRequestedReviewers } = useChangeRequestApi();
|
||||
const [reviewers, setReviewers] = useState<AvailableReviewerSchema[]>([]);
|
||||
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const approversEnabled = useUiFlag('changeRequestApproverEmails');
|
||||
const sendToReview = async (project: string) => {
|
||||
setDisabled(true);
|
||||
try {
|
||||
if (reviewers && reviewers.length > 0) {
|
||||
await updateRequestedReviewers(
|
||||
project,
|
||||
environmentChangeRequest.id,
|
||||
reviewers.map((reviewer) => reviewer.id),
|
||||
);
|
||||
}
|
||||
|
||||
await changeState(project, environmentChangeRequest.id, 'Draft', {
|
||||
state: 'In review',
|
||||
comment: commentText,
|
||||
@ -153,27 +167,50 @@ export const EnvironmentChangeRequest: FC<{
|
||||
<ConditionallyRender
|
||||
condition={environmentChangeRequest?.state === 'Draft'}
|
||||
show={
|
||||
<>
|
||||
<SubmitChangeRequestButton
|
||||
onClick={() => onReview(sendToReview)}
|
||||
count={changesCount(
|
||||
environmentChangeRequest,
|
||||
)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={approversEnabled}
|
||||
show={
|
||||
<DraftChangeRequestActions
|
||||
environmentChangeRequest={
|
||||
environmentChangeRequest
|
||||
}
|
||||
reviewers={reviewers}
|
||||
setReviewers={setReviewers}
|
||||
onReview={onReview}
|
||||
onDiscard={onDiscard}
|
||||
sendToReview={sendToReview}
|
||||
disabled={disabled}
|
||||
setDisabled={setDisabled}
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<>
|
||||
<SubmitChangeRequestButton
|
||||
onClick={() =>
|
||||
onReview(sendToReview)
|
||||
}
|
||||
count={changesCount(
|
||||
environmentChangeRequest,
|
||||
)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
variant='outlined'
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
setDisabled(true);
|
||||
onDiscard(environmentChangeRequest.id);
|
||||
}}
|
||||
>
|
||||
Discard changes
|
||||
</Button>
|
||||
</>
|
||||
<Button
|
||||
sx={{ ml: 2 }}
|
||||
variant='outlined'
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
setDisabled(true);
|
||||
onDiscard(
|
||||
environmentChangeRequest.id,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Discard changes
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
|
@ -139,6 +139,14 @@ export const DemoSteps = ({
|
||||
}
|
||||
}
|
||||
|
||||
step.onStep?.({
|
||||
el,
|
||||
index,
|
||||
next,
|
||||
step,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
if (!step.nextButton) {
|
||||
const clickHandler = (e: Event) => {
|
||||
abortController.abort();
|
||||
|
@ -12,6 +12,13 @@ export interface ITutorialTopicStep extends Step {
|
||||
backCloseModal?: boolean;
|
||||
backCollapseExpanded?: boolean;
|
||||
preventDefault?: boolean;
|
||||
onStep?: (params: {
|
||||
el: HTMLElement;
|
||||
index: number;
|
||||
next: (i?: number) => void;
|
||||
step: ITutorialTopicStep;
|
||||
signal: AbortSignal;
|
||||
}) => void;
|
||||
anyClick?: boolean;
|
||||
optional?: boolean;
|
||||
focus?: boolean | string;
|
||||
@ -246,6 +253,16 @@ export const TOPICS: ITutorialTopic[] = [
|
||||
placement: 'right',
|
||||
backCloseModal: true,
|
||||
},
|
||||
{
|
||||
title: 'Add constraint value',
|
||||
target: 'button[data-testid="CONSTRAINT_ADD_VALUES_BUTTON"]',
|
||||
content: (
|
||||
<Description>
|
||||
Add a new constraint value by using this button.
|
||||
</Description>
|
||||
),
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
title: 'Input value',
|
||||
target: 'div[data-testid="CONSTRAINT_VALUES_INPUT"]',
|
||||
@ -273,15 +290,29 @@ export const TOPICS: ITutorialTopic[] = [
|
||||
placement: 'right',
|
||||
nextButton: true,
|
||||
focus: 'input',
|
||||
onStep: ({ el, next, index, signal }) => {
|
||||
const input = el.querySelector('input');
|
||||
|
||||
input?.addEventListener(
|
||||
'keydown',
|
||||
(e) => {
|
||||
if (e.key === 'Enter' && input.value.trim()) {
|
||||
next(index);
|
||||
}
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Add value',
|
||||
target: 'button[data-testid="CONSTRAINT_VALUES_ADD_BUTTON"]',
|
||||
target: 'button[data-testid="CONSTRAINT_VALUES_ADD_BUTTON"]:not(:disabled)',
|
||||
content: (
|
||||
<Description>
|
||||
Add the constraint value by using this button.
|
||||
</Description>
|
||||
),
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
title: 'Save constraint setup',
|
||||
@ -623,6 +654,16 @@ export const TOPICS: ITutorialTopic[] = [
|
||||
placement: 'right',
|
||||
backCloseModal: true,
|
||||
},
|
||||
{
|
||||
title: 'Add constraint value',
|
||||
target: 'button[data-testid="CONSTRAINT_ADD_VALUES_BUTTON"]',
|
||||
content: (
|
||||
<Description>
|
||||
Add a new constraint value by using this button.
|
||||
</Description>
|
||||
),
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
title: 'Input value',
|
||||
target: 'div[data-testid="CONSTRAINT_VALUES_INPUT"]',
|
||||
@ -650,15 +691,29 @@ export const TOPICS: ITutorialTopic[] = [
|
||||
placement: 'right',
|
||||
nextButton: true,
|
||||
focus: 'input',
|
||||
onStep: ({ el, next, index, signal }) => {
|
||||
const input = el.querySelector('input');
|
||||
|
||||
input?.addEventListener(
|
||||
'keydown',
|
||||
(e) => {
|
||||
if (e.key === 'Enter' && input.value.trim()) {
|
||||
next(index);
|
||||
}
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Add value',
|
||||
target: 'button[data-testid="CONSTRAINT_VALUES_ADD_BUTTON"]',
|
||||
target: 'button[data-testid="CONSTRAINT_VALUES_ADD_BUTTON"]:not(:disabled)',
|
||||
content: (
|
||||
<Description>
|
||||
Add the constraint value by using this button.
|
||||
</Description>
|
||||
),
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
title: 'Save constraint setup',
|
||||
|
@ -130,6 +130,7 @@ export const AddValuesPopover: FC<AddValuesProps> = ({
|
||||
inputProps={{
|
||||
...inputProps,
|
||||
}}
|
||||
data-testid='CONSTRAINT_VALUES_INPUT'
|
||||
/>
|
||||
<AddButton
|
||||
variant='text'
|
||||
@ -137,6 +138,7 @@ export const AddValuesPopover: FC<AddValuesProps> = ({
|
||||
size='small'
|
||||
color='primary'
|
||||
disabled={!inputValue?.trim()}
|
||||
data-testid='CONSTRAINT_VALUES_ADD_BUTTON'
|
||||
>
|
||||
Add
|
||||
</AddButton>
|
||||
|
@ -74,6 +74,7 @@ export const AddValuesWidget = forwardRef<HTMLButtonElement, AddValuesProps>(
|
||||
ref={positioningRef}
|
||||
onClick={() => setOpen(true)}
|
||||
type='button'
|
||||
data-testid='CONSTRAINT_ADD_VALUES_BUTTON'
|
||||
>
|
||||
<Add />
|
||||
<span>Add values</span>
|
||||
|
@ -137,6 +137,11 @@ export const ProjectHealthChart: FC<IProjectHealthChartProps> = ({
|
||||
: 'health',
|
||||
xAxisKey: 'date',
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: !isAggregate,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
min: 0,
|
||||
|
@ -9,7 +9,6 @@ import { ProjectHealthGrid } from './ProjectHealthGrid.tsx';
|
||||
import { useFeedback } from 'component/feedbackNew/useFeedback';
|
||||
import FeedbackIcon from '@mui/icons-material/ChatOutlined';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
|
||||
const ModalContentContainer = styled('section')(({ theme }) => ({
|
||||
minHeight: '100vh',
|
||||
@ -18,7 +17,7 @@ const ModalContentContainer = styled('section')(({ theme }) => ({
|
||||
backgroundColor: theme.palette.background.default,
|
||||
display: 'flex',
|
||||
flexFlow: 'column',
|
||||
gap: theme.spacing(2),
|
||||
gap: theme.spacing(2.5),
|
||||
paddingInline: theme.spacing(4),
|
||||
paddingBlock: theme.spacing(3.75),
|
||||
}));
|
||||
@ -141,7 +140,6 @@ export const ProjectStatusModal = ({ open, onClose, onFollowLink }: Props) => {
|
||||
});
|
||||
};
|
||||
const { isOss } = useUiConfig();
|
||||
const healthToDebtEnabled = useUiFlag('healthToTechDebt');
|
||||
|
||||
return (
|
||||
<DynamicSidebarModal
|
||||
@ -161,9 +159,6 @@ export const ProjectStatusModal = ({ open, onClose, onFollowLink }: Props) => {
|
||||
</HeaderRow>
|
||||
<WidgetContainer>
|
||||
<Row>
|
||||
<RowHeader>
|
||||
{healthToDebtEnabled ? 'Technical debt' : 'Health'}
|
||||
</RowHeader>
|
||||
<ProjectHealthGrid />
|
||||
</Row>
|
||||
{!isOss() && (
|
||||
|
@ -190,6 +190,23 @@ export const useChangeRequestApi = () => {
|
||||
|
||||
return makeRequest(req.caller, req.id);
|
||||
};
|
||||
const updateRequestedReviewers = async (
|
||||
project: string,
|
||||
changeRequestId: number,
|
||||
reviewers: string[],
|
||||
) => {
|
||||
trackEvent('change_request', {
|
||||
props: {
|
||||
eventType: 'reviewers updated',
|
||||
},
|
||||
});
|
||||
const path = `api/admin/projects/${project}/change-requests/${changeRequestId}/reviewers`;
|
||||
const req = createRequest(path, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ reviewers }),
|
||||
});
|
||||
return makeRequest(req.caller, req.id);
|
||||
};
|
||||
|
||||
return {
|
||||
addChange,
|
||||
@ -200,6 +217,7 @@ export const useChangeRequestApi = () => {
|
||||
discardDraft,
|
||||
addComment,
|
||||
updateTitle,
|
||||
updateRequestedReviewers,
|
||||
errors,
|
||||
loading,
|
||||
};
|
||||
|
@ -0,0 +1,48 @@
|
||||
import useSWR from 'swr';
|
||||
import { useMemo } from 'react';
|
||||
import { formatApiPath } from 'utils/formatPath';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler.js';
|
||||
|
||||
// TODO: These will likely be created by Orval next time it is run
|
||||
export interface AvailableReviewerSchema {
|
||||
id: string;
|
||||
name?: string;
|
||||
email: string;
|
||||
username?: string;
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
export interface IAvailableReviewersResponse {
|
||||
reviewers: AvailableReviewerSchema[];
|
||||
refetchReviewers: () => void;
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export const useAvailableChangeRequestReviewers = (
|
||||
project: string,
|
||||
environment: string,
|
||||
): IAvailableReviewersResponse => {
|
||||
const { data, error, mutate } = useSWR(
|
||||
formatApiPath(
|
||||
`api/admin/projects/${project}/change-requests/available-reviewers/${environment}`,
|
||||
),
|
||||
fetcher,
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
reviewers: data?.reviewers || [],
|
||||
loading: !error && !data,
|
||||
refetchReviewers: () => mutate(),
|
||||
error,
|
||||
}),
|
||||
[data, error, mutate],
|
||||
);
|
||||
};
|
||||
|
||||
const fetcher = (path: string) => {
|
||||
return fetch(path)
|
||||
.then(handleErrorResponses('Available Change Request Reviewers'))
|
||||
.then((res) => res.json());
|
||||
};
|
@ -91,6 +91,7 @@ export type UiFlags = {
|
||||
healthToTechDebt?: boolean;
|
||||
improvedJsonDiff?: boolean;
|
||||
impactMetrics?: boolean;
|
||||
changeRequestApproverEmails?: boolean;
|
||||
};
|
||||
|
||||
export interface IVersionInfo {
|
||||
|
@ -674,7 +674,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@codemirror/lang-json@npm:6.0.1, @codemirror/lang-json@npm:^6.0.1":
|
||||
"@codemirror/lang-json@npm:6.0.2":
|
||||
version: 6.0.2
|
||||
resolution: "@codemirror/lang-json@npm:6.0.2"
|
||||
dependencies:
|
||||
"@codemirror/language": "npm:^6.0.0"
|
||||
"@lezer/json": "npm:^1.0.0"
|
||||
checksum: 10c0/4a36022226557d0571c143f907638eb2d46c0f7cf96c6d9a86dac397a789efa2b387e3dd3df94bac21e27692892443b24f8129c044c9012df66e68f5080745b0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@codemirror/lang-json@npm:^6.0.1":
|
||||
version: 6.0.1
|
||||
resolution: "@codemirror/lang-json@npm:6.0.1"
|
||||
dependencies:
|
||||
@ -3237,12 +3247,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react-dom@npm:18.3.5":
|
||||
version: 18.3.5
|
||||
resolution: "@types/react-dom@npm:18.3.5"
|
||||
"@types/react-dom@npm:18.3.7":
|
||||
version: 18.3.7
|
||||
resolution: "@types/react-dom@npm:18.3.7"
|
||||
peerDependencies:
|
||||
"@types/react": ^18.0.0
|
||||
checksum: 10c0/b163d35a6b32a79f5782574a7aeb12a31a647e248792bf437e6d596e2676961c394c5e3c6e91d1ce44ae90441dbaf93158efb4f051c0d61e2612f1cb04ce4faa
|
||||
checksum: 10c0/8bd309e2c3d1604a28a736a24f96cbadf6c05d5288cfef8883b74f4054c961b6b3a5e997fd5686e492be903c8f3380dba5ec017eff3906b1256529cd2d39603e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -3303,13 +3313,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react@npm:18.3.18":
|
||||
version: 18.3.18
|
||||
resolution: "@types/react@npm:18.3.18"
|
||||
"@types/react@npm:18.3.23":
|
||||
version: 18.3.23
|
||||
resolution: "@types/react@npm:18.3.23"
|
||||
dependencies:
|
||||
"@types/prop-types": "npm:*"
|
||||
csstype: "npm:^3.0.2"
|
||||
checksum: 10c0/8fb2b00672072135d0858dc9db07873ea107cc238b6228aaa2a9afd1ef7a64a7074078250db38afbeb19064be8ea6af5eac32d404efdd5f45e093cc4829d87f8
|
||||
checksum: 10c0/49331800b76572eb2992a5c44801dbf8c612a5f99c8f4e4200f06c7de6f3a6e9455c661784a6c5469df96fa45622cb4a9d0982c44e6a0d5719be5f2ef1f545ed
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -10038,9 +10048,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tss-react@npm:4.9.15":
|
||||
version: 4.9.15
|
||||
resolution: "tss-react@npm:4.9.15"
|
||||
"tss-react@npm:4.9.18":
|
||||
version: 4.9.18
|
||||
resolution: "tss-react@npm:4.9.18"
|
||||
dependencies:
|
||||
"@emotion/cache": "npm:*"
|
||||
"@emotion/serialize": "npm:*"
|
||||
@ -10048,7 +10058,7 @@ __metadata:
|
||||
peerDependencies:
|
||||
"@emotion/react": ^11.4.1
|
||||
"@emotion/server": ^11.4.0
|
||||
"@mui/material": ^5.0.0 || ^6.0.0
|
||||
"@mui/material": ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
"@types/react": ^16.8.0 || ^17.0.2 || ^18.0.0 || ^19.0.0
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
@ -10056,7 +10066,7 @@ __metadata:
|
||||
optional: true
|
||||
"@mui/material":
|
||||
optional: true
|
||||
checksum: 10c0/f11069b19ec276f34a26f5f4a987c53f7898a16dcb344b3102c977070ce1a378ae14fa8a84330b2554564206d15c781cd7d181df89fc1c5039ce84047f6c9f33
|
||||
checksum: 10c0/81bfdfee892c1eb1bf253b2b456b9d573911d8b2668c757e2f5c56d01def00139bd780efe85fe9115dd7bb15477840e816586d6e999317456ca1e6cf7c1dccd3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -10342,7 +10352,7 @@ __metadata:
|
||||
resolution: "unleash-frontend-local@workspace:."
|
||||
dependencies:
|
||||
"@biomejs/biome": "npm:1.9.4"
|
||||
"@codemirror/lang-json": "npm:6.0.1"
|
||||
"@codemirror/lang-json": "npm:6.0.2"
|
||||
"@emotion/react": "npm:11.11.4"
|
||||
"@emotion/styled": "npm:11.11.5"
|
||||
"@mui/icons-material": "npm:5.15.3"
|
||||
@ -10364,8 +10374,8 @@ __metadata:
|
||||
"@types/lodash.mapvalues": "npm:^4.6.9"
|
||||
"@types/lodash.omit": "npm:4.5.9"
|
||||
"@types/node": "npm:^22.0.0"
|
||||
"@types/react": "npm:18.3.18"
|
||||
"@types/react-dom": "npm:18.3.5"
|
||||
"@types/react": "npm:18.3.23"
|
||||
"@types/react-dom": "npm:18.3.7"
|
||||
"@types/react-router-dom": "npm:5.3.3"
|
||||
"@types/react-table": "npm:7.7.20"
|
||||
"@types/react-test-renderer": "npm:18.3.1"
|
||||
@ -10424,7 +10434,7 @@ __metadata:
|
||||
sass: "npm:1.85.1"
|
||||
semver: "npm:7.7.2"
|
||||
swr: "npm:2.3.3"
|
||||
tss-react: "npm:4.9.15"
|
||||
tss-react: "npm:4.9.18"
|
||||
typescript: "npm:5.8.3"
|
||||
unleash-proxy-client: "npm:^3.7.3"
|
||||
use-query-params: "npm:^2.2.1"
|
||||
@ -10439,12 +10449,12 @@ __metadata:
|
||||
linkType: soft
|
||||
|
||||
"unleash-proxy-client@npm:^3.7.3":
|
||||
version: 3.7.3
|
||||
resolution: "unleash-proxy-client@npm:3.7.3"
|
||||
version: 3.7.6
|
||||
resolution: "unleash-proxy-client@npm:3.7.6"
|
||||
dependencies:
|
||||
tiny-emitter: "npm:^2.1.0"
|
||||
uuid: "npm:^9.0.1"
|
||||
checksum: 10c0/3a061d4e3587325046fea0133fe405fef143dbcfdd6ed20c54200b46a22bf49acdccb6dcc0b250400a9ace2350b0065f856731a5712598d27c1e9266a141f559
|
||||
checksum: 10c0/ad365f6cbf4792506a47168f998e2d8af58db631af07a5a431d414a8d8dff49563e7caf3e0d8dbc77e9a92a6e848db412fb3c5a116fcac8870102fb4ee767594
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -10991,8 +11001,8 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"ws@npm:^8.18.0":
|
||||
version: 8.18.1
|
||||
resolution: "ws@npm:8.18.1"
|
||||
version: 8.18.2
|
||||
resolution: "ws@npm:8.18.2"
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: ">=5.0.2"
|
||||
@ -11001,7 +11011,7 @@ __metadata:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
checksum: 10c0/e498965d6938c63058c4310ffb6967f07d4fa06789d3364829028af380d299fe05762961742971c764973dce3d1f6a2633fe8b2d9410c9b52e534b4b882a99fa
|
||||
checksum: 10c0/4b50f67931b8c6943c893f59c524f0e4905bbd183016cfb0f2b8653aa7f28dad4e456b9d99d285bbb67cca4fedd9ce90dfdfaa82b898a11414ebd66ee99141e4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -127,7 +127,7 @@
|
||||
"stoppable": "^1.1.0",
|
||||
"tldts": "7.0.6",
|
||||
"ts-toolbelt": "^9.6.0",
|
||||
"type-is": "^1.6.18",
|
||||
"type-is": "^2.0.0",
|
||||
"ulidx": "^2.4.1",
|
||||
"unleash-client": "^6.7.0-beta.0",
|
||||
"uuid": "^9.0.0"
|
||||
@ -177,10 +177,10 @@
|
||||
"openapi-enforcer": "1.23.0",
|
||||
"proxyquire": "2.1.3",
|
||||
"source-map-support": "0.5.21",
|
||||
"superagent": "10.2.0",
|
||||
"superagent": "10.2.1",
|
||||
"supertest": "7.0.0",
|
||||
"ts-node": "10.9.2",
|
||||
"tsc-watch": "6.2.1",
|
||||
"tsc-watch": "7.1.1",
|
||||
"typescript": "5.8.3",
|
||||
"vite-node": "^3.1.3",
|
||||
"vitest": "^3.1.3",
|
||||
|
@ -62,16 +62,16 @@ import type { IAddonDefinition } from '../types/model.js';
|
||||
|
||||
const slackAppDefinition: IAddonDefinition = {
|
||||
name: 'slack-app',
|
||||
displayName: 'Slack App',
|
||||
displayName: 'App for Slack',
|
||||
description:
|
||||
'The Unleash Slack App posts messages to the selected channels in your Slack workspace.',
|
||||
howTo: 'Below you can specify which Slack channels receive event notifications. The configuration settings allow you to choose the events and whether you want to filter them by projects and environments.\n\nYou can also select which channels to post to by configuring your feature flags with “slack” tags. For example, if you’d like the bot to post messages to the #general channel, you can configure your feature flag with the “slack:general” tag.\n\nThe Unleash Slack App bot has access to public channels by default. If you want the bot to post messages to private channels, you’ll need to invite it to those channels.',
|
||||
'The Unleash App for Slack posts messages to the selected channels in your Slack workspace.',
|
||||
howTo: 'Below you can specify which Slack channels receive event notifications. The configuration settings allow you to choose the events and whether you want to filter them by projects and environments.\n\nYou can also select which channels to post to by configuring your feature flags with “slack” tags. For example, if you’d like the bot to post messages to the #general channel, you can configure your feature flag with the “slack:general” tag.\n\nThe Unleash App for Slack bot has access to public channels by default. If you want the bot to post messages to private channels, you’ll need to invite it to those channels.',
|
||||
documentationUrl: 'https://docs.getunleash.io/docs/addons/slack-app',
|
||||
installation: {
|
||||
url: 'https://slack-app.getunleash.io/install',
|
||||
title: 'Slack App installation',
|
||||
url: 'https://app-for-slack.getunleash.io/install',
|
||||
title: 'App for Slack installation',
|
||||
helpText:
|
||||
'After installing the Unleash Slack app in your Slack workspace, paste the access token into the appropriate field below in order to configure this integration.',
|
||||
'After installing the Unleash App for Slack in your Slack workspace, paste the access token into the appropriate field below in order to configure this integration.',
|
||||
},
|
||||
parameters: [
|
||||
{
|
||||
|
@ -23,11 +23,11 @@ const slackDefinition: IAddonDefinition = {
|
||||
description: 'Allows Unleash to post updates to Slack.',
|
||||
documentationUrl: 'https://docs.getunleash.io/docs/addons/slack',
|
||||
deprecated:
|
||||
'This integration is deprecated. Please try the new Slack App integration instead.',
|
||||
'This integration is deprecated. Please try the new App for Slack integration instead.',
|
||||
alerts: [
|
||||
{
|
||||
type: 'warning',
|
||||
text: `This integration is deprecated. Please try the new Slack App integration instead.`,
|
||||
text: `This integration is deprecated. Please try the new App for Slack integration instead.`,
|
||||
},
|
||||
],
|
||||
parameters: [
|
||||
|
@ -1,4 +1,4 @@
|
||||
import EventStore from '../features/events/event-store.js';
|
||||
import { EventStore } from '../features/events/event-store.js';
|
||||
// For backward compatibility
|
||||
export * from '../features/events/event-store.js';
|
||||
export default EventStore;
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
ReleasePlanTemplateStore,
|
||||
} from '../types/index.js';
|
||||
|
||||
import EventStore from '../features/events/event-store.js';
|
||||
import { EventStore } from '../features/events/event-store.js';
|
||||
import FeatureToggleStore from '../features/feature-toggle/feature-toggle-store.js';
|
||||
import FeatureTypeStore from './feature-type-store.js';
|
||||
import StrategyStore from './strategy-store.js';
|
||||
|
@ -155,6 +155,8 @@ export const CHANGE_REQUEST_SCHEDULED_APPLICATION_FAILURE =
|
||||
'change-request-scheduled-application-failure' as const;
|
||||
export const CHANGE_REQUEST_CONFIGURATION_UPDATED =
|
||||
'change-request-configuration-updated' as const;
|
||||
export const CHANGE_REQUEST_REQUESTED_APPROVERS_UPDATED =
|
||||
'change-request-requested-approvers-updated' as const;
|
||||
|
||||
export const API_TOKEN_CREATED = 'api-token-created' as const;
|
||||
export const API_TOKEN_UPDATED = 'api-token-updated' as const;
|
||||
@ -372,6 +374,7 @@ export const IEventTypes = [
|
||||
SCIM_USERS_DELETED,
|
||||
SCIM_GROUPS_DELETED,
|
||||
CDN_TOKEN_CREATED,
|
||||
CHANGE_REQUEST_REQUESTED_APPROVERS_UPDATED,
|
||||
] as const;
|
||||
export type IEventType = (typeof IEventTypes)[number];
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ClientFeatureToggleDelta } from './client-feature-toggle-delta.js';
|
||||
import EventStore from '../../events/event-store.js';
|
||||
import { EventStore } from '../../events/event-store.js';
|
||||
import ConfigurationRevisionService from '../../feature-toggle/configuration-revision-service.js';
|
||||
import type { IUnleashConfig } from '../../../types/index.js';
|
||||
import type { Db } from '../../../db/db.js';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import FakeEventStore from '../../../test/fixtures/fake-event-store.js';
|
||||
import FakeFeatureTagStore from '../../../test/fixtures/fake-feature-tag-store.js';
|
||||
import type { Db } from '../../db/db.js';
|
||||
import EventStore from './event-store.js';
|
||||
import { EventStore } from './event-store.js';
|
||||
import FeatureTagStore from '../../db/feature-tag-store.js';
|
||||
import { EventService } from '../../services/index.js';
|
||||
import type {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import EventStore from './event-store.js';
|
||||
import { EventStore } from './event-store.js';
|
||||
import getLogger from '../../../test/fixtures/no-logger.js';
|
||||
import dbInit, {
|
||||
type ITestDb,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import knex from 'knex';
|
||||
import EventStore from './event-store.js';
|
||||
import { EventStore } from './event-store.js';
|
||||
import getLogger from '../../../test/fixtures/no-logger.js';
|
||||
import { subHours, formatRFC3339 } from 'date-fns';
|
||||
import dbInit from '../../../test/e2e/helpers/database-init.js';
|
||||
|
@ -101,7 +101,7 @@ export interface IEventTable {
|
||||
|
||||
const TABLE = 'events';
|
||||
|
||||
class EventStore implements IEventStore {
|
||||
export class EventStore implements IEventStore {
|
||||
private db: Db;
|
||||
|
||||
// only one shared event emitter should exist across all event store instances
|
||||
@ -375,7 +375,7 @@ class EventStore implements IEventStore {
|
||||
): Promise<IEvent[]> {
|
||||
const query = this.buildSearchQuery(queryParams, params.query)
|
||||
.select(options?.withIp ? [...EVENT_COLUMNS, 'ip'] : EVENT_COLUMNS)
|
||||
.orderBy('created_at', 'desc')
|
||||
.orderBy('created_at', params.order || 'desc')
|
||||
.limit(Number(params.limit) ?? 100)
|
||||
.offset(Number(params.offset) ?? 0);
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { FakeFeatureLifecycleStore } from './fake-feature-lifecycle-store.js';
|
||||
import { FeatureLifecycleService } from './feature-lifecycle-service.js';
|
||||
import FakeEnvironmentStore from '../project-environments/fake-environment-store.js';
|
||||
import type { IUnleashConfig } from '../../types/index.js';
|
||||
import EventStore from '../../db/event-store.js';
|
||||
import { EventStore } from '../../db/event-store.js';
|
||||
import type { Db } from '../../db/db.js';
|
||||
import { FeatureLifecycleStore } from './feature-lifecycle-store.js';
|
||||
import EnvironmentStore from '../project-environments/environment-store.js';
|
||||
|
@ -6,13 +6,21 @@ import {
|
||||
NotFoundError,
|
||||
OperationDeniedError,
|
||||
} from '../../error/index.js';
|
||||
import { fakeImpactMetricsResolver } from '../../../test/fixtures/fake-impact-metrics.js';
|
||||
|
||||
test('create, update and delete feature link', async () => {
|
||||
const flagResolver = { impactMetrics: fakeImpactMetricsResolver() };
|
||||
const { featureLinkStore, featureLinkService } =
|
||||
createFakeFeatureLinkService({
|
||||
getLogger,
|
||||
flagResolver,
|
||||
} as unknown as IUnleashConfig);
|
||||
|
||||
flagResolver.impactMetrics.defineCounter(
|
||||
'feature_link_count',
|
||||
'Count of feature links',
|
||||
);
|
||||
|
||||
const link = await featureLinkService.createLink(
|
||||
'default',
|
||||
{
|
||||
@ -29,6 +37,10 @@ test('create, update and delete feature link', async () => {
|
||||
domain: 'example',
|
||||
});
|
||||
|
||||
expect(
|
||||
flagResolver.impactMetrics.counters.get('feature_link_count')!.value,
|
||||
).toBe(1);
|
||||
|
||||
const newLink = await featureLinkService.updateLink(
|
||||
{ projectId: 'default', linkId: link.id },
|
||||
{
|
||||
@ -53,8 +65,10 @@ test('create, update and delete feature link', async () => {
|
||||
});
|
||||
|
||||
test('cannot delete/update non existent link', async () => {
|
||||
const flagResolver = { impactMetrics: fakeImpactMetricsResolver() };
|
||||
const { featureLinkService } = createFakeFeatureLinkService({
|
||||
getLogger,
|
||||
flagResolver,
|
||||
} as unknown as IUnleashConfig);
|
||||
|
||||
await expect(
|
||||
@ -77,8 +91,10 @@ test('cannot delete/update non existent link', async () => {
|
||||
});
|
||||
|
||||
test('cannot create/update invalid link', async () => {
|
||||
const flagResolver = { impactMetrics: fakeImpactMetricsResolver() };
|
||||
const { featureLinkService } = createFakeFeatureLinkService({
|
||||
getLogger,
|
||||
flagResolver,
|
||||
} as unknown as IUnleashConfig);
|
||||
|
||||
await expect(
|
||||
@ -107,8 +123,10 @@ test('cannot create/update invalid link', async () => {
|
||||
});
|
||||
|
||||
test('cannot exceed allowed link count', async () => {
|
||||
const flagResolver = { impactMetrics: fakeImpactMetricsResolver() };
|
||||
const { featureLinkService } = createFakeFeatureLinkService({
|
||||
getLogger,
|
||||
flagResolver,
|
||||
} as unknown as IUnleashConfig);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
FeatureLinkRemovedEvent,
|
||||
FeatureLinkUpdatedEvent,
|
||||
type IAuditUser,
|
||||
type IFlagResolver,
|
||||
type IUnleashConfig,
|
||||
} from '../../types/index.js';
|
||||
import type {
|
||||
@ -18,6 +19,7 @@ import {
|
||||
} from '../../error/index.js';
|
||||
import normalizeUrl from 'normalize-url';
|
||||
import { parse } from 'tldts';
|
||||
import { FEAUTRE_LINK_COUNT } from '../metrics/impact/define-impact-metrics.js';
|
||||
|
||||
interface IFeatureLinkStoreObj {
|
||||
featureLinkStore: IFeatureLinkStore;
|
||||
@ -27,15 +29,20 @@ export default class FeatureLinkService {
|
||||
private logger: Logger;
|
||||
private featureLinkStore: IFeatureLinkStore;
|
||||
private eventService: EventService;
|
||||
private flagResolver: IFlagResolver;
|
||||
|
||||
constructor(
|
||||
stores: IFeatureLinkStoreObj,
|
||||
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
||||
{
|
||||
getLogger,
|
||||
flagResolver,
|
||||
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
|
||||
eventService: EventService,
|
||||
) {
|
||||
this.logger = getLogger('feature-links/feature-link-service.ts');
|
||||
this.featureLinkStore = stores.featureLinkStore;
|
||||
this.eventService = eventService;
|
||||
this.flagResolver = flagResolver;
|
||||
}
|
||||
|
||||
async getAll(): Promise<IFeatureLink[]> {
|
||||
@ -72,6 +79,8 @@ export default class FeatureLinkService {
|
||||
domain: domainWithoutSuffix,
|
||||
});
|
||||
|
||||
this.flagResolver.impactMetrics?.incrementCounter(FEAUTRE_LINK_COUNT);
|
||||
|
||||
await this.eventService.storeEvent(
|
||||
new FeatureLinkAddedEvent({
|
||||
featureName: newLink.featureName,
|
||||
|
@ -19,17 +19,7 @@ let featureLinkReadModel: IFeatureLinksReadModel;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('feature_link', getLogger);
|
||||
app = await setupAppWithAuth(
|
||||
db.stores,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
featureLinks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
db.rawDatabase,
|
||||
);
|
||||
app = await setupAppWithAuth(db.stores, {}, db.rawDatabase);
|
||||
eventStore = db.stores.eventStore;
|
||||
featureLinkStore = db.stores.featureLinkStore;
|
||||
featureLinkReadModel = new FeatureLinksReadModel(
|
||||
|
@ -20,7 +20,7 @@ import SegmentStore from '../segment/segment-store.js';
|
||||
import RoleStore from '../../db/role-store.js';
|
||||
import SettingStore from '../../db/setting-store.js';
|
||||
import ClientInstanceStore from '../../db/client-instance-store.js';
|
||||
import EventStore from '../events/event-store.js';
|
||||
import { EventStore } from '../events/event-store.js';
|
||||
import { ApiTokenStore } from '../../db/api-token-store.js';
|
||||
import { ClientMetricsStoreV2 } from '../metrics/client-metrics/client-metrics-store-v2.js';
|
||||
import VersionService from '../../services/version-service.js';
|
||||
|
25
src/lib/features/metrics/impact/define-impact-metrics.ts
Normal file
25
src/lib/features/metrics/impact/define-impact-metrics.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import type { IFlagResolver } from '../../../types/index.js';
|
||||
|
||||
export const FEAUTRE_LINK_COUNT = 'feature_link_count';
|
||||
export const CLIENT_ERROR_COUNT = 'client_error_count';
|
||||
export const SERVER_ERROR_COUNT = 'server_error_count';
|
||||
export const HEAP_MEMORY_TOTAL = 'heap_memory_total';
|
||||
|
||||
export const defineImpactMetrics = (flagResolver: IFlagResolver) => {
|
||||
flagResolver.impactMetrics?.defineCounter(
|
||||
FEAUTRE_LINK_COUNT,
|
||||
'Count of feature links',
|
||||
);
|
||||
flagResolver.impactMetrics?.defineCounter(
|
||||
CLIENT_ERROR_COUNT,
|
||||
'Count of 4xx errors',
|
||||
);
|
||||
flagResolver.impactMetrics?.defineCounter(
|
||||
SERVER_ERROR_COUNT,
|
||||
'Count of 5xx errors',
|
||||
);
|
||||
flagResolver.impactMetrics?.defineGauge(
|
||||
HEAP_MEMORY_TOTAL,
|
||||
'Total heap memory used by the application process',
|
||||
);
|
||||
};
|
@ -7,7 +7,7 @@ import { ProjectOwnersReadModel } from '../project/project-owners-read-model.js'
|
||||
import { FakeProjectOwnersReadModel } from '../project/fake-project-owners-read-model.js';
|
||||
import { ProjectReadModel } from '../project/project-read-model.js';
|
||||
import { FakeProjectReadModel } from '../project/fake-project-read-model.js';
|
||||
import EventStore from '../../db/event-store.js';
|
||||
import { EventStore } from '../../db/event-store.js';
|
||||
import { FeatureEventFormatterMd } from '../../addons/feature-event-formatter-md.js';
|
||||
import FakeEventStore from '../../../test/fixtures/fake-event-store.js';
|
||||
import { FakePrivateProjectChecker } from '../private-project/fakePrivateProjectChecker.js';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { Db, IUnleashConfig } from '../../types/index.js';
|
||||
import { ProjectStatusService } from './project-status-service.js';
|
||||
import EventStore from '../events/event-store.js';
|
||||
import { EventStore } from '../events/event-store.js';
|
||||
import FakeEventStore from '../../../test/fixtures/fake-event-store.js';
|
||||
import ProjectStore from '../project/project-store.js';
|
||||
import FakeProjectStore from '../../../test/fixtures/fake-project-store.js';
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { Db, IUnleashConfig } from '../../types/index.js';
|
||||
import EventStore from '../events/event-store.js';
|
||||
import { EventStore } from '../events/event-store.js';
|
||||
import GroupStore from '../../db/group-store.js';
|
||||
import { AccountStore } from '../../db/account-store.js';
|
||||
import EnvironmentStore from '../project-environments/environment-store.js';
|
||||
|
@ -41,6 +41,7 @@ import {
|
||||
import type { SchedulerService } from './services/index.js';
|
||||
import type { IClientMetricsEnv } from './features/metrics/client-metrics/client-metrics-store-v2-type.js';
|
||||
import { DbMetricsMonitor } from './metrics-gauge.js';
|
||||
import { HEAP_MEMORY_TOTAL } from './features/metrics/impact/define-impact-metrics.js';
|
||||
|
||||
export function registerPrometheusPostgresMetrics(
|
||||
db: Knex,
|
||||
@ -1137,6 +1138,10 @@ export function registerPrometheusMetrics(
|
||||
collectAggDbMetrics: dbMetrics.refreshMetrics,
|
||||
collectStaticCounters: async () => {
|
||||
try {
|
||||
config.flagResolver.impactMetrics?.updateGauge(
|
||||
HEAP_MEMORY_TOTAL,
|
||||
process.memoryUsage().heapUsed,
|
||||
);
|
||||
featureTogglesArchivedTotal.reset();
|
||||
featureTogglesArchivedTotal.set(
|
||||
await instanceStatsService.getArchivedToggleCount(),
|
||||
|
@ -1,14 +1,25 @@
|
||||
import url from 'url';
|
||||
import type { RequestHandler } from 'express';
|
||||
import type { IUnleashConfig } from '../types/option.js';
|
||||
import {
|
||||
CLIENT_ERROR_COUNT,
|
||||
SERVER_ERROR_COUNT,
|
||||
} from '../features/metrics/impact/define-impact-metrics.js';
|
||||
|
||||
const requestLogger: (config: IUnleashConfig) => RequestHandler = (config) => {
|
||||
const logger = config.getLogger('HTTP');
|
||||
const enable = config.server.enableRequestLogger;
|
||||
const impactMetrics = config.flagResolver.impactMetrics;
|
||||
return (req, res, next) => {
|
||||
if (enable) {
|
||||
res.on('finish', () => {
|
||||
const { pathname } = url.parse(req.originalUrl);
|
||||
if (res.statusCode >= 400 && res.statusCode < 500) {
|
||||
impactMetrics?.incrementCounter(CLIENT_ERROR_COUNT);
|
||||
}
|
||||
if (res.statusCode >= 500) {
|
||||
impactMetrics?.incrementCounter(SERVER_ERROR_COUNT);
|
||||
}
|
||||
logger.info(`${res.statusCode} ${req.method} ${pathname}`);
|
||||
});
|
||||
}
|
||||
|
@ -136,20 +136,20 @@ export const addonTypeSchema = {
|
||||
type: 'string',
|
||||
description:
|
||||
'A URL to where the addon configuration should redirect to install addons of this type.',
|
||||
example: 'https://slack-app.getunleash.io/install',
|
||||
example: 'https://app-for-slack.getunleash.io/install',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The title of the installation configuration. This will be displayed to the user when installing addons of this type.',
|
||||
example: 'Slack App installation',
|
||||
example: 'App for Slack installation',
|
||||
},
|
||||
helpText: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The help text of the installation configuration. This will be displayed to the user when installing addons of this type.',
|
||||
example:
|
||||
'Clicking the Install button will send you to Slack to initiate the installation procedure for the Unleash Slack app for your workspace',
|
||||
'Clicking the Install button will send you to Slack to initiate the installation procedure for the Unleash App for Slack for your workspace',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -174,7 +174,7 @@ export const addonTypeSchema = {
|
||||
description:
|
||||
'The text of the alert. This is what will be displayed to the user.',
|
||||
example:
|
||||
"Please ensure you have the Unleash Slack App installed in your Slack workspace if you haven't installed it already. If you want the Unleash Slack App bot to post messages to private channels, you'll need to invite it to those channels.",
|
||||
"Please ensure you have the Unleash App for Slack installed in your Slack workspace if you haven't installed it already. If you want the Unleash App for Slack bot to post messages to private channels, you'll need to invite it to those channels.",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -126,7 +126,7 @@ import metricsHelper from './util/metrics-helper.js';
|
||||
import type { ReleasePlanMilestoneWriteModel } from './features/release-plans/release-plan-milestone-store.js';
|
||||
import type { ReleasePlanMilestoneStrategyWriteModel } from './features/release-plans/release-plan-milestone-strategy-store.js';
|
||||
import type { IChangeRequestAccessReadModel } from './features/change-request-access-service/change-request-access-read-model.js';
|
||||
import EventStore from './db/event-store.js';
|
||||
import { EventStore } from './db/event-store.js';
|
||||
import RoleStore from './db/role-store.js';
|
||||
import { AccessStore } from './db/access-store.js';
|
||||
import {
|
||||
@ -183,6 +183,7 @@ import { testDbPrefix } from '../test/e2e/helpers/database-init.js';
|
||||
import type { RequestHandler } from 'express';
|
||||
import { UPDATE_REVISION } from './features/feature-toggle/configuration-revision-service.js';
|
||||
import type { IFeatureUsageInfo } from './services/version-service.js';
|
||||
import { defineImpactMetrics } from './features/metrics/impact/define-impact-metrics.js';
|
||||
|
||||
export async function initialServiceSetup(
|
||||
{ authentication }: Pick<IUnleashConfig, 'authentication'>,
|
||||
@ -232,6 +233,7 @@ export async function createApp(
|
||||
scheduleServices(services, config);
|
||||
}
|
||||
|
||||
defineImpactMetrics(config.flagResolver);
|
||||
const metricsMonitor = fm.createMetricsMonitor();
|
||||
const unleashSession = fm.createSessionDb(config, db);
|
||||
|
||||
|
@ -15,6 +15,7 @@ export interface IEventSearchParams {
|
||||
createdBy?: string;
|
||||
type?: string;
|
||||
environment?: string;
|
||||
order?: 'asc' | 'desc'; // desc by default
|
||||
offset: number;
|
||||
limit: number;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import type {
|
||||
IFlags,
|
||||
IFlagResolver,
|
||||
IFlagKey,
|
||||
IImpactMetricsResolver,
|
||||
} from '../types/experimental.js';
|
||||
import { getDefaultVariant } from 'unleash-client/lib/variant.js';
|
||||
|
||||
@ -65,6 +66,10 @@ export default class FlagResolver implements IFlagResolver {
|
||||
getStaticContext(): IFlagContext {
|
||||
return this.externalResolver.getStaticContext();
|
||||
}
|
||||
|
||||
get impactMetrics(): IImpactMetricsResolver | undefined {
|
||||
return this.externalResolver?.impactMetrics;
|
||||
}
|
||||
}
|
||||
|
||||
export const getVariantValue = <T = string>(
|
||||
|
34
src/test/fixtures/fake-impact-metrics.ts
vendored
Normal file
34
src/test/fixtures/fake-impact-metrics.ts
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
export const fakeImpactMetricsResolver = () => ({
|
||||
counters: new Map<string, { value: number; help: string }>(),
|
||||
gauges: new Map<string, { value: number; help: string }>(),
|
||||
|
||||
defineCounter(name: string, help: string) {
|
||||
this.counters.set(name, { value: 0, help });
|
||||
},
|
||||
|
||||
defineGauge(name: string, help: string) {
|
||||
this.gauges.set(name, { value: 0, help });
|
||||
},
|
||||
|
||||
incrementCounter(name: string, value: number = 1) {
|
||||
const counter = this.counters.get(name);
|
||||
|
||||
if (!counter) {
|
||||
return;
|
||||
}
|
||||
|
||||
counter.value += value;
|
||||
this.counters.set(name, counter);
|
||||
},
|
||||
|
||||
updateGauge(name: string, value: number) {
|
||||
const gauge = this.gauges.get(name);
|
||||
|
||||
if (!gauge) {
|
||||
return;
|
||||
}
|
||||
|
||||
gauge.value = value;
|
||||
this.gauges.set(name, gauge);
|
||||
},
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Insights
|
||||
title: Analytics
|
||||
---
|
||||
|
||||
:::note Availability
|
||||
@ -9,16 +9,16 @@ title: Insights
|
||||
:::
|
||||
|
||||
|
||||
Insights is a feature designed to help you better understand and gain insights into what is happening in your Unleash instance. You can view insights across all projects or by selecting single or multiple projects using the filter.
|
||||
Analytics is a feature designed to help you better understand and gain insights into what is happening in your Unleash instance. You can view analytics across all projects or by selecting single or multiple projects using the filter.
|
||||
|
||||
In total, there are 6 different charts available that show information over time:
|
||||
|
||||
- Total users (Pro, Enterprise)
|
||||
- Flags (Pro, Enterprise)
|
||||
- Health (Enterprise)
|
||||
- Median time to production (Enterprise)
|
||||
- Flag evaluation metrics (Enterprise)
|
||||
- Updates per environment type (Enterprise)
|
||||
- Total users
|
||||
- Flags
|
||||
- Technical debt
|
||||
- Median time to production
|
||||
- Flag evaluation metrics
|
||||
- Updates per environment type
|
||||
|
||||
|
||||
### Total users
|
||||
@ -33,11 +33,11 @@ The flags chart displays the total number of active (not archived) feature flags
|
||||
|
||||

|
||||
|
||||
### Health
|
||||
### Technical debt
|
||||
|
||||
The health chart represents the percentage of flags in the selected projects that are not stale or potentially stale. This chart helps you monitor the overall health of your feature flags, ensuring that they are actively maintained and relevant. The chart also shows how the overall health changes over time, allowing you to identify potential issues early and take corrective actions.
|
||||
The technical debt rating shows the percentage of healthy flags in a project compared to stale or potentially stale flags. This helps you ensure all flags are actively maintained and relevant. You can also view these changes over time to identify potential issues early and take corrective actions.
|
||||
|
||||

|
||||

|
||||
|
||||
### Median time to production
|
||||
|
||||
|
@ -22,7 +22,7 @@ Unleash currently supports the following integrations out of the box:
|
||||
- [Jira Cloud](./integrations/jira-cloud-plugin-usage) - Allows you to create, view and manage Unleash feature flags directly from a Jira Cloud issue
|
||||
- [Jira Server](./integrations/jira-server-plugin-usage) - Allows you to create and link Unleash feature flags directly from a Jira Server issue
|
||||
- [Microsoft Teams](./integrations/teams) - Allows Unleash to post updates to Microsoft Teams.
|
||||
- [Slack App](./integrations/slack-app) - The Unleash Slack App posts messages to the selected channels in your Slack workspace.
|
||||
- [App for Slack](./integrations/slack-app) - The Unleash App for Slack posts messages to the selected channels in your Slack workspace.
|
||||
- [Webhook](./integrations/webhook) - A generic way to post messages from Unleash to third party services.
|
||||
|
||||
:::tip Missing an integration? Request it!
|
||||
@ -35,7 +35,7 @@ If you're looking for an integration that Unleash doesn't have at the moment, yo
|
||||
|
||||
These integrations are deprecated and will be removed in a future release:
|
||||
|
||||
- [Slack](./integrations/slack) - Allows Unleash to post updates to Slack. Please try the new [Slack App](./integrations/slack-app) integration instead.
|
||||
- [Slack](./integrations/slack) - Allows Unleash to post updates to Slack. Please try the new [App for Slack](./integrations/slack-app) integration instead.
|
||||
|
||||
## Community integrations
|
||||
|
||||
@ -62,7 +62,7 @@ Integration events are logged for all outgoing integrations configured in Unleas
|
||||
- [Microsoft Teams](./integrations/teams)
|
||||
- New Relic
|
||||
- [Slack (deprecated)](./integrations/slack)
|
||||
- [Slack App](./integrations/slack-app)
|
||||
- [App for Slack](./integrations/slack-app)
|
||||
- [Webhook](./integrations/webhook)
|
||||
|
||||
### Viewing integration events
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Slack App
|
||||
title: App for Slack
|
||||
---
|
||||
|
||||
:::note Availability
|
||||
@ -8,19 +8,19 @@ title: Slack App
|
||||
|
||||
:::
|
||||
|
||||
The Slack App integration posts messages to a specified set of channels in your Slack workspace. The channels can be public or private, and can be specified on a per-flag basis by using [Slack tags](#tags).
|
||||
The App for Slack integration posts messages to a specified set of channels in your Slack workspace. The channels can be public or private, and can be specified on a per-flag basis by using [Slack tags](#tags).
|
||||
|
||||
## Installation {#installation}
|
||||
|
||||
To install the Slack App integration, follow these steps:
|
||||
To install the App for Slack integration, follow these steps:
|
||||
|
||||
1. Navigate to the *integrations* page in the Unleash admin UI (available at the URL `/integrations`) and select "configure" on the Slack App integration.
|
||||
1. Navigate to the *integrations* page in the Unleash admin UI (available at the URL `/integrations`) and select "configure" on the App for Slack integration.
|
||||
2. On the integration configuration form, use the "install & connect" button.
|
||||
3. A new tab will open, asking you to select the Slack workspace where you'd like to install the app.
|
||||
4. After successful installation of the Unleash Slack App in your chosen Slack workspace, you'll be automatically redirected to a page displaying a newly generated access token.
|
||||
4. After successful installation of the Unleash App for Slack in your chosen Slack workspace, you'll be automatically redirected to a page displaying a newly generated access token.
|
||||
5. Copy this access token and paste it into the `Access token` field within the integration settings.
|
||||
|
||||
By default, the Unleash Slack App is granted access to public channels. If you want the app to post messages to private channels, you'll need to manually invite it to each of those channels.
|
||||
By default, the Unleash App for Slack is granted access to public channels. If you want the app to post messages to private channels, you'll need to manually invite it to each of those channels.
|
||||
|
||||
## Configuration {#configuration}
|
||||
|
||||
@ -84,14 +84,14 @@ You can choose to trigger updates for the following events:
|
||||
|
||||
#### Parameters {#parameters}
|
||||
|
||||
The Unleash Slack App integration takes the following parameters.
|
||||
The Unleash App for Slack integration takes the following parameters.
|
||||
|
||||
- **Access token** - This is the only required property. After successful installation of the Unleash Slack App in your chosen Slack workspace, you'll be automatically redirected to a page displaying a newly generated access token. You should copy this access token and paste it into this field.
|
||||
- **Access token** - This is the only required property. After successful installation of the Unleash App for Slack in your chosen Slack workspace, you'll be automatically redirected to a page displaying a newly generated access token. You should copy this access token and paste it into this field.
|
||||
- **Channels** - A comma-separated list of channels to post the configured events to. These channels are always notified, regardless of the event type or the presence of a Slack tag.
|
||||
|
||||
## Tags {#tags}
|
||||
|
||||
Besides the configured channels, you can choose to notify other channels by tagging your feature flags with Slack-specific tags. For instance, if you want the Unleash Slack App to send notifications to the `#general` channel, add a Slack-type tag with the value "general" (or "#general"; both will work) to your flag. This will ensure that any configured events related to that feature flag will notify the tagged channel in addition to any channels configured on the integration-level.
|
||||
Besides the configured channels, you can choose to notify other channels by tagging your feature flags with Slack-specific tags. For instance, if you want the Unleash App for Slack to send notifications to the `#general` channel, add a Slack-type tag with the value "general" (or "#general"; both will work) to your flag. This will ensure that any configured events related to that feature flag will notify the tagged channel in addition to any channels configured on the integration-level.
|
||||
|
||||
To exclusively use tags for determining notification channels, you can leave the "channels" field blank in the integration configuration. Since you can have multiple configurations for the integration, you're free to mix and match settings to meet your precise needs. Before posting a message, all channels for that event, both configured and tagged, are combined and duplicates are removed.
|
||||
|
||||
|
@ -5,7 +5,7 @@ title: Slack (deprecated)
|
||||
|
||||
:::caution Deprecation notice
|
||||
|
||||
This Slack integration is deprecated and will be removed in a future release. We recommend using the new [Slack App](./slack-app) integration instead.
|
||||
This Slack integration is deprecated and will be removed in a future release. We recommend using the new [App for Slack](./slack-app) integration instead.
|
||||
|
||||
:::
|
||||
|
||||
|
@ -117,10 +117,10 @@ To change the default strategy for an environment in a project:
|
||||
|
||||
Unleash supports [predefined](./rbac#predefined-roles) and [custom roles](./rbac#custom-project-roles) at the project level. The two predefined project roles are Owner and Member. By default, the person creating the project becomes the Owner. If a project does not have an Owner, it is shown as owned by _System_.
|
||||
|
||||
## View project insights
|
||||
## View project status
|
||||
|
||||
Project insights is a great way to see how your project performed in the last 30 days compared to the previous 30 days. You can explore key metrics such as the total number of changes, the average time to production, the number of features created and archived, and project health.
|
||||
The [Project status](./technical-debt#project-status) dashboard provides an overview of your project's technical debt, information on project resources like API keys, recent activity within the project, and feature flag lifecycle information.
|
||||
|
||||
To view your project insights, go to the **Insights** within a project.
|
||||
For additional, in-depth analysis, go to **Analytics** and filter by your project.
|
||||
|
||||
|
||||
|
@ -32,8 +32,6 @@ While a flag's state does not affect its behavior in applications, using states
|
||||
|
||||
## Project status
|
||||
|
||||
Each project has a **Project status** dashboard, where you can view its health status and the total number of unhealthy flags. All active flags are considered healthy, while stale and potentially stale flags are considered unhealthy. To keep your project in a healthy state, [archive stale feature flags](/reference/feature-toggles#archive-a-feature-flag) and remove code from your codebase.
|
||||
Each project has a **Project status** dashboard, where you can view its technical debt rating—the percentage of healthy flags compared to stale or potentially stale flags. To keep your project's technical debt low, [archive stale feature flags](/reference/feature-toggles#archive-a-feature-flag) and remove them from your codebase. To view your project's technical debt rating over time, go to [Analytics](/reference/insights).
|
||||
|
||||

|
||||
|
||||
Your overall project health rating is the percentage of healthy flags in your project. To view your project health over time, go to [Insights](/reference/insights).
|
Binary file not shown.
Before Width: | Height: | Size: 258 KiB |
BIN
website/static/img/insights-technical-debt.png
Normal file
BIN
website/static/img/insights-technical-debt.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 71 KiB |
Binary file not shown.
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 171 KiB |
@ -4171,21 +4171,21 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@types/react@npm:*":
|
||||
version: 19.0.2
|
||||
resolution: "@types/react@npm:19.0.2"
|
||||
version: 19.1.8
|
||||
resolution: "@types/react@npm:19.1.8"
|
||||
dependencies:
|
||||
csstype: "npm:^3.0.2"
|
||||
checksum: 10c0/8992f39701fcf1bf893ef8f94a56196445667baf08fe4f6050a14e229a17aad3265ad3efc01595ff3b4d5d5c69da885f9aa4ff80f164a613018734efcff1eb8f
|
||||
checksum: 10c0/4908772be6dc941df276931efeb0e781777fa76e4d5d12ff9f75eb2dcc2db3065e0100efde16fde562c5bafa310cc8f50c1ee40a22640459e066e72cd342143e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react@npm:^18.3.12":
|
||||
version: 18.3.18
|
||||
resolution: "@types/react@npm:18.3.18"
|
||||
version: 18.3.23
|
||||
resolution: "@types/react@npm:18.3.23"
|
||||
dependencies:
|
||||
"@types/prop-types": "npm:*"
|
||||
csstype: "npm:^3.0.2"
|
||||
checksum: 10c0/8fb2b00672072135d0858dc9db07873ea107cc238b6228aaa2a9afd1ef7a64a7074078250db38afbeb19064be8ea6af5eac32d404efdd5f45e093cc4829d87f8
|
||||
checksum: 10c0/49331800b76572eb2992a5c44801dbf8c612a5f99c8f4e4200f06c7de6f3a6e9455c661784a6c5469df96fa45622cb4a9d0982c44e6a0d5719be5f2ef1f545ed
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -6176,7 +6176,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"create-hash@npm:^1.1.0, create-hash@npm:^1.1.2, create-hash@npm:^1.2.0":
|
||||
"create-hash@npm:^1.1.0, create-hash@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "create-hash@npm:1.2.0"
|
||||
dependencies:
|
||||
@ -6189,7 +6189,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"create-hmac@npm:^1.1.4, create-hmac@npm:^1.1.7":
|
||||
"create-hash@npm:~1.1.3":
|
||||
version: 1.1.3
|
||||
resolution: "create-hash@npm:1.1.3"
|
||||
dependencies:
|
||||
cipher-base: "npm:^1.0.1"
|
||||
inherits: "npm:^2.0.1"
|
||||
ripemd160: "npm:^2.0.0"
|
||||
sha.js: "npm:^2.4.0"
|
||||
checksum: 10c0/dbcf4a1b13c8dd5f2a69f5f30bd2701f919ed7d3fbf5aa530cf00b17a950c2b77f63bfe6a2981735a646ae2620d96c8f4584bf70aeeabf050a31de4e46219d08
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"create-hmac@npm:^1.1.7":
|
||||
version: 1.1.7
|
||||
resolution: "create-hmac@npm:1.1.7"
|
||||
dependencies:
|
||||
@ -8697,6 +8709,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hash-base@npm:^2.0.0":
|
||||
version: 2.0.2
|
||||
resolution: "hash-base@npm:2.0.2"
|
||||
dependencies:
|
||||
inherits: "npm:^2.0.1"
|
||||
checksum: 10c0/283f6060277b52e627a734c4d19d4315ba82326cab5a2f4f2f00b924d747dc7cc902a8cedb1904c7a3501075fcbb24c08de1152bae296698fdc5ad75b33986af
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hash-base@npm:^3.0.0":
|
||||
version: 3.1.0
|
||||
resolution: "hash-base@npm:3.1.0"
|
||||
@ -9735,7 +9756,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-typed-array@npm:^1.1.3":
|
||||
"is-typed-array@npm:^1.1.14, is-typed-array@npm:^1.1.3":
|
||||
version: 1.1.15
|
||||
resolution: "is-typed-array@npm:1.1.15"
|
||||
dependencies:
|
||||
@ -9774,6 +9795,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"isarray@npm:^2.0.5":
|
||||
version: 2.0.5
|
||||
resolution: "isarray@npm:2.0.5"
|
||||
checksum: 10c0/4199f14a7a13da2177c66c31080008b7124331956f47bca57dd0b6ea9f11687aa25e565a2c7a2b519bc86988d10398e3049a1f5df13c9f6b7664154690ae79fd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"isarray@npm:~1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "isarray@npm:1.0.0"
|
||||
@ -12859,15 +12887,16 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"pbkdf2@npm:^3.1.2":
|
||||
version: 3.1.2
|
||||
resolution: "pbkdf2@npm:3.1.2"
|
||||
version: 3.1.3
|
||||
resolution: "pbkdf2@npm:3.1.3"
|
||||
dependencies:
|
||||
create-hash: "npm:^1.1.2"
|
||||
create-hmac: "npm:^1.1.4"
|
||||
ripemd160: "npm:^2.0.1"
|
||||
safe-buffer: "npm:^5.0.1"
|
||||
sha.js: "npm:^2.4.8"
|
||||
checksum: 10c0/5a30374e87d33fa080a92734d778cf172542cc7e41b96198c4c88763997b62d7850de3fbda5c3111ddf79805ee7c1da7046881c90ac4920b5e324204518b05fd
|
||||
create-hash: "npm:~1.1.3"
|
||||
create-hmac: "npm:^1.1.7"
|
||||
ripemd160: "npm:=2.0.1"
|
||||
safe-buffer: "npm:^5.2.1"
|
||||
sha.js: "npm:^2.4.11"
|
||||
to-buffer: "npm:^1.2.0"
|
||||
checksum: 10c0/12779463dfb847701f186e0b7e5fd538a1420409a485dcf5100689c2b3ec3cb113204e82a68668faf3b6dd76ec19260b865313c9d3a9c252807163bdc24652ae
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -14941,6 +14970,16 @@ plugin-image-zoom@flexanalytics/plugin-image-zoom:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ripemd160@npm:=2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "ripemd160@npm:2.0.1"
|
||||
dependencies:
|
||||
hash-base: "npm:^2.0.0"
|
||||
inherits: "npm:^2.0.1"
|
||||
checksum: 10c0/d4cbb4713c1268bb35e44815b12e3744a952a72b72e6a72110c8f3932227ddf68841110285fe2ed1c04805e2621d85f905deb5f55f9d91fa1bfc0f8081a244e6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ripemd160@npm:^2.0.0, ripemd160@npm:^2.0.1":
|
||||
version: 2.0.2
|
||||
resolution: "ripemd160@npm:2.0.2"
|
||||
@ -15324,7 +15363,7 @@ plugin-image-zoom@flexanalytics/plugin-image-zoom:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sha.js@npm:^2.4.0, sha.js@npm:^2.4.8":
|
||||
"sha.js@npm:^2.4.0, sha.js@npm:^2.4.11, sha.js@npm:^2.4.8":
|
||||
version: 2.4.11
|
||||
resolution: "sha.js@npm:2.4.11"
|
||||
dependencies:
|
||||
@ -16122,6 +16161,17 @@ plugin-image-zoom@flexanalytics/plugin-image-zoom:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"to-buffer@npm:^1.2.0":
|
||||
version: 1.2.1
|
||||
resolution: "to-buffer@npm:1.2.1"
|
||||
dependencies:
|
||||
isarray: "npm:^2.0.5"
|
||||
safe-buffer: "npm:^5.2.1"
|
||||
typed-array-buffer: "npm:^1.0.3"
|
||||
checksum: 10c0/bbf07a2a7d6ff9e3ffe503c689176c7149cf3ec25887ce7c4aa5c4841a8845cc71121cd7b4a4769957f823b3f31dbf6b1be6e0a5955798ad864bf2245ee8b5e4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"to-regex-range@npm:^5.0.1":
|
||||
version: 5.0.1
|
||||
resolution: "to-regex-range@npm:5.0.1"
|
||||
@ -16232,6 +16282,17 @@ plugin-image-zoom@flexanalytics/plugin-image-zoom:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typed-array-buffer@npm:^1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "typed-array-buffer@npm:1.0.3"
|
||||
dependencies:
|
||||
call-bound: "npm:^1.0.3"
|
||||
es-errors: "npm:^1.3.0"
|
||||
is-typed-array: "npm:^1.1.14"
|
||||
checksum: 10c0/1105071756eb248774bc71646bfe45b682efcad93b55532c6ffa4518969fb6241354e4aa62af679ae83899ec296d69ef88f1f3763657cdb3a4d29321f7b83079
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typedarray-to-buffer@npm:^3.1.5":
|
||||
version: 3.1.5
|
||||
resolution: "typedarray-to-buffer@npm:3.1.5"
|
||||
|
92
yarn.lock
92
yarn.lock
@ -2560,7 +2560,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"content-type@npm:~1.0.4, content-type@npm:~1.0.5":
|
||||
"content-type@npm:^1.0.5, content-type@npm:~1.0.4, content-type@npm:~1.0.5":
|
||||
version: 1.0.5
|
||||
resolution: "content-type@npm:1.0.5"
|
||||
checksum: 10c0/b76ebed15c000aee4678c3707e0860cb6abd4e680a598c0a26e17f0bfae723ec9cc2802f0ff1bc6e4d80603719010431d2231018373d4dde10f9ccff9dadf5af
|
||||
@ -2727,6 +2727,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cross-spawn@npm:^7.0.6":
|
||||
version: 7.0.6
|
||||
resolution: "cross-spawn@npm:7.0.6"
|
||||
dependencies:
|
||||
path-key: "npm:^3.1.0"
|
||||
shebang-command: "npm:^2.0.0"
|
||||
which: "npm:^2.0.1"
|
||||
checksum: 10c0/053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d@npm:1, d@npm:^1.0.1, d@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "d@npm:1.0.2"
|
||||
@ -3590,14 +3601,14 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"fetch-mock@npm:^12.0.0":
|
||||
version: 12.5.2
|
||||
resolution: "fetch-mock@npm:12.5.2"
|
||||
version: 12.5.3
|
||||
resolution: "fetch-mock@npm:12.5.3"
|
||||
dependencies:
|
||||
"@types/glob-to-regexp": "npm:^0.4.4"
|
||||
dequal: "npm:^2.0.3"
|
||||
glob-to-regexp: "npm:^0.4.1"
|
||||
regexparam: "npm:^3.0.0"
|
||||
checksum: 10c0/015ca2c7eba304beb0df06e15399c50ed0f3e167a2b1ea218a8505c468ddeae0cfab1e5d4efcdee2a62aa17c9c8a1d8ccb5e1683b00a55e7a93bb5b31b183688
|
||||
checksum: 10c0/820c5b66d855c4b48698d57f7b46861515eb05a0511f98f648c05573e3b891e1a693c8f5e2db603cac32167e28673799251268a78bcd178d06e6457151f92616
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -3748,7 +3759,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"formidable@npm:^3.5.1, formidable@npm:^3.5.2":
|
||||
"formidable@npm:^3.5.1, formidable@npm:^3.5.4":
|
||||
version: 3.5.4
|
||||
resolution: "formidable@npm:3.5.4"
|
||||
dependencies:
|
||||
@ -5109,6 +5120,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"media-typer@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "media-typer@npm:1.1.0"
|
||||
checksum: 10c0/7b4baa40b25964bb90e2121ee489ec38642127e48d0cc2b6baa442688d3fde6262bfdca86d6bbf6ba708784afcac168c06840c71facac70e390f5f759ac121b9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"memoizee@npm:^0.4.17":
|
||||
version: 0.4.17
|
||||
resolution: "memoizee@npm:0.4.17"
|
||||
@ -5195,6 +5213,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mime-db@npm:^1.54.0":
|
||||
version: 1.54.0
|
||||
resolution: "mime-db@npm:1.54.0"
|
||||
checksum: 10c0/8d907917bc2a90fa2df842cdf5dfeaf509adc15fe0531e07bb2f6ab15992416479015828d6a74200041c492e42cce3ebf78e5ce714388a0a538ea9c53eece284
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mime-types@npm:^2.1.12, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34":
|
||||
version: 2.1.35
|
||||
resolution: "mime-types@npm:2.1.35"
|
||||
@ -5204,6 +5229,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mime-types@npm:^3.0.0":
|
||||
version: 3.0.1
|
||||
resolution: "mime-types@npm:3.0.1"
|
||||
dependencies:
|
||||
mime-db: "npm:^1.54.0"
|
||||
checksum: 10c0/bd8c20d3694548089cf229016124f8f40e6a60bbb600161ae13e45f793a2d5bb40f96bbc61f275836696179c77c1d6bf4967b2a75e0a8ad40fe31f4ed5be4da5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mime@npm:*, mime@npm:^4.0.4":
|
||||
version: 4.0.6
|
||||
resolution: "mime@npm:4.0.6"
|
||||
@ -7102,13 +7136,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"string-argv@npm:^0.3.1":
|
||||
version: 0.3.1
|
||||
resolution: "string-argv@npm:0.3.1"
|
||||
checksum: 10c0/f59582070f0a4a2d362d8331031f313771ad2b939b223b0593d7765de2689c975e0069186cef65977a29af9deec248c7e480ea4015d153ead754aea5e4bcfe7c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"string-argv@npm:^0.3.2":
|
||||
version: 0.3.2
|
||||
resolution: "string-argv@npm:0.3.2"
|
||||
@ -7206,20 +7233,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"superagent@npm:10.2.0":
|
||||
version: 10.2.0
|
||||
resolution: "superagent@npm:10.2.0"
|
||||
"superagent@npm:10.2.1":
|
||||
version: 10.2.1
|
||||
resolution: "superagent@npm:10.2.1"
|
||||
dependencies:
|
||||
component-emitter: "npm:^1.3.0"
|
||||
cookiejar: "npm:^2.1.4"
|
||||
debug: "npm:^4.3.4"
|
||||
fast-safe-stringify: "npm:^2.1.1"
|
||||
form-data: "npm:^4.0.0"
|
||||
formidable: "npm:^3.5.2"
|
||||
formidable: "npm:^3.5.4"
|
||||
methods: "npm:^1.1.2"
|
||||
mime: "npm:2.6.0"
|
||||
qs: "npm:^6.11.0"
|
||||
checksum: 10c0/a1616a352831feddbcb7fa04c0af0a65d1ac68f03c5d7710d4df25c71cd470721764f9a180aac8605c6695f2e8fee23a037457169b23467045b5d43bc8cbc646
|
||||
checksum: 10c0/526e3716f765873fc2f98a00fe0c8cbfe57976c501a30486307eefe54a6a5e379a0adf2ca8b29d40bc33a34d6baf7814972b0398b6002445ca3b4af07487f090
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -7534,19 +7561,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tsc-watch@npm:6.2.1":
|
||||
version: 6.2.1
|
||||
resolution: "tsc-watch@npm:6.2.1"
|
||||
"tsc-watch@npm:7.1.1":
|
||||
version: 7.1.1
|
||||
resolution: "tsc-watch@npm:7.1.1"
|
||||
dependencies:
|
||||
cross-spawn: "npm:^7.0.3"
|
||||
cross-spawn: "npm:^7.0.6"
|
||||
node-cleanup: "npm:^2.1.2"
|
||||
ps-tree: "npm:^1.2.0"
|
||||
string-argv: "npm:^0.3.1"
|
||||
string-argv: "npm:^0.3.2"
|
||||
peerDependencies:
|
||||
typescript: "*"
|
||||
bin:
|
||||
tsc-watch: dist/lib/tsc-watch.js
|
||||
checksum: 10c0/f5fe19e5ac9f4c42a5600c20aee9ff49e282f11813aead65ed58fa11d98a20f5a82bf4f931897270f49f6475dd54e9aab9c46a07c3801b8d237dfbe77bcf1bfc
|
||||
checksum: 10c0/e69b530c2664213574aa67bb47544cf8d4e55ea46cd2a8929f44d8b8c8a70c3574b9ebe7b1752348690b276ece895f8db86fcf84c1d88be9868eb0705cce2dad
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -7591,7 +7618,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"type-is@npm:^1.6.18, type-is@npm:~1.6.18":
|
||||
"type-is@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "type-is@npm:2.0.1"
|
||||
dependencies:
|
||||
content-type: "npm:^1.0.5"
|
||||
media-typer: "npm:^1.1.0"
|
||||
mime-types: "npm:^3.0.0"
|
||||
checksum: 10c0/7f7ec0a060b16880bdad36824ab37c26019454b67d73e8a465ed5a3587440fbe158bc765f0da68344498235c877e7dbbb1600beccc94628ed05599d667951b99
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"type-is@npm:~1.6.18":
|
||||
version: 1.6.18
|
||||
resolution: "type-is@npm:1.6.18"
|
||||
dependencies:
|
||||
@ -7827,13 +7865,13 @@ __metadata:
|
||||
slug: "npm:^9.0.0"
|
||||
source-map-support: "npm:0.5.21"
|
||||
stoppable: "npm:^1.1.0"
|
||||
superagent: "npm:10.2.0"
|
||||
superagent: "npm:10.2.1"
|
||||
supertest: "npm:7.0.0"
|
||||
tldts: "npm:7.0.6"
|
||||
ts-node: "npm:10.9.2"
|
||||
ts-toolbelt: "npm:^9.6.0"
|
||||
tsc-watch: "npm:6.2.1"
|
||||
type-is: "npm:^1.6.18"
|
||||
tsc-watch: "npm:7.1.1"
|
||||
type-is: "npm:^2.0.0"
|
||||
typescript: "npm:5.8.3"
|
||||
ulidx: "npm:^2.4.1"
|
||||
unleash-client: "npm:^6.7.0-beta.0"
|
||||
|
Loading…
Reference in New Issue
Block a user