Fix conflicts after merging from main

This commit is contained in:
Sonia Aguilar 2024-05-28 12:09:16 +02:00
commit c16244234e
43 changed files with 658 additions and 135 deletions

1
.github/CODEOWNERS vendored
View File

@ -435,6 +435,7 @@ playwright.config.ts @grafana/plugins-platform-frontend
/public/app/features/transformers/timeSeriesTable/ @grafana/dataviz-squad @grafana/app-o11y-visualizations
/public/app/features/users/ @grafana/identity-access-team
/public/app/features/variables/ @grafana/dashboards-squad
/public/app/features/trash-section/ @grafana/grafana-frontend-platform
/public/app/plugins/panel/alertlist/ @grafana/alerting-frontend
/public/app/plugins/panel/annolist/ @grafana/grafana-frontend-platform
/public/app/plugins/panel/barchart/ @grafana/dataviz-squad

View File

@ -0,0 +1,121 @@
diff --git a/build/GridItem.js b/build/GridItem.js
index 0a700da9f1180ca532e32e04dc7ea50f2e67b96c..a2e4673fa1133aeaa4018cc01312ca386c3395f5 100644
--- a/build/GridItem.js
+++ b/build/GridItem.js
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
});
exports.default = void 0;
var _react = _interopRequireDefault(require("react"));
+var _reactDOM = require("react-dom");
var _propTypes = _interopRequireDefault(require("prop-types"));
var _reactDraggable = require("react-draggable");
var _reactResizable = require("react-resizable");
@@ -147,8 +148,10 @@ class GridItem extends _react.default.Component /*:: <Props, State>*/{
const pTop = parentRect.top / transformScale;
newPosition.left = cLeft - pLeft + offsetParent.scrollLeft;
newPosition.top = cTop - pTop + offsetParent.scrollTop;
- this.setState({
- dragging: newPosition
+ _reactDOM.flushSync(() => {
+ this.setState({
+ dragging: newPosition
+ });
});
// Call callback with this data
@@ -213,8 +216,10 @@ class GridItem extends _react.default.Component /*:: <Props, State>*/{
top,
left
};
- this.setState({
- dragging: newPosition
+ _reactDOM.flushSync(() => {
+ this.setState({
+ dragging: newPosition
+ });
});
// Call callback with this data
@@ -261,8 +266,10 @@ class GridItem extends _react.default.Component /*:: <Props, State>*/{
top,
left
};
- this.setState({
- dragging: null
+ _reactDOM.flushSync(() => {
+ this.setState({
+ dragging: null
+ });
});
const {
x,
@@ -485,8 +492,10 @@ class GridItem extends _react.default.Component /*:: <Props, State>*/{
let updatedSize = size;
if (node) {
updatedSize = (0, _utils.resizeItemInDirection)(handle, position, size, containerWidth);
- this.setState({
- resizing: handlerName === "onResizeStop" ? null : updatedSize
+ _reactDOM.flushSync(() => {
+ this.setState({
+ resizing: handlerName === "onResizeStop" ? null : updatedSize
+ });
});
}
diff --git a/lib/GridItem.jsx b/lib/GridItem.jsx
index dbe41f92388f19d3e476690fa0ee5584ab9d5bb4..1e4713667cd7dadd6618fe06176804a02ee3ccc2 100644
--- a/lib/GridItem.jsx
+++ b/lib/GridItem.jsx
@@ -1,5 +1,6 @@
// @flow
import React from "react";
+import { flushSync } from "react-dom";
import PropTypes from "prop-types";
import { DraggableCore } from "react-draggable";
import { Resizable } from "react-resizable";
@@ -459,7 +460,9 @@ export default class GridItem extends React.Component<Props, State> {
const pTop = parentRect.top / transformScale;
newPosition.left = cLeft - pLeft + offsetParent.scrollLeft;
newPosition.top = cTop - pTop + offsetParent.scrollTop;
- this.setState({ dragging: newPosition });
+ flushSync(() => {
+ this.setState({ dragging: newPosition });
+ });
// Call callback with this data
const { x, y } = calcXY(
@@ -516,7 +519,9 @@ export default class GridItem extends React.Component<Props, State> {
}
const newPosition: PartialPosition = { top, left };
- this.setState({ dragging: newPosition });
+ flushSync(() => {
+ this.setState({ dragging: newPosition });
+ });
// Call callback with this data
const { containerPadding } = this.props;
@@ -549,7 +554,9 @@ export default class GridItem extends React.Component<Props, State> {
const { w, h, i, containerPadding } = this.props;
const { left, top } = this.state.dragging;
const newPosition: PartialPosition = { top, left };
- this.setState({ dragging: null });
+ flushSync(() => {
+ this.setState({ dragging: null });
+ });
const { x, y } = calcXY(
this.getPositionParams(),
@@ -605,8 +612,10 @@ export default class GridItem extends React.Component<Props, State> {
size,
containerWidth
);
- this.setState({
- resizing: handlerName === "onResizeStop" ? null : updatedSize
+ flushSync(() => {
+ this.setState({
+ resizing: handlerName === "onResizeStop" ? null : updatedSize
+ });
});
}

View File

@ -28,7 +28,6 @@ For more information about feature release stages, refer to [Release life cycle
| `featureHighlights` | Highlight Grafana Enterprise features | |
| `correlations` | Correlations page | Yes |
| `exploreContentOutline` | Content outline sidebar | Yes |
| `returnToPrevious` | Enables the return to previous context functionality | Yes |
| `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes |
| `nestedFolders` | Enable folder nesting | Yes |
| `nestedFolderPicker` | Enables the new folder picker to work with nested folders. Requires the nestedFolders feature toggle | Yes |
@ -191,6 +190,7 @@ Experimental features might be changed or removed without prior notice.
| `alertingListViewV2` | Enables the new alert list view design |
| `notificationBanner` | Enables the notification banner UI and API |
| `alertingCentralAlertHistory` | Enables the new central alert history. |
| `dashboardRestore` | Enables deleted dashboard restore feature |
## Development feature toggles

View File

@ -3,9 +3,6 @@ import { e2e } from '../utils';
describe('ReturnToPrevious button', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
cy.window().then((win) => {
win.localStorage.setItem('grafana.featureToggles', 'returnToPrevious=1');
});
cy.visit('/alerting/list');
e2e.components.AlertRules.groupToggle().first().click();

View File

@ -354,7 +354,7 @@
"react-dom": "18.2.0",
"react-draggable": "4.4.6",
"react-dropzone": "^14.2.3",
"react-grid-layout": "1.4.4",
"react-grid-layout": "patch:react-grid-layout@npm%3A1.4.4#~/.yarn/patches/react-grid-layout-npm-1.4.4-4024c5395b.patch",
"react-highlight-words": "0.20.0",
"react-hook-form": "^7.49.2",
"react-i18next": "^14.0.0",
@ -414,7 +414,8 @@
"history@4.10.1": "patch:history@npm%3A4.10.1#./.yarn/patches/history-npm-4.10.1-ee217563ae.patch",
"history@^4.9.0": "patch:history@npm%3A4.10.1#./.yarn/patches/history-npm-4.10.1-ee217563ae.patch",
"redux": "^5.0.0",
"@storybook/blocks@npm:8.0.10": "patch:@storybook/blocks@npm%3A8.0.10#~/.yarn/patches/@storybook-blocks-npm-8.0.10-6f477cd35f.patch"
"@storybook/blocks@npm:8.0.10": "patch:@storybook/blocks@npm%3A8.0.10#~/.yarn/patches/@storybook-blocks-npm-8.0.10-6f477cd35f.patch",
"react-grid-layout": "patch:react-grid-layout@npm%3A1.4.4#~/.yarn/patches/react-grid-layout-npm-1.4.4-4024c5395b.patch"
},
"workspaces": {
"packages": [

View File

@ -971,7 +971,7 @@ describe('getLinksSupplier', () => {
});
it('handles link click handlers', () => {
const onClickSpy = jest.fn();
const replaceSpy = jest.fn();
const replaceSpy = jest.fn().mockImplementation((value, vars, format) => value);
const f0 = createDataFrame({
name: 'A',
fields: [
@ -1008,8 +1008,8 @@ describe('getLinksSupplier', () => {
links[0].onClick!({});
expect(onClickSpy).toBeCalledTimes(1);
expect(replaceSpy).toBeCalledTimes(4);
expect(onClickSpy).toHaveBeenCalledTimes(1);
expect(replaceSpy).toHaveBeenCalledTimes(5);
// check that onClick variable replacer has scoped vars bound to it
expect(replaceSpy.mock.calls[1][1]).toHaveProperty('foo', { text: 'bar', value: 'bar' });
});
@ -1057,6 +1057,49 @@ describe('getLinksSupplier', () => {
expect(replaceSpy.mock.calls[1][1]).toHaveProperty('foo', { text: 'bar', value: 'bar' });
});
});
it('handles dynamic links with onclick handler', () => {
const replaceSpy = jest.fn().mockReturnValue('url interpolated 10');
const onClickUrlSpy = jest.fn();
const scopedVars = { foo: { text: 'bar', value: 'bar' } };
const f0 = createDataFrame({
name: 'A',
fields: [
{
name: 'message',
type: FieldType.string,
config: {
links: [
{
url: 'should be ignored',
onClick: (evt) => {
onClickUrlSpy();
evt.replaceVariables?.('${foo}');
},
title: 'title to be interpolated',
},
{
url: 'should not be ignored',
title: 'title to be interpolated',
},
],
},
},
],
});
const supplier = getLinksSupplier(f0, f0.fields[0], scopedVars, replaceSpy);
const links = supplier({});
links[0].onClick!({});
expect(onClickUrlSpy).toHaveBeenCalledTimes(1);
expect(links.length).toBe(2);
expect(links[0].href).toEqual('url interpolated 10');
expect(links[0].onClick).toBeDefined();
expect(replaceSpy).toHaveBeenCalledTimes(5);
// check that onClick variable replacer has scoped vars bound to it
expect(replaceSpy.mock.calls[1][1]).toHaveProperty('foo', { text: 'bar', value: 'bar' });
});
});
describe('applyRawFieldOverrides', () => {

View File

@ -468,9 +468,23 @@ export const getLinksSupplier =
let linkModel: LinkModel<Field>;
let href =
link.onClick || !link.onBuildUrl
? link.url
: link.onBuildUrl({
origin: field,
replaceVariables: boundReplaceVariables,
});
if (href) {
href = locationUtil.assureBaseUrl(href.replace(/\n/g, ''));
href = replaceVariables(href, dataLinkScopedVars, VariableFormatID.UriEncode);
href = locationUtil.processUrl(href);
}
if (link.onClick) {
linkModel = {
href: link.url,
href,
title: replaceVariables(link.title || '', dataLinkScopedVars),
target: link.targetBlank ? '_blank' : undefined,
onClick: (evt: MouseEvent, origin: Field) => {
@ -483,19 +497,6 @@ export const getLinksSupplier =
origin: field,
};
} else {
let href = link.onBuildUrl
? link.onBuildUrl({
origin: field,
replaceVariables: boundReplaceVariables,
})
: link.url;
if (href) {
href = locationUtil.assureBaseUrl(href.replace(/\n/g, ''));
href = replaceVariables(href, dataLinkScopedVars, VariableFormatID.UriEncode);
href = locationUtil.processUrl(href);
}
linkModel = {
href,
title: replaceVariables(link.title || '', dataLinkScopedVars),

View File

@ -44,7 +44,6 @@ export interface FeatureToggles {
disableSecretsCompatibility?: boolean;
logRequestsInstrumentedAsUnknown?: boolean;
topnav?: boolean;
returnToPrevious?: boolean;
grpcServer?: boolean;
unifiedStorage?: boolean;
dualWritePlaylistsMode2?: boolean;

View File

@ -132,6 +132,7 @@
"@testing-library/jest-dom": "6.4.2",
"@testing-library/react": "15.0.2",
"@testing-library/user-event": "14.5.2",
"@types/chance": "1.1.6",
"@types/common-tags": "^1.8.0",
"@types/d3": "7.4.3",
"@types/hoist-non-react-statics": "3.3.5",
@ -158,6 +159,7 @@
"@types/testing-library__jest-dom": "5.14.9",
"@types/tinycolor2": "1.4.6",
"@types/uuid": "9.0.8",
"chance": "1.1.11",
"common-tags": "1.8.2",
"core-js": "3.37.0",
"css-loader": "7.1.1",

View File

@ -1,6 +1,7 @@
import { auto } from '@popperjs/core';
import { action } from '@storybook/addon-actions';
import { Meta, StoryFn } from '@storybook/react';
import Chance from 'chance';
import React, { useState } from 'react';
import { SelectableValue, toIconName } from '@grafana/data';
@ -12,6 +13,26 @@ import mdx from './Select.mdx';
import { generateOptions, generateThousandsOfOptions } from './mockOptions';
import { SelectCommonProps } from './types';
const chance = new Chance();
const manyGroupedOptions = [
{ label: 'Foo', value: '1' },
{
label: 'Animals',
options: new Array(100).fill(0).map((_, i) => {
const animal = chance.animal();
return { label: animal, value: animal };
}),
},
{
label: 'People',
options: new Array(100).fill(0).map((_, i) => {
const person = chance.name();
return { label: person, value: person };
}),
},
];
const meta: Meta = {
title: 'Forms/Select',
component: Select,
@ -242,6 +263,26 @@ export const MultiSelectWithOptionGroups: StoryFn = (args) => {
);
};
export const MultiSelectWithOptionGroupsVirtualized: StoryFn = (args) => {
const [value, setValue] = useState<string[]>();
return (
<>
<MultiSelect
options={manyGroupedOptions}
virtualized
value={value}
onChange={(v) => {
setValue(v.map((v) => v.value!));
action('onChange')(v);
}}
prefix={getPrefix(args.icon)}
{...args}
/>
</>
);
};
export const MultiSelectBasic: StoryFn = (args) => {
const [value, setValue] = useState<Array<SelectableValue<string>>>([]);

View File

@ -1,6 +1,6 @@
import { cx } from '@emotion/css';
import { max } from 'lodash';
import React, { RefCallback, useEffect, useRef } from 'react';
import React, { RefCallback, useEffect, useMemo, useRef } from 'react';
import { MenuListProps } from 'react-select';
import { FixedSizeList as List } from 'react-window';
@ -57,8 +57,18 @@ export const VirtualizedSelectMenu = ({
const styles = getSelectStyles(theme);
const listRef = useRef<List>(null);
const focusedIndex = options.findIndex((option: SelectableValue<unknown>) => option.value === focusedOption?.value);
// we need to check for option groups (categories)
// these are top level options with child options
// if they exist, flatten the list of options
const flattenedOptions = useMemo(
() => options.flatMap((option) => (option.options ? [option, ...option.options] : [option])),
[options]
);
// scroll the focused option into view when navigating with keyboard
const focusedIndex = flattenedOptions.findIndex(
(option: SelectableValue<unknown>) => option.value === focusedOption?.value
);
useEffect(() => {
listRef.current?.scrollToItem(focusedIndex);
}, [focusedIndex]);
@ -67,10 +77,23 @@ export const VirtualizedSelectMenu = ({
return null;
}
const longestOption = max(options.map((option) => option.label?.length)) ?? 0;
// flatten the children to account for any categories
// these will have array children that are the individual options
const flattenedChildren = children.flatMap((child) => {
if (hasArrayChildren(child)) {
// need to remove the children from the category else they end up in the DOM twice
const childWithoutChildren = React.cloneElement(child, {
children: null,
});
return [childWithoutChildren, ...child.props.children];
}
return [child];
});
const longestOption = max(flattenedOptions.map((option) => option.label?.length)) ?? 0;
const widthEstimate =
longestOption * VIRTUAL_LIST_WIDTH_ESTIMATE_MULTIPLIER + VIRTUAL_LIST_PADDING * 2 + VIRTUAL_LIST_WIDTH_EXTRA;
const heightEstimate = Math.min(options.length * VIRTUAL_LIST_ITEM_HEIGHT, maxHeight);
const heightEstimate = Math.min(flattenedChildren.length * VIRTUAL_LIST_ITEM_HEIGHT, maxHeight);
return (
<List
@ -79,14 +102,20 @@ export const VirtualizedSelectMenu = ({
height={heightEstimate}
width={widthEstimate}
aria-label="Select options menu"
itemCount={children.length}
itemCount={flattenedChildren.length}
itemSize={VIRTUAL_LIST_ITEM_HEIGHT}
>
{({ index, style }) => <div style={{ ...style, overflow: 'hidden' }}>{children[index]}</div>}
{({ index, style }) => <div style={{ ...style, overflow: 'hidden' }}>{flattenedChildren[index]}</div>}
</List>
);
};
// check if a child has array children (and is therefore a react-select group)
// we need to flatten these so the correct count and elements are passed to the virtualized list
const hasArrayChildren = (child: React.ReactNode) => {
return React.isValidElement(child) && Array.isArray(child.props.children);
};
VirtualizedSelectMenu.displayName = 'VirtualizedSelectMenu';
interface SelectMenuOptionProps<T> {

View File

@ -315,8 +315,8 @@ func (s *Service) prepareInstanceSettings(ctx context.Context, pluginContext bac
// Make sure it is a known plugin type
p, found := s.pluginStore.Plugin(ctx, settings.Type)
if !found {
return nil, errutil.BadRequest("datasource.unknownPlugin",
errutil.WithPublicMessage(fmt.Sprintf("plugin '%s' not found", settings.Type)))
// Ignore non-existing plugins for the time being
return settings, nil
}
// When the APIVersion is set, the client must also implement AdmissionHandler

View File

@ -206,14 +206,6 @@ var (
Expression: "true", // enabled by default
Owner: grafanaFrontendPlatformSquad,
},
{
Name: "returnToPrevious",
Description: "Enables the return to previous context functionality",
Stage: FeatureStageGeneralAvailability,
FrontendOnly: true,
Expression: "true", // enabled by default
Owner: grafanaFrontendPlatformSquad,
},
{
Name: "grpcServer",
Description: "Run the GRPC server",
@ -1287,7 +1279,6 @@ var (
Description: "Enables deleted dashboard restore feature",
Stage: FeatureStageExperimental,
Owner: grafanaFrontendPlatformSquad,
HideFromDocs: true,
HideFromAdminPage: true,
},
{

View File

@ -25,7 +25,6 @@ scenes,experimental,@grafana/dashboards-squad,false,false,true
disableSecretsCompatibility,experimental,@grafana/hosted-grafana-team,false,true,false
logRequestsInstrumentedAsUnknown,experimental,@grafana/hosted-grafana-team,false,false,false
topnav,deprecated,@grafana/grafana-frontend-platform,false,false,false
returnToPrevious,GA,@grafana/grafana-frontend-platform,false,false,true
grpcServer,preview,@grafana/grafana-app-platform-squad,false,false,false
unifiedStorage,experimental,@grafana/grafana-app-platform-squad,false,true,false
dualWritePlaylistsMode2,experimental,@grafana/search-and-storage,false,false,false

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
25 disableSecretsCompatibility experimental @grafana/hosted-grafana-team false true false
26 logRequestsInstrumentedAsUnknown experimental @grafana/hosted-grafana-team false false false
27 topnav deprecated @grafana/grafana-frontend-platform false false false
returnToPrevious GA @grafana/grafana-frontend-platform false false true
28 grpcServer preview @grafana/grafana-app-platform-squad false false false
29 unifiedStorage experimental @grafana/grafana-app-platform-squad false true false
30 dualWritePlaylistsMode2 experimental @grafana/search-and-storage false false false

View File

@ -111,10 +111,6 @@ const (
// Enables topnav support in external plugins. The new Grafana navigation cannot be disabled.
FlagTopnav = "topnav"
// FlagReturnToPrevious
// Enables the return to previous context functionality
FlagReturnToPrevious = "returnToPrevious"
// FlagGrpcServer
// Run the GRPC server
FlagGrpcServer = "grpcServer"

View File

@ -60,7 +60,8 @@
"metadata": {
"name": "returnToPrevious",
"resourceVersion": "1716448665531",
"creationTimestamp": "2024-05-23T07:17:45Z"
"creationTimestamp": "2024-05-23T07:17:45Z",
"deletionTimestamp": "2024-05-27T09:59:33Z"
},
"spec": {
"description": "Enables the return to previous context functionality",
@ -587,15 +588,17 @@
{
"metadata": {
"name": "dashboardRestore",
"resourceVersion": "1716448665531",
"creationTimestamp": "2024-05-23T07:17:45Z"
"resourceVersion": "1716564259132",
"creationTimestamp": "2024-05-23T07:17:45Z",
"annotations": {
"grafana.app/updatedTimestamp": "2024-05-24 15:24:19.132272 +0000 UTC"
}
},
"spec": {
"description": "Enables deleted dashboard restore feature",
"stage": "experimental",
"codeowner": "@grafana/grafana-frontend-platform",
"hideFromAdminPage": true,
"hideFromDocs": true
"hideFromAdminPage": true
}
},
{
@ -2203,26 +2206,19 @@
},
{
"metadata": {
"name": "influxdbRunQueriesInParallel",
"resourceVersion": "1716448665531",
"creationTimestamp": "2024-05-23T07:17:45Z"
},
"spec": {
"description": "Enables running InfluxDB Influxql queries in parallel",
"stage": "privatePreview",
"codeowner": "@grafana/observability-metrics"
}
},
{
"metadata": {
"name": "disableSSEDataplane",
"resourceVersion": "1716448665531",
"creationTimestamp": "2024-05-23T07:17:45Z"
"name": "dashboardRestore",
"resourceVersion": "1716563559003",
"creationTimestamp": "2024-02-20T18:50:41Z",
"deletionTimestamp": "2024-05-24T15:24:19Z",
"annotations": {
"grafana.app/updatedTimestamp": "2024-05-24 15:12:39.003245 +0000 UTC"
}
},
"spec": {
"description": "Disables dataplane specific processing in server side expressions.",
"stage": "experimental",
"codeowner": "@grafana/observability-metrics"
"codeowner": "@grafana/grafana-frontend-platform",
"hideFromAdminPage": true
}
},
{
@ -2277,6 +2273,30 @@
"codeowner": "@grafana/alerting-squad",
"frontend": true
}
},
{
"metadata": {
"name": "disableSSEDataplane",
"resourceVersion": "1716816471156",
"creationTimestamp": "2024-05-27T13:27:51Z"
},
"spec": {
"description": "Disables dataplane specific processing in server side expressions.",
"stage": "experimental",
"codeowner": "@grafana/observability-metrics"
}
},
{
"metadata": {
"name": "influxdbRunQueriesInParallel",
"resourceVersion": "1716816471156",
"creationTimestamp": "2024-05-27T13:27:51Z"
},
"spec": {
"description": "Enables running InfluxDB Influxql queries in parallel",
"stage": "privatePreview",
"codeowner": "@grafana/observability-metrics"
}
}
]
}

View File

@ -353,6 +353,15 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navt
Icon: "library-panel",
})
}
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagDashboardRestore) && hasAccess(ac.EvalPermission(dashboards.ActionDashboardsDelete)) {
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Trash",
SubTitle: "Any items remaining in the Trash for more than 30 days will be automatically deleted",
Id: "dashboards/trash",
Url: s.cfg.AppSubURL + "/dashboard/trash",
})
}
}
if hasAccess(ac.EvalPermission(dashboards.ActionDashboardsCreate)) {

View File

@ -164,7 +164,7 @@ func (ng *AlertNG) init() error {
// If enabled, configure the remote Alertmanager.
// - If several toggles are enabled, the order of precedence is RemoteOnly, RemotePrimary, RemoteSecondary
// - If no toggles are enabled, we default to using only the internal Alertmanager
// We currently support only remote secondary mode, so in case other toggles are enabled we fall back to remote secondary.
// We currently do not support remote primary mode, so we fall back to remote secondary.
var overrides []notifier.Option
moaLogger := log.New("ngalert.multiorg.alertmanager")
remoteOnly := ng.FeatureToggles.IsEnabled(initCtx, featuremgmt.FlagAlertmanagerRemoteOnly)
@ -172,7 +172,35 @@ func (ng *AlertNG) init() error {
remoteSecondary := ng.FeatureToggles.IsEnabled(initCtx, featuremgmt.FlagAlertmanagerRemoteSecondary)
if ng.Cfg.UnifiedAlerting.RemoteAlertmanager.Enable {
switch {
case remoteOnly, remotePrimary:
case remoteOnly:
ng.Log.Debug("Starting Grafana with remote only mode enabled")
m := ng.Metrics.GetRemoteAlertmanagerMetrics()
m.Info.WithLabelValues(metrics.ModeRemoteOnly).Set(1)
// This function will be used by the MOA to create new Alertmanagers.
override := notifier.WithAlertmanagerOverride(func(_ notifier.OrgAlertmanagerFactory) notifier.OrgAlertmanagerFactory {
return func(ctx context.Context, orgID int64) (notifier.Alertmanager, error) {
// Create remote Alertmanager.
cfg := remote.AlertmanagerConfig{
BasicAuthPassword: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.Password,
DefaultConfig: ng.Cfg.UnifiedAlerting.DefaultConfiguration,
OrgID: orgID,
TenantID: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.TenantID,
URL: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.URL,
PromoteConfig: true,
}
remoteAM, err := createRemoteAlertmanager(cfg, ng.KVStore, ng.SecretsService.Decrypt, m)
if err != nil {
moaLogger.Error("Failed to create remote Alertmanager", "err", err)
return nil, err
}
return remoteAM, nil
}
})
overrides = append(overrides, override)
case remotePrimary:
ng.Log.Warn("Only remote secondary mode is supported at the moment, falling back to remote secondary")
fallthrough

View File

@ -179,7 +179,7 @@ func (p *EnvVarsProvider) pluginSettingsEnvVars(pluginID string) []string {
func (p *EnvVarsProvider) envVar(key, value string) string {
if strings.Contains(value, "\x00") {
p.logger.Error("Variable with key '%s' contains a nil character", key)
p.logger.Error("Variable with key '%s' contains NUL", key)
}
return fmt.Sprintf("%s=%s", key, value)
}

View File

@ -15,16 +15,19 @@ const (
kvStoreType = "extsvc-token"
// #nosec G101 - this is not a hardcoded secret
tokenNamePrefix = "extsvc-token"
maxTokenGenRetries = 10
)
var (
ErrCannotBeDeleted = errutil.BadRequest("extsvcaccounts.ErrCannotBeDeleted", errutil.WithPublicMessage("external service account cannot be deleted"))
ErrCannotBeUpdated = errutil.BadRequest("extsvcaccounts.ErrCannotBeUpdated", errutil.WithPublicMessage("external service account cannot be updated"))
ErrCannotCreateToken = errutil.BadRequest("extsvcaccounts.ErrCannotCreateToken", errutil.WithPublicMessage("cannot add external service account token"))
ErrCannotDeleteToken = errutil.BadRequest("extsvcaccounts.ErrCannotDeleteToken", errutil.WithPublicMessage("cannot delete external service account token"))
ErrCannotListTokens = errutil.BadRequest("extsvcaccounts.ErrCannotListTokens", errutil.WithPublicMessage("cannot list external service account tokens"))
ErrCredentialsNotFound = errutil.NotFound("extsvcaccounts.credentials-not-found")
ErrInvalidName = errutil.BadRequest("extsvcaccounts.ErrInvalidName", errutil.WithPublicMessage("only external service account names can be prefixed with 'extsvc-'"))
ErrCannotBeDeleted = errutil.BadRequest("extsvcaccounts.ErrCannotBeDeleted", errutil.WithPublicMessage("external service account cannot be deleted"))
ErrCannotBeUpdated = errutil.BadRequest("extsvcaccounts.ErrCannotBeUpdated", errutil.WithPublicMessage("external service account cannot be updated"))
ErrCannotCreateToken = errutil.BadRequest("extsvcaccounts.ErrCannotCreateToken", errutil.WithPublicMessage("cannot add external service account token"))
ErrCannotDeleteToken = errutil.BadRequest("extsvcaccounts.ErrCannotDeleteToken", errutil.WithPublicMessage("cannot delete external service account token"))
ErrCannotListTokens = errutil.BadRequest("extsvcaccounts.ErrCannotListTokens", errutil.WithPublicMessage("cannot list external service account tokens"))
ErrCredentialsGenFailed = errutil.Internal("extsvcaccounts.ErrCredentialsGenFailed")
ErrCredentialsNotFound = errutil.NotFound("extsvcaccounts.ErrCredentialsNotFound")
ErrInvalidName = errutil.BadRequest("extsvcaccounts.ErrInvalidName", errutil.WithPublicMessage("only external service account names can be prefixed with 'extsvc-'"))
extsvcuser = &user.SignedInUser{
OrgID: extsvcauth.TmpOrgID,

View File

@ -355,7 +355,7 @@ func (esa *ExtSvcAccountsService) getExtSvcAccountToken(ctx context.Context, org
// Generate token
ctxLogger.Info("Generate new service account token", "service", extSvcSlug, "orgID", orgID)
newKeyInfo, err := satokengen.New(extSvcSlug)
newKeyInfo, err := genTokenWithRetries(ctxLogger, extSvcSlug)
if err != nil {
return "", err
}
@ -380,6 +380,52 @@ func (esa *ExtSvcAccountsService) getExtSvcAccountToken(ctx context.Context, org
return newKeyInfo.ClientSecret, nil
}
func genTokenWithRetries(ctxLogger log.Logger, extSvcSlug string) (satokengen.KeyGenResult, error) {
var newKeyInfo satokengen.KeyGenResult
var err error
retry := 0
for retry < maxTokenGenRetries {
newKeyInfo, err = satokengen.New(extSvcSlug)
if err != nil {
return satokengen.KeyGenResult{}, err
}
if !strings.Contains(newKeyInfo.ClientSecret, "\x00") {
return newKeyInfo, nil
}
retry++
ctxLogger.Warn("Generated a token containing NUL, retrying",
"service", extSvcSlug,
"retry", retry,
)
// On first retry, log the token parts that contain a nil byte
if retry == 1 {
logTokenNULParts(ctxLogger, extSvcSlug, newKeyInfo.ClientSecret)
}
}
return satokengen.KeyGenResult{}, ErrCredentialsGenFailed.Errorf("Failed to generate a token for %s", extSvcSlug)
}
// logTokenNULParts logs a warning if the external service token contains a nil byte
// Tokens normally have 3 parts "gl+serviceID_secret_checksum"
// Log the part of the generated token that contains a nil byte
func logTokenNULParts(ctxLogger log.Logger, extSvcSlug string, token string) {
parts := strings.Split(token, "_")
for i := range parts {
if strings.Contains(parts[i], "\x00") {
ctxLogger.Warn("Token contains NUL",
"service", extSvcSlug,
"part", i,
"part_len", len(parts[i]),
"parts_count", len(parts),
)
}
}
}
// GetExtSvcCredentials get the credentials of an External Service from an encrypted storage
func (esa *ExtSvcAccountsService) GetExtSvcCredentials(ctx context.Context, orgID int64, extSvcSlug string) (*Credentials, error) {
ctxLogger := esa.logger.FromContext(ctx)
@ -391,6 +437,10 @@ func (esa *ExtSvcAccountsService) GetExtSvcCredentials(ctx context.Context, orgI
if !ok {
return nil, ErrCredentialsNotFound.Errorf("No credential found for in store %v", extSvcSlug)
}
if strings.Contains(token, "\x00") {
ctxLogger.Warn("Loaded token from store containing NUL", "service", extSvcSlug)
logTokenNULParts(ctxLogger, extSvcSlug, token)
}
return &Credentials{Secret: token}, nil
}

View File

@ -760,6 +760,43 @@
}
}
},
"/datasources/uid/{uid}/lbac/teams": {
"put": {
"tags": [
"enterprise"
],
"summary": "Updates LBAC rules for a team.",
"operationId": "updateTeamLBACRulesApi",
"parameters": [
{
"type": "string",
"name": "uid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/okResponse"
},
"400": {
"$ref": "#/responses/badRequestError"
},
"401": {
"$ref": "#/responses/unauthorisedError"
},
"403": {
"$ref": "#/responses/forbiddenError"
},
"404": {
"$ref": "#/responses/notFoundError"
},
"500": {
"$ref": "#/responses/internalServerError"
}
}
}
},
"/datasources/{dataSourceUID}/cache": {
"get": {
"description": "get cache config for a single data source",

View File

@ -4315,6 +4315,40 @@
}
}
},
"/datasources/uid/{uid}/lbac/teams": {
"get": {
"summary": "Retrieves LBAC rules for a team.",
"operationId": "getTeamLBACRulesApiResponse",
"parameters": [
{
"type": "string",
"name": "uid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/okResponse"
},
"400": {
"$ref": "#/responses/badRequestError"
},
"401": {
"$ref": "#/responses/unauthorisedError"
},
"403": {
"$ref": "#/responses/forbiddenError"
},
"404": {
"$ref": "#/responses/notFoundError"
},
"500": {
"$ref": "#/responses/internalServerError"
}
}
}
},
"/datasources/uid/{uid}/resources/{datasource_proxy_route}": {
"get": {
"tags": [

View File

@ -5,7 +5,6 @@ import React, { PropsWithChildren, useEffect } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { locationSearchToObject, locationService } from '@grafana/runtime';
import { useStyles2, LinkButton, useTheme2 } from '@grafana/ui';
import config from 'app/core/config';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange';
import store from 'app/core/store';
@ -55,8 +54,7 @@ export function AppChrome({ children }: Props) {
const { pathname, search } = locationService.getLocation();
const url = pathname + search;
const shouldShowReturnToPrevious =
config.featureToggles.returnToPrevious && state.returnToPrevious && url !== state.returnToPrevious.href;
const shouldShowReturnToPrevious = state.returnToPrevious && url !== state.returnToPrevious.href;
// Clear returnToPrevious when the page is manually navigated to
useEffect(() => {

View File

@ -90,10 +90,6 @@ export class AppChromeService {
}
public setReturnToPrevious = (returnToPrevious: ReturnToPreviousProps) => {
const isReturnToPreviousEnabled = config.featureToggles.returnToPrevious;
if (!isReturnToPreviousEnabled) {
return;
}
const previousPage = this.state.getValue().returnToPrevious;
reportInteraction('grafana_return_to_previous_button_created', {
page: returnToPrevious.href,
@ -105,10 +101,6 @@ export class AppChromeService {
};
public clearReturnToPrevious = (interactionAction: 'clicked' | 'dismissed' | 'auto_dismissed') => {
const isReturnToPreviousEnabled = config.featureToggles.returnToPrevious;
if (!isReturnToPreviousEnabled) {
return;
}
const existingRtp = this.state.getValue().returnToPrevious;
if (existingRtp) {
reportInteraction('grafana_return_to_previous_button_dismissed', {

View File

@ -4,7 +4,7 @@ import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
import { config, reportInteraction } from '@grafana/runtime';
import { reportInteraction } from '@grafana/runtime';
import { ReturnToPrevious, ReturnToPreviousProps } from './ReturnToPrevious';
@ -36,14 +36,9 @@ const setup = () => {
};
describe('ReturnToPrevious', () => {
beforeEach(() => {
/* We enabled the feature toggle */
config.featureToggles.returnToPrevious = true;
});
afterEach(() => {
window.sessionStorage.clear();
jest.resetAllMocks();
config.featureToggles.returnToPrevious = false;
});
it('should render component', async () => {
setup();

View File

@ -40,6 +40,8 @@ export function getNavTitle(navId: string | undefined) {
return t('nav.reporting.title', 'Reporting');
case 'dashboards/public':
return t('nav.public.title', 'Public dashboards');
case 'dashboards/trash':
return t('nav.trash.title', 'Trash');
case 'dashboards/new':
return t('nav.new-dashboard.title', 'New dashboard');
case 'dashboards/folder/new':
@ -206,6 +208,11 @@ export function getNavSubTitle(navId: string | undefined) {
);
case 'dashboards/library-panels':
return t('nav.library-panels.subtitle', 'Reusable panels that can be added to multiple dashboards');
case 'dashboards/trash':
return t(
'nav.trash.subtitle',
'Any items remaining in the Trash for more than 30 days will be automatically deleted'
);
case 'alerting':
return t('nav.alerting.subtitle', 'Learn about problems in your systems moments after they occur');
case 'alerting-upgrade':

View File

@ -1,7 +1,7 @@
import React, { Fragment } from 'react';
import { textUtil } from '@grafana/data';
import { config, useReturnToPrevious } from '@grafana/runtime';
import { useReturnToPrevious } from '@grafana/runtime';
import { Button, LinkButton, Stack } from '@grafana/ui';
import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
@ -68,7 +68,6 @@ const RuleDetailsButtons = ({ rule, rulesSource }: Props) => {
}
if (rule.annotations[Annotation.dashboardUID]) {
const dashboardUID = rule.annotations[Annotation.dashboardUID];
const isReturnToPreviousEnabled = config.featureToggles.returnToPrevious;
if (dashboardUID) {
buttons.push(
<LinkButton
@ -76,7 +75,6 @@ const RuleDetailsButtons = ({ rule, rulesSource }: Props) => {
key="dashboard"
variant="primary"
icon="apps"
target={isReturnToPreviousEnabled ? undefined : '_blank'}
href={`d/${encodeURIComponent(dashboardUID)}`}
onClick={() => {
setReturnToPrevious(rule.name);
@ -93,7 +91,6 @@ const RuleDetailsButtons = ({ rule, rulesSource }: Props) => {
key="panel"
variant="primary"
icon="apps"
target={isReturnToPreviousEnabled ? undefined : '_blank'}
href={`d/${encodeURIComponent(dashboardUID)}?viewPanel=${encodeURIComponent(panelId)}`}
onClick={() => {
setReturnToPrevious(rule.name);

View File

@ -308,6 +308,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
pinned={this.props.pinned}
mouseIsOver={this.state.mouseIsOver}
onBlur={this.onMouseLeave}
expanded={this.state.showDetails}
/>
)}
</tr>

View File

@ -160,4 +160,41 @@ describe('LogRowMessage', () => {
});
});
});
describe('For multi-line logs', () => {
const entry = `Line1
line2
line3`;
const singleLineEntry = entry.replace(/(\r\n|\n|\r)/g, '');
it('Displays the original log line when wrapping is enabled', () => {
setup({
row: createLogRow({ entry, logLevel: LogLevel.error, timeEpochMs: 1546297200000 }),
wrapLogMessage: true,
});
expect(screen.getByText(/Line1/)).toBeInTheDocument();
expect(screen.getByText(/line2/)).toBeInTheDocument();
expect(screen.getByText(/line3/)).toBeInTheDocument();
expect(screen.queryByText(singleLineEntry)).not.toBeInTheDocument();
});
it('Removes new lines from the original log line when wrapping is disabled', () => {
setup({
row: createLogRow({ entry, logLevel: LogLevel.error, timeEpochMs: 1546297200000 }),
wrapLogMessage: false,
});
expect(screen.getByText(singleLineEntry)).toBeInTheDocument();
});
it('Displays the original log line when the line is expanded', () => {
setup({
row: createLogRow({ entry, logLevel: LogLevel.error, timeEpochMs: 1546297200000 }),
wrapLogMessage: true,
expanded: true,
});
expect(screen.getByText(/Line1/)).toBeInTheDocument();
expect(screen.getByText(/line2/)).toBeInTheDocument();
expect(screen.getByText(/line3/)).toBeInTheDocument();
expect(screen.queryByText(singleLineEntry)).not.toBeInTheDocument();
});
});
});

View File

@ -29,6 +29,7 @@ interface Props {
styles: LogRowStyles;
mouseIsOver: boolean;
onBlur: () => void;
expanded?: boolean;
}
interface LogMessageProps {
@ -58,13 +59,20 @@ const LogMessage = ({ hasAnsi, entry, highlights, styles }: LogMessageProps) =>
return <>{entry}</>;
};
const restructureLog = (line: string, prettifyLogMessage: boolean): string => {
const restructureLog = (
line: string,
prettifyLogMessage: boolean,
wrapLogMessage: boolean,
expanded: boolean
): string => {
if (prettifyLogMessage) {
try {
return JSON.stringify(JSON.parse(line), undefined, 2);
} catch (error) {
return line;
}
} catch (error) {}
}
// With wrapping disabled, we want to turn it into a single-line log entry unless the line is expanded
if (!wrapLogMessage && !expanded) {
line = line.replace(/(\r\n|\n|\r)/g, '');
}
return line;
};
@ -84,9 +92,13 @@ export const LogRowMessage = React.memo((props: Props) => {
mouseIsOver,
onBlur,
getRowContextQuery,
expanded,
} = props;
const { hasAnsi, raw } = row;
const restructuredEntry = useMemo(() => restructureLog(raw, prettifyLogMessage), [raw, prettifyLogMessage]);
const restructuredEntry = useMemo(
() => restructureLog(raw, prettifyLogMessage, wrapLogMessage, Boolean(expanded)),
[raw, prettifyLogMessage, wrapLogMessage, expanded]
);
const shouldShowMenu = useMemo(() => mouseIsOver || pinned, [mouseIsOver, pinned]);
return (
<>

View File

@ -0,0 +1,15 @@
import React from 'react';
import { Page } from 'app/core/components/Page/Page';
const TrashPage = () => {
return (
<Page navId="dashboards/trash">
<Page.Contents>
<p>page content</p>
</Page.Contents>
</Page>
);
};
export default TrashPage;

View File

@ -435,6 +435,13 @@ export function getAppRoutes(): RouteDescriptor[] {
() => import(/* webpackChunkName: "SnapshotListPage" */ 'app/features/manage-dashboards/SnapshotListPage')
),
},
config.featureToggles.dashboardRestore && {
path: '/dashboard/trash',
roles: () => contextSrv.evaluatePermission([AccessControlAction.DashboardsDelete]),
component: SafeDynamicImport(
() => import(/* webpackChunkName: "TrashPage" */ 'app/features/trash-section/TrashPage')
),
},
{
path: '/playlists',
component: SafeDynamicImport(

View File

@ -1179,6 +1179,10 @@
"subtitle": "Optimieren Sie die Leistung mit Erkenntnissen aus k6 und Synthetic Monitoring",
"title": "Testen & Synthetics"
},
"trash": {
"subtitle": "",
"title": ""
},
"upgrading": {
"title": "Statistiken und Lizenz"
},

View File

@ -1179,6 +1179,10 @@
"subtitle": "Optimize performance with k6 and Synthetic Monitoring insights",
"title": "Testing & synthetics"
},
"trash": {
"subtitle": "Any items remaining in the Trash for more than 30 days will be automatically deleted",
"title": "Trash"
},
"upgrading": {
"title": "Stats and license"
},

View File

@ -1179,6 +1179,10 @@
"subtitle": "Optimiza el rendimiento con información de seguimiento k6 y sintética",
"title": "Pruebas y síntesis"
},
"trash": {
"subtitle": "",
"title": ""
},
"upgrading": {
"title": "Estadísticas y licencia"
},

View File

@ -1179,6 +1179,10 @@
"subtitle": "Optimisez les performances grâce aux analyses de la surveillance synthétique et k6",
"title": "Tests et synthèses"
},
"trash": {
"subtitle": "",
"title": ""
},
"upgrading": {
"title": "Statistiques et licence"
},

View File

@ -1179,6 +1179,10 @@
"subtitle": "Øpŧįmįžę pęřƒőřmäʼnčę ŵįŧĥ ĸ6 äʼnđ Ŝyʼnŧĥęŧįč Mőʼnįŧőřįʼnģ įʼnşįģĥŧş",
"title": "Ŧęşŧįʼnģ & şyʼnŧĥęŧįčş"
},
"trash": {
"subtitle": "Åʼny įŧęmş řęmäįʼnįʼnģ įʼn ŧĥę Ŧřäşĥ ƒőř mőřę ŧĥäʼn 30 đäyş ŵįľľ þę äūŧőmäŧįčäľľy đęľęŧęđ",
"title": "Ŧřäşĥ"
},
"upgrading": {
"title": "Ŝŧäŧş äʼnđ ľįčęʼnşę"
},

View File

@ -1179,6 +1179,10 @@
"subtitle": "Otimize o desempenho com visões k6 e monitorização sintética",
"title": "Teste e sintética"
},
"trash": {
"subtitle": "",
"title": ""
},
"upgrading": {
"title": "Estatísticas e licença"
},

View File

@ -1173,6 +1173,10 @@
"subtitle": "使用 k6 和 Synthetic Monitoring 洞察优化性能。",
"title": "测试与合成"
},
"trash": {
"subtitle": "",
"title": ""
},
"upgrading": {
"title": "统计信息和许可证"
},

View File

@ -17143,6 +17143,42 @@
]
}
},
"/datasources/uid/{uid}/lbac/teams": {
"get": {
"operationId": "getTeamLBACRulesApiResponse",
"parameters": [
{
"in": "path",
"name": "uid",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"$ref": "#/components/responses/okResponse"
},
"400": {
"$ref": "#/components/responses/badRequestError"
},
"401": {
"$ref": "#/components/responses/unauthorisedError"
},
"403": {
"$ref": "#/components/responses/forbiddenError"
},
"404": {
"$ref": "#/components/responses/notFoundError"
},
"500": {
"$ref": "#/components/responses/internalServerError"
}
},
"summary": "Retrieves LBAC rules for a team."
}
},
"/datasources/uid/{uid}/resources/{datasource_proxy_route}": {
"get": {
"operationId": "callDatasourceResourceWithUID",

View File

@ -101,7 +101,7 @@ $text-color: rgba(36, 41, 46, 1);
$text-color-strong: #000000;
$text-color-semi-weak: rgba(36, 41, 46, 0.75);
$text-color-weak: rgba(36, 41, 46, 0.75);
$text-color-faint: rgba(36, 41, 46, 0.50);
$text-color-faint: rgba(36, 41, 46, 0.64);
$text-color-emphasis: #000000;
$text-blue: #1F62E0;
@ -114,7 +114,7 @@ $brand-gradient-vertical: linear-gradient(0.01deg, #F53E4C -31.2%, #FF8833 113.0
// Links
// -------------------------
$link-color: rgba(36, 41, 46, 1);
$link-color-disabled: rgba(36, 41, 46, 0.50);
$link-color-disabled: rgba(36, 41, 46, 0.64);
$link-hover-color: #000000;
$external-link-color: #1F62E0;
@ -215,7 +215,7 @@ $input-border-color: rgba(36, 41, 46, 0.30);
$input-box-shadow: none;
$input-border-focus: #5794f2;
$input-box-shadow-focus: #5794f2;
$input-color-placeholder: rgba(36, 41, 46, 0.50);
$input-color-placeholder: rgba(36, 41, 46, 0.64);
$input-label-bg: #F4F5F5;
$input-color-select-arrow: #7b8087;

View File

@ -3648,6 +3648,7 @@ __metadata:
"@testing-library/jest-dom": "npm:6.4.2"
"@testing-library/react": "npm:15.0.2"
"@testing-library/user-event": "npm:14.5.2"
"@types/chance": "npm:1.1.6"
"@types/common-tags": "npm:^1.8.0"
"@types/d3": "npm:7.4.3"
"@types/hoist-non-react-statics": "npm:3.3.5"
@ -3676,6 +3677,7 @@ __metadata:
"@types/uuid": "npm:9.0.8"
ansicolor: "npm:1.1.100"
calculate-size: "npm:1.1.1"
chance: "npm:1.1.11"
classnames: "npm:2.5.1"
common-tags: "npm:1.8.2"
core-js: "npm:3.37.0"
@ -7827,7 +7829,7 @@ __metadata:
languageName: node
linkType: hard
"@types/chance@npm:^1.1.3":
"@types/chance@npm:1.1.6, @types/chance@npm:^1.1.3":
version: 1.1.6
resolution: "@types/chance@npm:1.1.6"
checksum: 10/f4366f1b3144d143af3e6f0fad2ed1db7b9bdfa7d82d40944e9619d57fe7e6b60e8c1452f47a8ededa6b2188932879518628ecd9aac81c40384ded39c26338ba
@ -11639,7 +11641,7 @@ __metadata:
languageName: node
linkType: hard
"chance@npm:^1.0.10":
"chance@npm:1.1.11, chance@npm:^1.0.10":
version: 1.1.11
resolution: "chance@npm:1.1.11"
checksum: 10/d76cc76dbcb837f051e6080d94be8bdcd07559d52077854f614c7081062fee10d4c3b508f6ae4b70303dac000422ea35160b01de165fcd176a01f67ea4b2cef5
@ -16943,7 +16945,7 @@ __metadata:
react-dom: "npm:18.2.0"
react-draggable: "npm:4.4.6"
react-dropzone: "npm:^14.2.3"
react-grid-layout: "npm:1.4.4"
react-grid-layout: "patch:react-grid-layout@npm%3A1.4.4#~/.yarn/patches/react-grid-layout-npm-1.4.4-4024c5395b.patch"
react-highlight-words: "npm:0.20.0"
react-hook-form: "npm:^7.49.2"
react-i18next: "npm:^14.0.0"
@ -20251,13 +20253,6 @@ __metadata:
languageName: node
linkType: hard
"lodash.isequal@npm:^4.0.0":
version: 4.5.0
resolution: "lodash.isequal@npm:4.5.0"
checksum: 10/82fc58a83a1555f8df34ca9a2cd300995ff94018ac12cc47c349655f0ae1d4d92ba346db4c19bbfc90510764e0c00ddcc985a358bdcd4b3b965abf8f2a48a214
languageName: node
linkType: hard
"lodash.ismatch@npm:^4.4.0":
version: 4.4.0
resolution: "lodash.ismatch@npm:4.4.0"
@ -24784,7 +24779,7 @@ __metadata:
languageName: node
linkType: hard
"react-draggable@npm:4.4.6, react-draggable@npm:^4.0.0, react-draggable@npm:^4.0.3, react-draggable@npm:^4.4.5":
"react-draggable@npm:4.4.6, react-draggable@npm:^4.0.3, react-draggable@npm:^4.4.5":
version: 4.4.6
resolution: "react-draggable@npm:4.4.6"
dependencies:
@ -24860,22 +24855,6 @@ __metadata:
languageName: node
linkType: hard
"react-grid-layout@npm:1.3.4":
version: 1.3.4
resolution: "react-grid-layout@npm:1.3.4"
dependencies:
clsx: "npm:^1.1.1"
lodash.isequal: "npm:^4.0.0"
prop-types: "npm:^15.8.1"
react-draggable: "npm:^4.0.0"
react-resizable: "npm:^3.0.4"
peerDependencies:
react: ">= 16.3.0"
react-dom: ">= 16.3.0"
checksum: 10/944ab133e59bfaa5633625f57be9f69133b5cec2de0232d9581e2c988e257ebafe010ee9bbbff6c2754f9d7d8bb0053072bac103f20fc232be2a58e15d14fc64
languageName: node
linkType: hard
"react-grid-layout@npm:1.4.4":
version: 1.4.4
resolution: "react-grid-layout@npm:1.4.4"
@ -24893,6 +24872,23 @@ __metadata:
languageName: node
linkType: hard
"react-grid-layout@patch:react-grid-layout@npm%3A1.4.4#~/.yarn/patches/react-grid-layout-npm-1.4.4-4024c5395b.patch":
version: 1.4.4
resolution: "react-grid-layout@patch:react-grid-layout@npm%3A1.4.4#~/.yarn/patches/react-grid-layout-npm-1.4.4-4024c5395b.patch::version=1.4.4&hash=e69ce6"
dependencies:
clsx: "npm:^2.0.0"
fast-equals: "npm:^4.0.3"
prop-types: "npm:^15.8.1"
react-draggable: "npm:^4.4.5"
react-resizable: "npm:^3.0.5"
resize-observer-polyfill: "npm:^1.5.1"
peerDependencies:
react: ">= 16.3.0"
react-dom: ">= 16.3.0"
checksum: 10/fcc4fe40e38d723870dbc138af9a4a58dff6459efd191d03a66de2e663e169591a66d77b0f377bad780f64f4e24ca41735e4b2ecd99ae7c48071a0b8a1876a25
languageName: node
linkType: hard
"react-highlight-words@npm:0.20.0":
version: 0.20.0
resolution: "react-highlight-words@npm:0.20.0"
@ -25117,7 +25113,7 @@ __metadata:
languageName: node
linkType: hard
"react-resizable@npm:3.0.5, react-resizable@npm:^3.0.4, react-resizable@npm:^3.0.5":
"react-resizable@npm:3.0.5, react-resizable@npm:^3.0.5":
version: 3.0.5
resolution: "react-resizable@npm:3.0.5"
dependencies: