interactionStart('drag', e)}
- onMouseUp={(e) => interactionStart('drop', e)}
- >
-
-
- {/* Resize handle */}
- interactionStart('resize', e)}
- onMouseUp={(e) => interactionStart('drop', e)}
- css={css`
- right: 0;
- bottom: 0;
- opacity: 0;
- margin: -2px;
- position: absolute;
- width: ${euiThemeVars.euiSizeL};
- height: ${euiThemeVars.euiSizeL};
- transition: opacity 0.2s, border 0.2s;
- border-radius: 7px 0 7px 0;
- border-bottom: 2px solid ${euiThemeVars.euiColorSuccess};
- border-right: 2px solid ${euiThemeVars.euiColorSuccess};
- :hover {
- background-color: ${transparentize(euiThemeVars.euiColorSuccess, 0.05)};
- cursor: se-resize;
+ /** Set initial styles based on state at mount to prevent styles from "blipping" */
+ const initialStyles = useMemo(() => {
+ const initialPanel = gridLayoutStateManager.gridLayout$.getValue()[rowIndex].panels[panelId];
+ return css`
+ grid-column-start: ${initialPanel.column + 1};
+ grid-column-end: ${initialPanel.column + 1 + initialPanel.width};
+ grid-row-start: ${initialPanel.row + 1};
+ grid-row-end: ${initialPanel.row + 1 + initialPanel.height};
+ `;
+ }, [gridLayoutStateManager, rowIndex, panelId]);
+
+ useEffect(
+ () => {
+ /** Update the styles of the panel via a subscription to prevent re-renders */
+ const styleSubscription = combineLatest([
+ gridLayoutStateManager.activePanel$,
+ gridLayoutStateManager.gridLayout$,
+ gridLayoutStateManager.runtimeSettings$,
+ ])
+ .pipe(skip(1)) // skip the first emit because the `initialStyles` will take care of it
+ .subscribe(([activePanel, gridLayout, runtimeSettings]) => {
+ const ref = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
+ const panel = gridLayout[rowIndex].panels[panelId];
+ if (!ref || !panel) return;
+
+ const currentInteractionEvent = gridLayoutStateManager.interactionEvent$.getValue();
+ if (panelId === activePanel?.id) {
+ // if the current panel is active, give it fixed positioning depending on the interaction event
+ const { position: draggingPosition } = activePanel;
+
+ ref.style.zIndex = `${euiThemeVars.euiZModal}`;
+ if (currentInteractionEvent?.type === 'resize') {
+ // if the current panel is being resized, ensure it is not shrunk past the size of a single cell
+ ref.style.width = `${Math.max(
+ draggingPosition.right - draggingPosition.left,
+ runtimeSettings.columnPixelWidth
+ )}px`;
+ ref.style.height = `${Math.max(
+ draggingPosition.bottom - draggingPosition.top,
+ runtimeSettings.rowHeight
+ )}px`;
+
+ // undo any "lock to grid" styles **except** for the top left corner, which stays locked
+ ref.style.gridColumnStart = `${panel.column + 1}`;
+ ref.style.gridRowStart = `${panel.row + 1}`;
+ ref.style.gridColumnEnd = ``;
+ ref.style.gridRowEnd = ``;
+ } else {
+ // if the current panel is being dragged, render it with a fixed position + size
+ ref.style.position = 'fixed';
+ ref.style.left = `${draggingPosition.left}px`;
+ ref.style.top = `${draggingPosition.top}px`;
+ ref.style.width = `${draggingPosition.right - draggingPosition.left}px`;
+ ref.style.height = `${draggingPosition.bottom - draggingPosition.top}px`;
+
+ // undo any "lock to grid" styles
+ ref.style.gridColumnStart = ``;
+ ref.style.gridRowStart = ``;
+ ref.style.gridColumnEnd = ``;
+ ref.style.gridRowEnd = ``;
+ }
+ } else {
+ ref.style.zIndex = '0';
+
+ // if the panel is not being dragged and/or resized, undo any fixed position styles
+ ref.style.position = '';
+ ref.style.left = ``;
+ ref.style.top = ``;
+ ref.style.width = ``;
+ ref.style.height = ``;
+
+ // and render the panel locked to the grid
+ ref.style.gridColumnStart = `${panel.column + 1}`;
+ ref.style.gridColumnEnd = `${panel.column + 1 + panel.width}`;
+ ref.style.gridRowStart = `${panel.row + 1}`;
+ ref.style.gridRowEnd = `${panel.row + 1 + panel.height}`;
}
- `}
- />
-
{
+ styleSubscription.unsubscribe();
+ };
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ []
+ );
+
+ return (
+
+
- {renderPanelContents(panelData.id)}
-
-
-
- );
-});
+ {/* drag handle */}
+
interactionStart('drag', e)}
+ onMouseUp={(e) => interactionStart('drop', e)}
+ >
+
+
+ {/* Resize handle */}
+
interactionStart('resize', e)}
+ onMouseUp={(e) => interactionStart('drop', e)}
+ css={css`
+ right: 0;
+ bottom: 0;
+ opacity: 0;
+ margin: -2px;
+ position: absolute;
+ width: ${euiThemeVars.euiSizeL};
+ height: ${euiThemeVars.euiSizeL};
+ transition: opacity 0.2s, border 0.2s;
+ border-radius: 7px 0 7px 0;
+ border-bottom: 2px solid ${euiThemeVars.euiColorSuccess};
+ border-right: 2px solid ${euiThemeVars.euiColorSuccess};
+ :hover {
+ background-color: ${transparentize(euiThemeVars.euiColorSuccess, 0.05)};
+ cursor: se-resize;
+ }
+ `}
+ />
+
+ {renderPanelContents(panelId)}
+
+
+
+ );
+ }
+);
diff --git a/packages/kbn-grid-layout/grid/grid_row.tsx b/packages/kbn-grid-layout/grid/grid_row.tsx
index 917f661c91740..e797cd570550a 100644
--- a/packages/kbn-grid-layout/grid/grid_row.tsx
+++ b/packages/kbn-grid-layout/grid/grid_row.tsx
@@ -7,41 +7,23 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import React, { forwardRef, useMemo, useRef } from 'react';
+import React, { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
+import { combineLatest, map, pairwise, skip } from 'rxjs';
import { EuiButtonIcon, EuiFlexGroup, EuiSpacer, EuiTitle, transparentize } from '@elastic/eui';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import { euiThemeVars } from '@kbn/ui-theme';
-import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
+import { DragPreview } from './drag_preview';
import { GridPanel } from './grid_panel';
-import {
- GridLayoutStateManager,
- GridRowData,
- PanelInteractionEvent,
- RuntimeGridSettings,
-} from './types';
-
-const gridColor = transparentize(euiThemeVars.euiColorSuccess, 0.2);
-const getGridBackgroundCSS = (settings: RuntimeGridSettings) => {
- const { gutterSize, columnPixelWidth, rowHeight } = settings;
- return css`
- background-position: top -${gutterSize / 2}px left -${gutterSize / 2}px;
- background-size: ${columnPixelWidth + gutterSize}px ${rowHeight + gutterSize}px;
- background-image: linear-gradient(to right, ${gridColor} 1px, transparent 1px),
- linear-gradient(to bottom, ${gridColor} 1px, transparent 1px);
- `;
-};
+import { GridLayoutStateManager, GridRowData, PanelInteractionEvent } from './types';
export const GridRow = forwardRef<
HTMLDivElement,
{
rowIndex: number;
- rowData: GridRowData;
toggleIsCollapsed: () => void;
- targetRowIndex: number | undefined;
- runtimeSettings: RuntimeGridSettings;
renderPanelContents: (panelId: string) => React.ReactNode;
setInteractionEvent: (interactionData?: PanelInteractionEvent) => void;
gridLayoutStateManager: GridLayoutStateManager;
@@ -49,10 +31,7 @@ export const GridRow = forwardRef<
>(
(
{
- rowData,
rowIndex,
- targetRowIndex,
- runtimeSettings,
toggleIsCollapsed,
renderPanelContents,
setInteractionEvent,
@@ -60,19 +39,122 @@ export const GridRow = forwardRef<
},
gridRef
) => {
- const dragPreviewRef = useRef
(null);
- const activePanel = useStateFromPublishingSubject(gridLayoutStateManager.activePanel$);
+ const currentRow = gridLayoutStateManager.gridLayout$.value[rowIndex];
+ const [panelIds, setPanelIds] = useState(Object.keys(currentRow.panels));
+ const [rowTitle, setRowTitle] = useState(currentRow.title);
+ const [isCollapsed, setIsCollapsed] = useState(currentRow.isCollapsed);
- const { gutterSize, columnCount, rowHeight } = runtimeSettings;
- const isGridTargeted = activePanel?.id && targetRowIndex === rowIndex;
+ const getRowCount = useCallback(
+ (row: GridRowData) => {
+ const maxRow = Object.values(row.panels).reduce((acc, panel) => {
+ return Math.max(acc, panel.row + panel.height);
+ }, 0);
+ return maxRow || 1;
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [rowIndex]
+ );
+
+ /** Set initial styles based on state at mount to prevent styles from "blipping" */
+ const initialStyles = useMemo(() => {
+ const initialRow = gridLayoutStateManager.gridLayout$.getValue()[rowIndex];
+ const runtimeSettings = gridLayoutStateManager.runtimeSettings$.getValue();
+ const { gutterSize, columnCount, rowHeight } = runtimeSettings;
+
+ return css`
+ gap: ${gutterSize}px;
+ grid-template-columns: repeat(
+ ${columnCount},
+ calc((100% - ${gutterSize * (columnCount - 1)}px) / ${columnCount})
+ );
+ grid-template-rows: repeat(${getRowCount(initialRow)}, ${rowHeight}px);
+ `;
+ }, [gridLayoutStateManager, getRowCount, rowIndex]);
+
+ useEffect(
+ () => {
+ /** Update the styles of the grid row via a subscription to prevent re-renders */
+ const styleSubscription = combineLatest([
+ gridLayoutStateManager.interactionEvent$,
+ gridLayoutStateManager.gridLayout$,
+ gridLayoutStateManager.runtimeSettings$,
+ ])
+ .pipe(skip(1)) // skip the first emit because the `initialStyles` will take care of it
+ .subscribe(([interactionEvent, gridLayout, runtimeSettings]) => {
+ const rowRef = gridLayoutStateManager.rowRefs.current[rowIndex];
+ if (!rowRef) return;
+
+ const { gutterSize, rowHeight, columnPixelWidth } = runtimeSettings;
- // calculate row count based on the number of rows needed to fit all panels
- const rowCount = useMemo(() => {
- const maxRow = Object.values(rowData.panels).reduce((acc, panel) => {
- return Math.max(acc, panel.row + panel.height);
- }, 0);
- return maxRow || 1;
- }, [rowData]);
+ rowRef.style.gridTemplateRows = `repeat(${getRowCount(
+ gridLayout[rowIndex]
+ )}, ${rowHeight}px)`;
+
+ const targetRow = interactionEvent?.targetRowIndex;
+ if (rowIndex === targetRow && interactionEvent?.type !== 'drop') {
+ // apply "targetted row" styles
+ const gridColor = transparentize(euiThemeVars.euiColorSuccess, 0.2);
+ rowRef.style.backgroundPosition = `top -${gutterSize / 2}px left -${
+ gutterSize / 2
+ }px`;
+ rowRef.style.backgroundSize = ` ${columnPixelWidth + gutterSize}px ${
+ rowHeight + gutterSize
+ }px`;
+ rowRef.style.backgroundImage = `linear-gradient(to right, ${gridColor} 1px, transparent 1px),
+ linear-gradient(to bottom, ${gridColor} 1px, transparent 1px)`;
+ rowRef.style.backgroundColor = `${transparentize(
+ euiThemeVars.euiColorSuccess,
+ 0.05
+ )}`;
+ } else {
+ // undo any "targetted row" styles
+ rowRef.style.backgroundPosition = ``;
+ rowRef.style.backgroundSize = ``;
+ rowRef.style.backgroundImage = ``;
+ rowRef.style.backgroundColor = `transparent`;
+ }
+ });
+
+ /**
+ * The things that should trigger a re-render are title, collapsed state, and panel ids - panel positions
+ * are being controlled via CSS styles, so they do not need to trigger a re-render. This subscription ensures
+ * that the row will re-render when one of those three things changes.
+ */
+ const rowStateSubscription = gridLayoutStateManager.gridLayout$
+ .pipe(
+ skip(1), // we are initializing all row state with a value, so skip the initial emit
+ map((gridLayout) => {
+ return {
+ title: gridLayout[rowIndex].title,
+ isCollapsed: gridLayout[rowIndex].isCollapsed,
+ panelIds: Object.keys(gridLayout[rowIndex].panels),
+ };
+ }),
+ pairwise()
+ )
+ .subscribe(([oldRowData, newRowData]) => {
+ if (oldRowData.title !== newRowData.title) setRowTitle(newRowData.title);
+ if (oldRowData.isCollapsed !== newRowData.isCollapsed)
+ setIsCollapsed(newRowData.isCollapsed);
+ if (
+ oldRowData.panelIds.length !== newRowData.panelIds.length ||
+ !(
+ oldRowData.panelIds.every((p) => newRowData.panelIds.includes(p)) &&
+ newRowData.panelIds.every((p) => oldRowData.panelIds.includes(p))
+ )
+ ) {
+ setPanelIds(newRowData.panelIds);
+ }
+ });
+
+ return () => {
+ styleSubscription.unsubscribe();
+ rowStateSubscription.unsubscribe();
+ };
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [rowIndex]
+ );
return (
<>
@@ -85,51 +167,43 @@ export const GridRow = forwardRef<
aria-label={i18n.translate('kbnGridLayout.row.toggleCollapse', {
defaultMessage: 'Toggle collapse',
})}
- iconType={rowData.isCollapsed ? 'arrowRight' : 'arrowDown'}
+ iconType={isCollapsed ? 'arrowRight' : 'arrowDown'}
onClick={toggleIsCollapsed}
/>
- {rowData.title}
+ {rowTitle}
>
)}
- {!rowData.isCollapsed && (
+ {!isCollapsed && (
- {Object.values(rowData.panels).map((panelData) => (
+ {panelIds.map((panelId) => (
{
e.preventDefault();
e.stopPropagation();
- const panelRef = gridLayoutStateManager.panelRefs.current[rowIndex][panelData.id];
+ const panelRef = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
if (!panelRef) return;
const panelRect = panelRef.getBoundingClientRect();
setInteractionEvent({
type,
- id: panelData.id,
+ id: panelId,
panelDiv: panelRef,
targetRowIndex: rowIndex,
mouseOffsets: {
@@ -144,32 +218,12 @@ export const GridRow = forwardRef<
if (!gridLayoutStateManager.panelRefs.current[rowIndex]) {
gridLayoutStateManager.panelRefs.current[rowIndex] = {};
}
- gridLayoutStateManager.panelRefs.current[rowIndex][panelData.id] = element;
+ gridLayoutStateManager.panelRefs.current[rowIndex][panelId] = element;
}}
/>
))}
- {/* render the drag preview if this row is currently being targetted */}
- {isGridTargeted && (
-
- )}
+
)}
>
diff --git a/packages/kbn-grid-layout/grid/use_grid_layout_events.ts b/packages/kbn-grid-layout/grid/use_grid_layout_events.ts
index dfbd013d3642a..bd6343b9e5652 100644
--- a/packages/kbn-grid-layout/grid/use_grid_layout_events.ts
+++ b/packages/kbn-grid-layout/grid/use_grid_layout_events.ts
@@ -8,6 +8,7 @@
*/
import { useEffect, useRef } from 'react';
+import deepEqual from 'fast-deep-equal';
import { resolveGridRow } from './resolve_grid_row';
import { GridLayoutStateManager, GridPanelData } from './types';
@@ -121,6 +122,7 @@ export const useGridLayoutEvents = ({
maxColumn
);
const targetRow = Math.max(Math.round(localYCoordinate / (rowHeight + gutterSize)), 0);
+
const requestedGridData = { ...currentGridData };
if (isResize) {
requestedGridData.width = Math.max(targetColumn - requestedGridData.column, 1);
@@ -154,8 +156,9 @@ export const useGridLayoutEvents = ({
const resolvedOriginGrid = resolveGridRow(originGrid);
nextLayout[lastRowIndex] = resolvedOriginGrid;
}
-
- gridLayout$.next(nextLayout);
+ if (!deepEqual(currentLayout, nextLayout)) {
+ gridLayout$.next(nextLayout);
+ }
}
};
diff --git a/packages/kbn-grid-layout/grid/use_grid_layout_state.ts b/packages/kbn-grid-layout/grid/use_grid_layout_state.ts
index cdb99a9ebbfd0..fe657ae253107 100644
--- a/packages/kbn-grid-layout/grid/use_grid_layout_state.ts
+++ b/packages/kbn-grid-layout/grid/use_grid_layout_state.ts
@@ -7,10 +7,11 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import { i18n } from '@kbn/i18n';
import { useEffect, useMemo, useRef } from 'react';
-import { BehaviorSubject, combineLatest, debounceTime, map, retry } from 'rxjs';
+import { BehaviorSubject, debounceTime } from 'rxjs';
+
import useResizeObserver, { type ObservedSize } from 'use-resize-observer/polyfilled';
+
import {
ActivePanel,
GridLayoutData,
@@ -43,10 +44,14 @@ export const useGridLayoutState = ({
...gridSettings,
columnPixelWidth: 0,
});
+ const panelIds$ = new BehaviorSubject(
+ initialLayout.map(({ panels }) => Object.keys(panels))
+ );
return {
rowRefs,
panelRefs,
+ panelIds$,
gridLayout$,
activePanel$,
gridDimensions$,
@@ -67,117 +72,12 @@ export const useGridLayoutState = ({
const columnPixelWidth =
(elementWidth - gridSettings.gutterSize * (gridSettings.columnCount - 1)) /
gridSettings.columnCount;
- gridLayoutStateManager.runtimeSettings$.next({ ...gridSettings, columnPixelWidth });
- });
-
- /**
- * on layout change, update the styles of every panel so that it renders as expected
- */
- const onLayoutChangeSubscription = combineLatest([
- gridLayoutStateManager.gridLayout$,
- gridLayoutStateManager.activePanel$,
- ])
- .pipe(
- map(([gridLayout, activePanel]) => {
- // wait for all panel refs to be ready before continuing
- for (let rowIndex = 0; rowIndex < gridLayout.length; rowIndex++) {
- const currentRow = gridLayout[rowIndex];
- Object.keys(currentRow.panels).forEach((key) => {
- const panelRef = gridLayoutStateManager.panelRefs.current[rowIndex][key];
- if (!panelRef && !currentRow.isCollapsed) {
- throw new Error(
- i18n.translate('kbnGridLayout.panelRefNotFoundError', {
- defaultMessage: 'Panel reference does not exist', // the retry will catch this error
- })
- );
- }
- });
- }
- return { gridLayout, activePanel };
- }),
- retry({ delay: 10 }) // retry until panel references all exist
- )
- .subscribe(({ gridLayout, activePanel }) => {
- const runtimeSettings = gridLayoutStateManager.runtimeSettings$.getValue();
- const currentInteractionEvent = gridLayoutStateManager.interactionEvent$.getValue();
- for (let rowIndex = 0; rowIndex < gridLayout.length; rowIndex++) {
- if (activePanel && rowIndex !== currentInteractionEvent?.targetRowIndex) {
- /**
- * If there is an interaction event happening but the current row is not being targetted, it
- * does not need to be re-rendered; so, skip setting the panel styles of this row.
- *
- * If there is **no** interaction event, then this is the initial render so the styles of every
- * panel should be initialized; so, don't skip setting the panel styles.
- */
- continue;
- }
-
- // re-render the targetted row
- const currentRow = gridLayout[rowIndex];
- Object.keys(currentRow.panels).forEach((key) => {
- const panel = currentRow.panels[key];
- const panelRef = gridLayoutStateManager.panelRefs.current[rowIndex][key];
- if (!panelRef) {
- return;
- }
-
- const isResize = currentInteractionEvent?.type === 'resize';
- if (panel.id === activePanel?.id) {
- // if the current panel is active, give it fixed positioning depending on the interaction event
- const { position: draggingPosition } = activePanel;
-
- if (isResize) {
- // if the current panel is being resized, ensure it is not shrunk past the size of a single cell
- panelRef.style.width = `${Math.max(
- draggingPosition.right - draggingPosition.left,
- runtimeSettings.columnPixelWidth
- )}px`;
- panelRef.style.height = `${Math.max(
- draggingPosition.bottom - draggingPosition.top,
- runtimeSettings.rowHeight
- )}px`;
-
- // undo any "lock to grid" styles **except** for the top left corner, which stays locked
- panelRef.style.gridColumnStart = `${panel.column + 1}`;
- panelRef.style.gridRowStart = `${panel.row + 1}`;
- panelRef.style.gridColumnEnd = ``;
- panelRef.style.gridRowEnd = ``;
- } else {
- // if the current panel is being dragged, render it with a fixed position + size
- panelRef.style.position = 'fixed';
- panelRef.style.left = `${draggingPosition.left}px`;
- panelRef.style.top = `${draggingPosition.top}px`;
- panelRef.style.width = `${draggingPosition.right - draggingPosition.left}px`;
- panelRef.style.height = `${draggingPosition.bottom - draggingPosition.top}px`;
-
- // undo any "lock to grid" styles
- panelRef.style.gridColumnStart = ``;
- panelRef.style.gridRowStart = ``;
- panelRef.style.gridColumnEnd = ``;
- panelRef.style.gridRowEnd = ``;
- }
- } else {
- // if the panel is not being dragged and/or resized, undo any fixed position styles
- panelRef.style.position = '';
- panelRef.style.left = ``;
- panelRef.style.top = ``;
- panelRef.style.width = ``;
- panelRef.style.height = ``;
-
- // and render the panel locked to the grid
- panelRef.style.gridColumnStart = `${panel.column + 1}`;
- panelRef.style.gridColumnEnd = `${panel.column + 1 + panel.width}`;
- panelRef.style.gridRowStart = `${panel.row + 1}`;
- panelRef.style.gridRowEnd = `${panel.row + 1 + panel.height}`;
- }
- });
- }
+ gridLayoutStateManager.runtimeSettings$.next({ ...gridSettings, columnPixelWidth });
});
return () => {
resizeSubscription.unsubscribe();
- onLayoutChangeSubscription.unsubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
diff --git a/packages/kbn-grid-layout/tsconfig.json b/packages/kbn-grid-layout/tsconfig.json
index 5a1888db0dc64..f0dd3232a42d5 100644
--- a/packages/kbn-grid-layout/tsconfig.json
+++ b/packages/kbn-grid-layout/tsconfig.json
@@ -17,7 +17,6 @@
"target/**/*"
],
"kbn_references": [
- "@kbn/presentation-publishing",
"@kbn/ui-theme",
"@kbn/i18n",
]
diff --git a/packages/kbn-management/settings/setting_ids/index.ts b/packages/kbn-management/settings/setting_ids/index.ts
index e926007f77f25..b146be6f6e252 100644
--- a/packages/kbn-management/settings/setting_ids/index.ts
+++ b/packages/kbn-management/settings/setting_ids/index.ts
@@ -83,7 +83,6 @@ export const DISCOVER_SAMPLE_SIZE_ID = 'discover:sampleSize';
export const DISCOVER_SEARCH_FIELDS_FROM_SOURCE_ID = 'discover:searchFieldsFromSource';
export const DISCOVER_SEARCH_ON_PAGE_LOAD_ID = 'discover:searchOnPageLoad';
export const DISCOVER_SHOW_FIELD_STATISTICS_ID = 'discover:showFieldStatistics';
-export const DISCOVER_SHOW_LEGACY_FIELD_TOP_VALUES_ID = 'discover:showLegacyFieldTopValues';
export const DISCOVER_SHOW_MULTI_FIELDS_ID = 'discover:showMultiFields';
export const DISCOVER_SORT_DEFAULT_ORDER_ID = 'discover:sort:defaultOrder';
export const DOC_TABLE_HIDE_TIME_COLUMNS_ID = 'doc_table:hideTimeColumn';
@@ -122,10 +121,6 @@ export const OBSERVABILITY_APM_TRACE_EXPLORER_TAB_ID = 'observability:apmTraceEx
export const OBSERVABILITY_ENABLE_AWS_LAMBDA_METRICS_ID = 'observability:enableAwsLambdaMetrics';
export const OBSERVABILITY_ENABLE_COMPARISON_BY_DEFAULT_ID =
'observability:enableComparisonByDefault';
-export const OBSERVABILITY_ENABLE_INFRASTRUCTURE_HOSTS_VIEW_ID =
- 'observability:enableInfrastructureHostsView';
-export const OBSERVABILITY_ENABLE_INFRASTRUCTURE_CONTAINER_ASSET_VIEW_ID =
- 'observability:enableInfrastructureContainerAssetView';
export const OBSERVABILITY_ENABLE_INFRASTRUCTURE_ASSET_CUSTOM_DASHBOARDS_ID =
'observability:enableInfrastructureAssetCustomDashboards';
export const OBSERVABILITY_ENABLE_INSPECT_ES_QUERIES_ID = 'observability:enableInspectEsQueries';
diff --git a/packages/kbn-router-to-openapispec/src/extract_authz_description.test.ts b/packages/kbn-router-to-openapispec/src/extract_authz_description.test.ts
new file mode 100644
index 0000000000000..8da2324e68f02
--- /dev/null
+++ b/packages/kbn-router-to-openapispec/src/extract_authz_description.test.ts
@@ -0,0 +1,81 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { schema } from '@kbn/config-schema';
+import { extractAuthzDescription } from './extract_authz_description';
+import { InternalRouterRoute } from './type';
+import { RouteSecurity } from '@kbn/core-http-server';
+
+describe('extractAuthzDescription', () => {
+ it('should return empty if route does not require privileges', () => {
+ const route: InternalRouterRoute = {
+ path: '/foo',
+ options: { access: 'internal' },
+ handler: jest.fn(),
+ validationSchemas: { request: { body: schema.object({}) } },
+ method: 'get',
+ isVersioned: false,
+ };
+ const description = extractAuthzDescription(route.security);
+ expect(description).toBe('');
+ });
+
+ it('should return route authz description for simple privileges', () => {
+ const routeSecurity: RouteSecurity = {
+ authz: {
+ requiredPrivileges: ['manage_spaces'],
+ },
+ };
+ const description = extractAuthzDescription(routeSecurity);
+ expect(description).toBe('[Authz] Route required privileges: ALL of [manage_spaces].');
+ });
+
+ it('should return route authz description for privilege groups', () => {
+ {
+ const routeSecurity: RouteSecurity = {
+ authz: {
+ requiredPrivileges: [{ allRequired: ['console'] }],
+ },
+ };
+ const description = extractAuthzDescription(routeSecurity);
+ expect(description).toBe('[Authz] Route required privileges: ALL of [console].');
+ }
+ {
+ const routeSecurity: RouteSecurity = {
+ authz: {
+ requiredPrivileges: [
+ {
+ anyRequired: ['manage_spaces', 'taskmanager'],
+ },
+ ],
+ },
+ };
+ const description = extractAuthzDescription(routeSecurity);
+ expect(description).toBe(
+ '[Authz] Route required privileges: ANY of [manage_spaces OR taskmanager].'
+ );
+ }
+ {
+ const routeSecurity: RouteSecurity = {
+ authz: {
+ requiredPrivileges: [
+ {
+ allRequired: ['console', 'filesManagement'],
+ anyRequired: ['manage_spaces', 'taskmanager'],
+ },
+ ],
+ },
+ };
+ const description = extractAuthzDescription(routeSecurity);
+ expect(description).toBe(
+ '[Authz] Route required privileges: ALL of [console, filesManagement] AND ANY of [manage_spaces OR taskmanager].'
+ );
+ }
+ });
+});
diff --git a/packages/kbn-router-to-openapispec/src/extract_authz_description.ts b/packages/kbn-router-to-openapispec/src/extract_authz_description.ts
new file mode 100644
index 0000000000000..4cd6875913780
--- /dev/null
+++ b/packages/kbn-router-to-openapispec/src/extract_authz_description.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type { AuthzEnabled, AuthzDisabled, InternalRouteSecurity } from '@kbn/core-http-server';
+
+interface PrivilegeGroupValue {
+ allRequired: string[];
+ anyRequired: string[];
+}
+
+export const extractAuthzDescription = (routeSecurity: InternalRouteSecurity | undefined) => {
+ if (!routeSecurity) {
+ return '';
+ }
+ if (!('authz' in routeSecurity) || (routeSecurity.authz as AuthzDisabled).enabled === false) {
+ return '';
+ }
+
+ const privileges = (routeSecurity.authz as AuthzEnabled).requiredPrivileges;
+
+ const groupedPrivileges = privileges.reduce(
+ (groups, privilege) => {
+ if (typeof privilege === 'string') {
+ groups.allRequired.push(privilege);
+
+ return groups;
+ }
+ groups.allRequired.push(...(privilege.allRequired ?? []));
+ groups.anyRequired.push(...(privilege.anyRequired ?? []));
+
+ return groups;
+ },
+ {
+ anyRequired: [],
+ allRequired: [],
+ }
+ );
+
+ const getPrivilegesDescription = (allRequired: string[], anyRequired: string[]) => {
+ const allDescription = allRequired.length ? `ALL of [${allRequired.join(', ')}]` : '';
+ const anyDescription = anyRequired.length ? `ANY of [${anyRequired.join(' OR ')}]` : '';
+
+ return `${allDescription}${allDescription && anyDescription ? ' AND ' : ''}${anyDescription}`;
+ };
+
+ const getDescriptionForRoute = () => {
+ const allRequired = [...groupedPrivileges.allRequired];
+ const anyRequired = [...groupedPrivileges.anyRequired];
+
+ return `Route required privileges: ${getPrivilegesDescription(allRequired, anyRequired)}.`;
+ };
+
+ return `[Authz] ${getDescriptionForRoute()}`;
+};
diff --git a/packages/kbn-router-to-openapispec/src/process_router.test.ts b/packages/kbn-router-to-openapispec/src/process_router.test.ts
index 22e03efdf08fc..96a10b25d648a 100644
--- a/packages/kbn-router-to-openapispec/src/process_router.test.ts
+++ b/packages/kbn-router-to-openapispec/src/process_router.test.ts
@@ -11,7 +11,8 @@ import { schema } from '@kbn/config-schema';
import { Router } from '@kbn/core-http-router-server-internal';
import { OasConverter } from './oas_converter';
import { createOperationIdCounter } from './operation_id_counter';
-import { extractResponses, processRouter, type InternalRouterRoute } from './process_router';
+import { extractResponses, processRouter } from './process_router';
+import { type InternalRouterRoute } from './type';
describe('extractResponses', () => {
let oasConverter: OasConverter;
@@ -102,6 +103,24 @@ describe('processRouter', () => {
handler: jest.fn(),
validationSchemas: { request: { body: schema.object({}) } },
},
+ {
+ path: '/qux',
+ method: 'post',
+ options: {},
+ handler: jest.fn(),
+ validationSchemas: { request: { body: schema.object({}) } },
+ security: {
+ authz: {
+ requiredPrivileges: [
+ 'manage_spaces',
+ {
+ allRequired: ['taskmanager'],
+ anyRequired: ['console'],
+ },
+ ],
+ },
+ },
+ },
],
} as unknown as Router;
@@ -110,11 +129,23 @@ describe('processRouter', () => {
version: '2023-10-31',
});
- expect(Object.keys(result1.paths!)).toHaveLength(3);
+ expect(Object.keys(result1.paths!)).toHaveLength(4);
const result2 = processRouter(testRouter, new OasConverter(), createOperationIdCounter(), {
version: '2024-10-31',
});
expect(Object.keys(result2.paths!)).toHaveLength(0);
});
+
+ it('updates description with privileges required', () => {
+ const result = processRouter(testRouter, new OasConverter(), createOperationIdCounter(), {
+ version: '2023-10-31',
+ });
+
+ expect(result.paths['/qux']?.post).toBeDefined();
+
+ expect(result.paths['/qux']?.post?.description).toEqual(
+ '[Authz] Route required privileges: ALL of [manage_spaces, taskmanager] AND ANY of [console].'
+ );
+ });
});
diff --git a/packages/kbn-router-to-openapispec/src/process_router.ts b/packages/kbn-router-to-openapispec/src/process_router.ts
index f0d37fd208b7b..cb55af3735b34 100644
--- a/packages/kbn-router-to-openapispec/src/process_router.ts
+++ b/packages/kbn-router-to-openapispec/src/process_router.ts
@@ -27,7 +27,8 @@ import {
} from './util';
import type { OperationIdCounter } from './operation_id_counter';
import type { GenerateOpenApiDocumentOptionsFilters } from './generate_oas';
-import type { CustomOperationObject } from './type';
+import type { CustomOperationObject, InternalRouterRoute } from './type';
+import { extractAuthzDescription } from './extract_authz_description';
export const processRouter = (
appRouter: Router,
@@ -63,10 +64,19 @@ export const processRouter = (
parameters.push(...pathObjects, ...queryObjects);
}
+ let description = '';
+ if (route.security) {
+ const authzDescription = extractAuthzDescription(route.security);
+
+ description = `${route.options.description ?? ''}${authzDescription ?? ''}`;
+ }
+
const hasDeprecations = !!route.options.deprecated;
+
const operation: CustomOperationObject = {
summary: route.options.summary ?? '',
tags: route.options.tags ? extractTags(route.options.tags) : [],
+ ...(description ? { description } : {}),
...(route.options.description ? { description: route.options.description } : {}),
...(hasDeprecations ? { deprecated: true } : {}),
...(route.options.discontinued ? { 'x-discontinued': route.options.discontinued } : {}),
@@ -99,7 +109,6 @@ export const processRouter = (
return { paths };
};
-export type InternalRouterRoute = ReturnType[0];
export const extractResponses = (route: InternalRouterRoute, converter: OasConverter) => {
const responses: OpenAPIV3.ResponsesObject = {};
if (!route.validationSchemas) return responses;
diff --git a/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts b/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts
index f9f4f4898c1d0..3738c207f1f78 100644
--- a/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts
+++ b/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts
@@ -144,6 +144,22 @@ describe('processVersionedRouter', () => {
'application/test+json; Elastic-Api-Version=2023-10-31',
]);
});
+
+ it('correctly updates the authz description for routes that require privileges', () => {
+ const results = processVersionedRouter(
+ { getRoutes: () => [createTestRoute()] } as unknown as CoreVersionedRouter,
+ new OasConverter(),
+ createOperationIdCounter(),
+ {}
+ );
+ expect(results.paths['/foo']).toBeDefined();
+
+ expect(results.paths['/foo']!.get).toBeDefined();
+
+ expect(results.paths['/foo']!.get!.description).toBe(
+ '[Authz] Route required privileges: ALL of [manage_spaces].'
+ );
+ });
});
const createTestRoute: () => VersionedRouterRoute = () => ({
@@ -155,6 +171,11 @@ const createTestRoute: () => VersionedRouterRoute = () => ({
deprecated: true,
discontinued: 'discontinued versioned router',
options: { body: { access: ['application/test+json'] } as any },
+ security: {
+ authz: {
+ requiredPrivileges: ['manage_spaces'],
+ },
+ },
},
handlers: [
{
diff --git a/packages/kbn-router-to-openapispec/src/process_versioned_router.ts b/packages/kbn-router-to-openapispec/src/process_versioned_router.ts
index 7eee0d20c11d2..5dad5677c94ac 100644
--- a/packages/kbn-router-to-openapispec/src/process_versioned_router.ts
+++ b/packages/kbn-router-to-openapispec/src/process_versioned_router.ts
@@ -14,6 +14,7 @@ import {
} from '@kbn/core-http-router-server-internal';
import type { RouteMethod, VersionedRouterRoute } from '@kbn/core-http-server';
import type { OpenAPIV3 } from 'openapi-types';
+import { extractAuthzDescription } from './extract_authz_description';
import type { GenerateOpenApiDocumentOptionsFilters } from './generate_oas';
import type { OasConverter } from './oas_converter';
import { isReferenceObject } from './oas_converter/common';
@@ -90,6 +91,13 @@ export const processVersionedRouter = (
];
}
+ let description = '';
+ if (route.options.security) {
+ const authzDescription = extractAuthzDescription(route.options.security);
+
+ description = `${route.options.description ?? ''}${authzDescription ?? ''}`;
+ }
+
const hasBody = Boolean(extractValidationSchemaFromVersionedHandler(handler)?.request?.body);
const contentType = extractContentType(route.options.options?.body);
const hasVersionFilter = Boolean(filters?.version);
@@ -98,6 +106,7 @@ export const processVersionedRouter = (
const operation: OpenAPIV3.OperationObject = {
summary: route.options.summary ?? '',
tags: route.options.options?.tags ? extractTags(route.options.options.tags) : [],
+ ...(description ? { description } : {}),
...(route.options.description ? { description: route.options.description } : {}),
...(hasDeprecations ? { deprecated: true } : {}),
...(route.options.discontinued ? { 'x-discontinued': route.options.discontinued } : {}),
diff --git a/packages/kbn-router-to-openapispec/src/type.ts b/packages/kbn-router-to-openapispec/src/type.ts
index 5c5f992a0de0f..f57e4d00ad7db 100644
--- a/packages/kbn-router-to-openapispec/src/type.ts
+++ b/packages/kbn-router-to-openapispec/src/type.ts
@@ -7,6 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+import type { Router } from '@kbn/core-http-router-server-internal';
import type { OpenAPIV3 } from '../openapi-types';
export type { OpenAPIV3 } from '../openapi-types';
export interface KnownParameters {
@@ -39,3 +40,5 @@ export type CustomOperationObject = OpenAPIV3.OperationObject<{
// Custom OpenAPI from ES API spec based on @availability
'x-state'?: 'Technical Preview' | 'Beta';
}>;
+
+export type InternalRouterRoute = ReturnType[0];
diff --git a/packages/kbn-router-to-openapispec/tsconfig.json b/packages/kbn-router-to-openapispec/tsconfig.json
index d82ca0bf48910..3536a90a8256f 100644
--- a/packages/kbn-router-to-openapispec/tsconfig.json
+++ b/packages/kbn-router-to-openapispec/tsconfig.json
@@ -17,6 +17,6 @@
"@kbn/core-http-router-server-internal",
"@kbn/core-http-server",
"@kbn/config-schema",
- "@kbn/zod"
+ "@kbn/zod",
]
}
diff --git a/packages/kbn-rrule/rrule.ts b/packages/kbn-rrule/rrule.ts
index 9b5eea7d4979f..43e89ee209cb7 100644
--- a/packages/kbn-rrule/rrule.ts
+++ b/packages/kbn-rrule/rrule.ts
@@ -16,6 +16,7 @@ export enum Frequency {
DAILY = 3,
HOURLY = 4,
MINUTELY = 5,
+ SECONDLY = 6,
}
export enum Weekday {
@@ -270,6 +271,13 @@ export const getNextRecurrences = function ({
...opts,
});
}
+ case Frequency.SECONDLY: {
+ const nextRef = moment(refDT).add(interval, 's');
+ return getMinuteOfRecurrences({
+ refDT: nextRef,
+ ...opts,
+ });
+ }
}
};
diff --git a/packages/kbn-search-connectors/components/configuration/connector_configuration_field.tsx b/packages/kbn-search-connectors/components/configuration/connector_configuration_field.tsx
index a43163caa6091..08b91ffc1842c 100644
--- a/packages/kbn-search-connectors/components/configuration/connector_configuration_field.tsx
+++ b/packages/kbn-search-connectors/components/configuration/connector_configuration_field.tsx
@@ -52,7 +52,7 @@ export const ConfigInputField: React.FC = ({
isLoading,
validateAndSetConfigValue,
}) => {
- const { isValid, required, placeholder, value } = configEntry;
+ const { isValid, required, placeholder, value, label } = configEntry;
const [innerValue, setInnerValue] = useState(value);
return (
= ({
validateAndSetConfigValue(event.target.value);
}}
placeholder={placeholder}
+ aria-label={label}
/>
);
};
@@ -74,7 +75,7 @@ export const ConfigInputTextArea: React.FC = ({
configEntry,
validateAndSetConfigValue,
}) => {
- const { isValid, required, placeholder, value } = configEntry;
+ const { isValid, required, placeholder, value, label } = configEntry;
const [innerValue, setInnerValue] = useState(value);
return (
= ({
validateAndSetConfigValue(event.target.value);
}}
placeholder={placeholder}
+ aria-label={label}
/>
);
};
@@ -129,7 +131,7 @@ export const ConfigInputPassword: React.FC = ({
configEntry,
validateAndSetConfigValue,
}) => {
- const { required, value } = configEntry;
+ const { required, value, label } = configEntry;
const [innerValue, setInnerValue] = useState(value);
return (
= ({
setInnerValue(event.target.value);
validateAndSetConfigValue(event.target.value);
}}
+ aria-label={label}
/>
);
};
@@ -170,6 +173,7 @@ export const ConnectorConfigurationField: React.FC {
validateAndSetConfigValue(event.target.value);
}}
+ aria-label={label}
/>
) : (
{
validateAndSetConfigValue(id);
}}
+ aria-label={label}
/>
);
@@ -227,6 +232,7 @@ export const ConnectorConfigurationField: React.FC {
validateAndSetConfigValue(event.target.checked);
}}
+ aria-label={label}
/>
{!hasPlatinumLicense && (
diff --git a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts
index 724cf5bc2b25e..964359cdb7ee5 100644
--- a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts
+++ b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts
@@ -11,6 +11,7 @@ import Url from 'url';
import { resolve } from 'path';
import type { ToolingLog } from '@kbn/tooling-log';
import getPort from 'get-port';
+import getopts from 'getopts';
import { REPO_ROOT } from '@kbn/repo-info';
import type { ArtifactLicense, ServerlessProjectType } from '@kbn/es';
import { isServerlessProjectType, extractAndArchiveLogs } from '@kbn/es/src/utils';
@@ -196,12 +197,8 @@ function getESServerlessOptions(
(config.get('kbnTestServer.serverArgs') as string[])) ||
[];
- const projectType = kbnServerArgs
- .filter((arg) => arg.startsWith('--serverless'))
- .reduce((acc, arg) => {
- const match = arg.match(/--serverless[=\s](\w+)/);
- return acc + (match ? match[1] : '');
- }, '') as ServerlessProjectType;
+ const options = getopts(kbnServerArgs);
+ const projectType = options.serverless as ServerlessProjectType;
if (!isServerlessProjectType(projectType)) {
throw new Error(`Unsupported serverless projectType: ${projectType}`);
diff --git a/packages/kbn-test/src/jest/resolver.js b/packages/kbn-test/src/jest/resolver.js
index 8f985e9463962..aab1b0f597284 100644
--- a/packages/kbn-test/src/jest/resolver.js
+++ b/packages/kbn-test/src/jest/resolver.js
@@ -51,6 +51,13 @@ module.exports = (request, options) => {
});
}
+ if (request === '@launchdarkly/js-sdk-common') {
+ return resolve.sync('@launchdarkly/js-sdk-common/dist/cjs/index.cjs', {
+ basedir: options.basedir,
+ extensions: options.extensions,
+ });
+ }
+
if (request === `elastic-apm-node`) {
return APM_AGENT_MOCK;
}
diff --git a/packages/kbn-test/src/mocha/junit_report_generation.test.js b/packages/kbn-test/src/mocha/junit_report_generation.test.js
index caf023a795154..6dbc8bf6cf1f8 100644
--- a/packages/kbn-test/src/mocha/junit_report_generation.test.js
+++ b/packages/kbn-test/src/mocha/junit_report_generation.test.js
@@ -55,9 +55,12 @@ describe('dev/mocha/junit report generation', () => {
const [testsuite] = report.testsuites.testsuite;
expect(testsuite.$.time).toMatch(DURATION_REGEX);
expect(testsuite.$.timestamp).toMatch(ISO_DATE_SEC_REGEX);
- expect(testsuite.$).toEqual({
- 'command-line':
- 'node scripts/jest --config=packages/kbn-test/jest.config.js --runInBand --coverage=false --passWithNoTests',
+ const expectedCommandLine = process.env.CI
+ ? 'node scripts/jest --config=packages/kbn-test/jest.config.js --runInBand --coverage=false --passWithNoTests'
+ : 'node node_modules/jest-worker/build/workers/processChild.js';
+
+ expect(testsuite.$).toMatchObject({
+ 'command-line': expectedCommandLine,
failures: '2',
name: 'test',
skipped: '1',
diff --git a/packages/serverless/settings/observability_project/index.ts b/packages/serverless/settings/observability_project/index.ts
index 44f30e4320463..d04b0238c5510 100644
--- a/packages/serverless/settings/observability_project/index.ts
+++ b/packages/serverless/settings/observability_project/index.ts
@@ -27,8 +27,6 @@ export const OBSERVABILITY_PROJECT_SETTINGS = [
settings.OBSERVABILITY_APM_TRACE_EXPLORER_TAB_ID,
settings.OBSERVABILITY_ENABLE_AWS_LAMBDA_METRICS_ID,
settings.OBSERVABILITY_APM_ENABLE_CRITICAL_PATH_ID,
- settings.OBSERVABILITY_ENABLE_INFRASTRUCTURE_HOSTS_VIEW_ID,
- settings.OBSERVABILITY_ENABLE_INFRASTRUCTURE_CONTAINER_ASSET_VIEW_ID,
settings.OBSERVABILITY_ENABLE_INFRASTRUCTURE_ASSET_CUSTOM_DASHBOARDS_ID,
settings.OBSERVABILITY_LOGS_EXPLORER_ALLOWED_DATA_VIEWS_ID,
settings.OBSERVABILITY_APM_ENABLE_TABLE_SEARCH_BAR,
diff --git a/packages/shared-ux/button/exit_full_screen/src/services.tsx b/packages/shared-ux/button/exit_full_screen/src/services.tsx
index a167b2b116bf0..9497a6ed34468 100644
--- a/packages/shared-ux/button/exit_full_screen/src/services.tsx
+++ b/packages/shared-ux/button/exit_full_screen/src/services.tsx
@@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import React, { FC, useContext, PropsWithChildren } from 'react';
+import React, { FC, useContext, PropsWithChildren, useCallback } from 'react';
import type {
Services,
@@ -37,12 +37,16 @@ export const ExitFullScreenButtonProvider: FC
> = ({ children, ...services }) => {
+ const setIsFullscreen = useCallback(
+ (isFullscreen: boolean) => {
+ services.coreStart.chrome.setIsVisible(!isFullscreen);
+ },
+ [services.coreStart.chrome]
+ );
return (
{
- services.coreStart.chrome.setIsVisible(!isFullscreen);
- },
+ setIsFullscreen,
customBranding$: services.coreStart.customBranding.customBranding$,
}}
>
diff --git a/scripts/codeql/codeql.dockerfile b/scripts/codeql/codeql.dockerfile
new file mode 100644
index 0000000000000..fce6b9c3fdd63
--- /dev/null
+++ b/scripts/codeql/codeql.dockerfile
@@ -0,0 +1,39 @@
+FROM ubuntu:latest
+
+ENV DEBIAN_FRONTEND=noninteractive
+
+ARG USERNAME=codeql
+ARG CODEQL_VERSION="v2.19.0"
+ENV CODEQL_HOME /usr/local/codeql-home
+
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends \
+ passwd \
+ adduser \
+ bash \
+ curl \
+ git \
+ unzip \
+ nodejs \
+ jq
+
+RUN adduser --home ${CODEQL_HOME} ${USERNAME}
+
+RUN curl -Lk "https://github.com/github/codeql-action/releases/download/codeql-bundle-${CODEQL_VERSION}/codeql-bundle-linux64.tar.gz" -o codeql.tar.gz \
+ && mkdir -p ${CODEQL_HOME} \
+ && tar -xvzf codeql.tar.gz -C ${CODEQL_HOME} \
+ && rm codeql.tar.gz
+
+RUN chmod +x ${CODEQL_HOME}/codeql/codeql
+
+RUN chown -R ${USERNAME}:${USERNAME} ${CODEQL_HOME}
+
+USER ${USERNAME}
+
+ENV PATH="${CODEQL_HOME}/codeql:${PATH}"
+
+RUN echo $PATH && codeql --version
+
+WORKDIR /workspace
+
+ENTRYPOINT ["/bin/bash", "-c"]
diff --git a/scripts/codeql/quick_check.sh b/scripts/codeql/quick_check.sh
new file mode 100644
index 0000000000000..15023bfb13bfa
--- /dev/null
+++ b/scripts/codeql/quick_check.sh
@@ -0,0 +1,126 @@
+#!/bin/bash
+
+LANGUAGE="javascript"
+CODEQL_DIR=".codeql"
+DATABASE_PATH="$CODEQL_DIR/database"
+QUERY_OUTPUT="$DATABASE_PATH/results.sarif"
+OUTPUT_FORMAT="sarif-latest"
+DOCKER_IMAGE="codeql-env"
+BASE_DIR="$(cd "$(dirname "$0")"; pwd)"
+
+# Colors
+bold=$(tput bold)
+reset=$(tput sgr0)
+red=$(tput setaf 1)
+green=$(tput setaf 2)
+blue=$(tput setaf 4)
+yellow=$(tput setaf 3)
+
+while getopts ":s:r:" opt; do
+ case $opt in
+ s) SRC_DIR="$OPTARG" ;;
+ r) CODEQL_DIR="$OPTARG"; DATABASE_PATH="$CODEQL_DIR/database"; QUERY_OUTPUT="$DATABASE_PATH/results.sarif" ;;
+ \?) echo "Invalid option -$OPTARG" >&2; exit 1 ;;
+ :) echo "Option -$OPTARG requires an argument." >&2; exit 1 ;;
+ esac
+done
+
+if [ -z "$SRC_DIR" ]; then
+ echo "Usage: $0 -s [-r ]"
+ exit 1
+fi
+
+mkdir -p "$CODEQL_DIR"
+
+# Check the architecture
+ARCH=$(uname -m)
+PLATFORM_FLAG=""
+
+# CodeQL CLI binary does not support arm64 architecture, setting the platform to linux/amd64
+if [[ "$ARCH" == "arm64" ]]; then
+ PLATFORM_FLAG="--platform linux/amd64"
+fi
+
+if [[ "$(docker images -q $DOCKER_IMAGE 2> /dev/null)" == "" ]]; then
+ echo "Docker image $DOCKER_IMAGE not found. Building locally..."
+ docker build $PLATFORM_FLAG -t "$DOCKER_IMAGE" -f "$BASE_DIR/codeql.dockerfile" "$BASE_DIR"
+ if [ $? -ne 0 ]; then
+ echo "${red}Docker image build failed.${reset}"
+ exit 1
+ fi
+fi
+
+cleanup_database() {
+ echo "Deleting contents of $CODEQL_DIR."
+ rm -rf "$CODEQL_DIR"/*
+}
+
+SRC_DIR="$(cd "$(dirname "$SRC_DIR")"; pwd)/$(basename "$SRC_DIR")"
+CODEQL_DIR="$(cd "$(dirname "$CODEQL_DIR")"; pwd)/$(basename "$CODEQL_DIR")"
+DATABASE_PATH="$(cd "$(dirname "$DATABASE_PATH")"; pwd)/$(basename "$DATABASE_PATH")"
+
+# Step 1: Run the Docker container to create a CodeQL database from the source code.
+echo "Creating a CodeQL database from the source code: $SRC_DIR"
+docker run $PLATFORM_FLAG --rm -v "$SRC_DIR":/workspace/source-code \
+ -v "${DATABASE_PATH}":/workspace/shared $DOCKER_IMAGE \
+ "codeql database create /workspace/shared/codeql-db --language=javascript --source-root=/workspace/source-code --overwrite"
+
+if [ $? -ne 0 ]; then
+ echo "CodeQL database creation failed."
+ cleanup_database
+ exit 1
+fi
+
+echo "Analyzing a CodeQL database: $DATABASE_PATH"
+# Step 2: Run the Docker container to analyze the CodeQL database.
+docker run $PLATFORM_FLAG --rm -v "${DATABASE_PATH}":/workspace/shared $DOCKER_IMAGE \
+ "codeql database analyze --format=${OUTPUT_FORMAT} --output=/workspace/shared/results.sarif /workspace/shared/codeql-db javascript-security-and-quality.qls"
+
+if [ $? -ne 0 ]; then
+ echo "CodeQL database analysis failed."
+ cleanup_database
+ exit 1
+fi
+
+# Step 3: Print summary of SARIF results
+echo "Analysis complete. Results saved to $QUERY_OUTPUT"
+if command -v jq &> /dev/null; then
+ vulnerabilities=$(jq -r '.runs[] | select(.results | length > 0)' "$QUERY_OUTPUT")
+
+ if [[ -z "$vulnerabilities" ]]; then
+ echo "${blue}${bold}No vulnerabilities found in the SARIF results.${reset}"
+ else
+ echo "${yellow}${bold}Summary of SARIF results:${reset}"
+ jq -r '
+ .runs[] |
+ .results[] as $result |
+ .tool.driver.rules[] as $rule |
+ select($rule.id == $result.ruleId) |
+ "Rule: \($result.ruleId)\nMessage: \($result.message.text)\nFile: \($result.locations[].physicalLocation.artifactLocation.uri)\nLine: \($result.locations[].physicalLocation.region.startLine)\nSecurity Severity: \($rule.properties."security-severity" // "N/A")\n"' "$QUERY_OUTPUT" |
+ while IFS= read -r line; do
+ case "$line" in
+ Rule:*)
+ echo "${red}${bold}$line${reset}"
+ ;;
+ Message:*)
+ echo "${green}$line${reset}"
+ ;;
+ File:*)
+ echo "${blue}$line${reset}"
+ ;;
+ Line:*)
+ echo "${yellow}$line${reset}"
+ ;;
+ Security\ Severity:*)
+ echo "${yellow}$line${reset}"
+ ;;
+ *)
+ echo "$line"
+ ;;
+ esac
+ done
+ fi
+else
+ echo "${red}${bold}Please install jq to display a summary of the SARIF results.${reset}"
+ echo "${bold}You can view the full results in the SARIF file using a SARIF viewer.${reset}"
+fi
diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts
index e442a0efeea05..f31ec223e3d9b 100644
--- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts
+++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts
@@ -59,7 +59,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"action": "0e6fc0b74c7312a8c11ff6b14437b93a997358b8",
"action_task_params": "b50cb5c8a493881474918e8d4985e61374ca4c30",
"ad_hoc_run_params": "d4e3c5c794151d0a4f5c71e886b2aa638da73ad2",
- "alert": "05b07040b12ff45ab642f47464e8a6c903cf7b86",
+ "alert": "556a03378f5ee1c31593c3a37c66b54555ee14ff",
"api_key_pending_invalidation": "8f5554d1984854011b8392d9a6f7ef985bcac03c",
"apm-custom-dashboards": "b67128f78160c288bd7efe25b2da6e2afd5e82fc",
"apm-indices": "8a2d68d415a4b542b26b0d292034a28ffac6fed4",
diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts
index d764978c9db92..c810a74091458 100644
--- a/src/dev/build/tasks/os_packages/docker_generator/run.ts
+++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts
@@ -51,7 +51,7 @@ export async function runDockerGenerator(
*/
if (flags.baseImage === 'wolfi')
baseImageName =
- 'docker.elastic.co/wolfi/chainguard-base:latest@sha256:de4d5b06ee2074eb716f29e72b170346fd4715e5f083fc83a378603ce5bd9ced';
+ 'docker.elastic.co/wolfi/chainguard-base:latest@sha256:8cff240b81057968575dd28dab0c3609657cb7e0e60ff017261e5b721fad9e1b';
let imageFlavor = '';
if (flags.baseImage === 'ubi') imageFlavor += `-ubi`;
diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx
index ae9c5a844dcc3..467e6afa6a897 100644
--- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx
+++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx
@@ -842,7 +842,7 @@ describe('XYChart component', () => {
const lineArea = dataLayers.find(LineSeries).at(0);
const expectedSeriesStyle = expect.objectContaining({
point: expect.objectContaining({
- visible: showPoints ? 'always' : 'never',
+ visible: showPoints ? 'always' : 'auto',
}),
});
expect(lineArea.prop('areaSeriesStyle')).toEqual(expectedSeriesStyle);
diff --git a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx
index 473562b63ad5e..942909880f301 100644
--- a/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx
+++ b/src/plugins/chart_expressions/expression_xy/public/helpers/data_layers.tsx
@@ -298,7 +298,7 @@ export const getSeriesName: GetSeriesNameFn = (
const getPointConfig: GetPointConfigFn = ({ markSizeAccessor, showPoints, pointsRadius }) => {
return {
- visible: showPoints || markSizeAccessor ? 'always' : 'never',
+ visible: showPoints || markSizeAccessor ? 'always' : 'auto',
radius: pointsRadius,
fill: markSizeAccessor ? ColorVariant.Series : undefined,
};
diff --git a/src/plugins/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.tsx b/src/plugins/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.tsx
index 034ee2f8e45f4..59b3b3926060a 100644
--- a/src/plugins/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.tsx
+++ b/src/plugins/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.tsx
@@ -50,11 +50,16 @@ export const DashboardListingPage = ({
}, []);
useEffect(() => {
- coreServices.chrome.setBreadcrumbs([
+ coreServices.chrome.setBreadcrumbs(
+ [
+ {
+ text: getDashboardBreadcrumb(),
+ },
+ ],
{
- text: getDashboardBreadcrumb(),
- },
- ]);
+ project: { value: [] },
+ }
+ );
if (serverlessService) {
// if serverless breadcrumbs available,
diff --git a/src/plugins/dashboard/public/dashboard_container/component/viewport/dashboard_viewport.tsx b/src/plugins/dashboard/public/dashboard_container/component/viewport/dashboard_viewport.tsx
index 664a3c43a8d9d..027d2aee62b15 100644
--- a/src/plugins/dashboard/public/dashboard_container/component/viewport/dashboard_viewport.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/component/viewport/dashboard_viewport.tsx
@@ -10,7 +10,7 @@
import { debounce } from 'lodash';
import classNames from 'classnames';
import useResizeObserver from 'use-resize-observer/polyfilled';
-import React, { useEffect, useMemo, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { EuiPortal } from '@elastic/eui';
import { ReactEmbeddableRenderer, ViewMode } from '@kbn/embeddable-plugin/public';
@@ -22,10 +22,7 @@ import {
ControlGroupSerializedState,
} from '@kbn/controls-plugin/public';
import { CONTROL_GROUP_TYPE } from '@kbn/controls-plugin/common';
-import {
- useBatchedPublishingSubjects,
- useStateFromPublishingSubject,
-} from '@kbn/presentation-publishing';
+import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { DashboardGrid } from '../grid';
import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api';
import { DashboardEmptyScreen } from '../empty_screen/dashboard_empty_screen';
@@ -44,7 +41,7 @@ export const useDebouncedWidthObserver = (skipDebounce = false, wait = 100) => {
return { ref, width };
};
-export const DashboardViewportComponent = () => {
+export const DashboardViewport = () => {
const dashboardApi = useDashboardApi();
const [hasControls, setHasControls] = useState(false);
const [
@@ -70,6 +67,9 @@ export const DashboardViewportComponent = () => {
dashboardApi.uuid$,
dashboardApi.fullScreenMode$
);
+ const onExit = useCallback(() => {
+ dashboardApi.setFullScreenMode(false);
+ }, [dashboardApi]);
const panelCount = useMemo(() => {
return Object.keys(panels).length;
@@ -142,6 +142,11 @@ export const DashboardViewportComponent = () => {
/>
) : null}
+ {fullScreenMode && (
+