mirror of
https://github.com/prymitive/karma
synced 2026-05-19 04:26:41 +00:00
fix(ui): better handling of scrollbar gap when modal is open
This commit is contained in:
committed by
Łukasz Mierzwa
parent
06550c00b3
commit
a7540a1e76
2325
ui/package-lock.json
generated
2325
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "5.13.1",
|
||||
"@fortawesome/free-solid-svg-icons": "5.13.1",
|
||||
"@fortawesome/react-fontawesome": "0.1.11",
|
||||
"@juggle/resize-observer": "3.2.0",
|
||||
"@popperjs/core": "2.4.4",
|
||||
"@sentry/browser": "5.19.0",
|
||||
"@types/react": "16.9.41",
|
||||
@@ -36,6 +37,7 @@
|
||||
"react": "16.13.1",
|
||||
"react-app-polyfill": "1.0.6",
|
||||
"react-autosuggest": "10.0.2",
|
||||
"react-cool-dimensions": "1.1.3",
|
||||
"react-day-picker": "7.4.8",
|
||||
"react-dom": "16.13.1",
|
||||
"react-highlighter": "0.4.3",
|
||||
@@ -48,7 +50,6 @@
|
||||
"react-linkify": "0.2.2",
|
||||
"react-media-hook": "0.4.7",
|
||||
"react-popper": "2.2.3",
|
||||
"react-resize-detector": "5.0.6",
|
||||
"react-scripts": "3.4.1",
|
||||
"react-select": "3.1.0",
|
||||
"react-transition-group": "4.4.1",
|
||||
|
||||
@@ -22,6 +22,12 @@ beforeEach(() => {
|
||||
|
||||
// matchMedia needs mocking
|
||||
window.matchMedia = mockMatchMedia({});
|
||||
|
||||
global.ResizeObserver = jest.fn((cb) => ({
|
||||
observe: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
}));
|
||||
global.ResizeObserverEntry = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -95,6 +95,10 @@ const Grid = ({
|
||||
}
|
||||
}, [grid.alertGroups.length, groupsToRender]);
|
||||
|
||||
useEffect(() => {
|
||||
repack();
|
||||
});
|
||||
|
||||
return useObserver(() => (
|
||||
<React.Fragment>
|
||||
<CSSTransition
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import React, { useEffect, useCallback, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
import debounce from "lodash.debounce";
|
||||
|
||||
import ReactResizeDetector from "react-resize-detector";
|
||||
import useDimensions from "react-cool-dimensions";
|
||||
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import { Settings } from "Stores/Settings";
|
||||
@@ -13,8 +11,6 @@ import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { Grid } from "./Grid";
|
||||
import { GridSizesConfig, GetGridElementWidth } from "./GridSize";
|
||||
|
||||
const GridPadding = 5;
|
||||
|
||||
const AlertGrid = ({ alertStore, settingsStore, silenceFormStore }) => {
|
||||
const getGridSizesConfig = (windowWidth) =>
|
||||
GridSizesConfig(windowWidth, settingsStore.gridConfig.config.groupWidth);
|
||||
@@ -23,9 +19,7 @@ const AlertGrid = ({ alertStore, settingsStore, silenceFormStore }) => {
|
||||
GetGridElementWidth(
|
||||
canvasWidth,
|
||||
windowWidth,
|
||||
alertStore.data.grids.filter((g) => g.labelName !== "").length > 0
|
||||
? GridPadding * 2
|
||||
: 0,
|
||||
alertStore.data.gridPadding * 2,
|
||||
settingsStore.gridConfig.config.groupWidth
|
||||
);
|
||||
|
||||
@@ -36,27 +30,18 @@ const AlertGrid = ({ alertStore, settingsStore, silenceFormStore }) => {
|
||||
getGroupWidth(document.body.clientWidth, window.innerWidth)
|
||||
);
|
||||
|
||||
const handleResize = useCallback(
|
||||
debounce(() => {
|
||||
setGridSizesConfig(getGridSizesConfig(window.innerWidth));
|
||||
setGroupWidth(
|
||||
getGroupWidth(document.body.clientWidth, window.innerWidth)
|
||||
);
|
||||
}, 100),
|
||||
[]
|
||||
);
|
||||
const handleResize = ({ width }) => {
|
||||
setGridSizesConfig(getGridSizesConfig(window.innerWidth));
|
||||
setGroupWidth(getGroupWidth(width, window.innerWidth));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => {
|
||||
handleResize.cancel();
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, [handleResize]);
|
||||
const { ref } = useDimensions({
|
||||
onResize: handleResize,
|
||||
});
|
||||
|
||||
return useObserver(() => (
|
||||
<React.Fragment>
|
||||
<ReactResizeDetector handleWidth handleHeight onResize={handleResize} />
|
||||
<div ref={ref}></div>
|
||||
{alertStore.data.grids.map((grid) => (
|
||||
<Grid
|
||||
key={`${grid.labelName}/${grid.labelValue}`}
|
||||
@@ -66,7 +51,7 @@ const AlertGrid = ({ alertStore, settingsStore, silenceFormStore }) => {
|
||||
gridSizesConfig={gridSizesConfig}
|
||||
groupWidth={groupWidth}
|
||||
grid={grid}
|
||||
outerPadding={grid.labelName !== "" ? GridPadding : 0}
|
||||
outerPadding={alertStore.data.gridPadding}
|
||||
/>
|
||||
))}
|
||||
</React.Fragment>
|
||||
|
||||
@@ -19,12 +19,22 @@ import { AlertGrid } from ".";
|
||||
let alertStore;
|
||||
let settingsStore;
|
||||
let silenceFormStore;
|
||||
let resizeCallback;
|
||||
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(document.body, "clientWidth", {
|
||||
writable: true,
|
||||
value: 1000,
|
||||
});
|
||||
|
||||
global.ResizeObserver = jest.fn((cb) => {
|
||||
resizeCallback = cb;
|
||||
return {
|
||||
observe: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
};
|
||||
});
|
||||
global.ResizeObserverEntry = jest.fn();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -324,7 +334,7 @@ describe("<Grid />", () => {
|
||||
|
||||
it("doesn't throw errors after FontFaceObserver timeout", () => {
|
||||
jest.useFakeTimers();
|
||||
MockGroupList(60, 5);
|
||||
MockGroupList(1, 1);
|
||||
MountedGrid();
|
||||
// skip a minute to trigger FontFaceObserver timeout handler
|
||||
advanceBy(60 * 1000);
|
||||
@@ -340,7 +350,7 @@ describe("<Grid />", () => {
|
||||
|
||||
describe("<AlertGrid />", () => {
|
||||
const VerifyColumnCount = (innerWidth, outerWidth, columns) => {
|
||||
MockGroupList(40, 5);
|
||||
MockGroupList(20, 1);
|
||||
|
||||
document.body.clientWidth = innerWidth;
|
||||
window.innerWidth = outerWidth;
|
||||
@@ -428,7 +438,7 @@ describe("<AlertGrid />", () => {
|
||||
});
|
||||
|
||||
it("viewport resize also resizes alert groups", () => {
|
||||
MockGroupList(40, 5);
|
||||
MockGroupList(20, 1);
|
||||
|
||||
// set initial width
|
||||
document.body.clientWidth = 1980;
|
||||
@@ -447,7 +457,7 @@ describe("<AlertGrid />", () => {
|
||||
document.body.clientWidth = 1000;
|
||||
window.innerWidth = 1000;
|
||||
act(() => {
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
resizeCallback([{ contentRect: { width: 1000, height: 1000 } }]);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find("Grid").props().groupWidth).toBe(1000 / 2);
|
||||
@@ -462,7 +472,7 @@ describe("<AlertGrid />", () => {
|
||||
it("scrollbar render doesn't resize alert groups", () => {
|
||||
settingsStore.gridConfig.config.groupWidth = 400;
|
||||
|
||||
MockGroupList(40, 5);
|
||||
MockGroupList(20, 1);
|
||||
// set initial width
|
||||
document.body.clientWidth = 1600;
|
||||
window.innerWidth = 1600;
|
||||
@@ -477,10 +487,8 @@ describe("<AlertGrid />", () => {
|
||||
expect(tree.find("AlertGroup").at(0).props().groupWidth).toBe(400);
|
||||
|
||||
// then resize and verify if column count was changed
|
||||
document.body.clientWidth = 1584;
|
||||
window.innerWidth = 1600;
|
||||
act(() => {
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
resizeCallback([{ contentRect: { width: 1584, height: 1000 } }]);
|
||||
});
|
||||
wrapper.update();
|
||||
tree.setProps({
|
||||
@@ -493,7 +501,7 @@ describe("<AlertGrid />", () => {
|
||||
it("viewport resize doesn't allow loops", () => {
|
||||
settingsStore.gridConfig.config.groupWidth = 400;
|
||||
|
||||
MockGroupList(40, 5);
|
||||
MockGroupList(10, 1);
|
||||
|
||||
document.body.clientWidth = 1600;
|
||||
window.innerWidth = 1600;
|
||||
@@ -502,11 +510,13 @@ describe("<AlertGrid />", () => {
|
||||
const tree = ShallowGrid();
|
||||
|
||||
let results = [];
|
||||
const cb = () =>
|
||||
resizeCallback([
|
||||
{ contentRect: { width: index % 2 === 0 ? 1600 : 1584, height: 10 } },
|
||||
]);
|
||||
for (var index = 0; index < 14; index++) {
|
||||
document.body.clientWidth = index % 2 === 0 ? 1600 : 1584;
|
||||
window.innerWidth = 1600;
|
||||
act(() => {
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
cb();
|
||||
});
|
||||
wrapper.update();
|
||||
tree.setProps({
|
||||
@@ -650,7 +660,7 @@ describe("<AlertGrid />", () => {
|
||||
});
|
||||
|
||||
it("doesn't crash on unmount", () => {
|
||||
MockGroupList(60, 5);
|
||||
MockGroupList(5, 1);
|
||||
const tree = MountedAlertGrid();
|
||||
tree.unmount();
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import PropTypes from "prop-types";
|
||||
import { reaction } from "mobx";
|
||||
import { useObserver } from "mobx-react-lite";
|
||||
|
||||
import ReactResizeDetector from "react-resize-detector";
|
||||
import useDimensions from "react-cool-dimensions";
|
||||
|
||||
import IdleTimer from "react-idle-timer";
|
||||
|
||||
@@ -28,23 +28,20 @@ const NavBar = ({ alertStore, settingsStore, silenceFormStore, fixedTop }) => {
|
||||
const idleTimer = useRef(null);
|
||||
const [isIdle, setIsIdle] = useState(false);
|
||||
const [containerClass, setContainerClass] = useState("visible");
|
||||
const [elementSize, setElementSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
const context = React.useContext(ThemeContext);
|
||||
|
||||
const { ref, height } = useDimensions({});
|
||||
|
||||
const updateBodyPaddingTop = useCallback(
|
||||
(idle) => {
|
||||
const paddingTop = idle ? 0 : elementSize.height + 8;
|
||||
const paddingTop = idle ? 0 : height + 8;
|
||||
document.body.style.paddingTop = `${paddingTop}px`;
|
||||
setContainerClass(idle ? "invisible" : "visible");
|
||||
},
|
||||
[elementSize.height]
|
||||
[height]
|
||||
);
|
||||
|
||||
const onResize = useCallback((width, height) => {
|
||||
setElementSize({ width: width, height: height });
|
||||
}, []);
|
||||
|
||||
const onActive = useCallback(() => {
|
||||
setIsIdle(false);
|
||||
}, []);
|
||||
@@ -64,12 +61,7 @@ const NavBar = ({ alertStore, settingsStore, silenceFormStore, fixedTop }) => {
|
||||
updateBodyPaddingTop(false);
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [
|
||||
elementSize.height,
|
||||
updateBodyPaddingTop,
|
||||
isIdle,
|
||||
context.animations.duration,
|
||||
]);
|
||||
}, [height, updateBodyPaddingTop, isIdle, context.animations.duration]);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
@@ -106,11 +98,11 @@ const NavBar = ({ alertStore, settingsStore, silenceFormStore, fixedTop }) => {
|
||||
exit
|
||||
>
|
||||
<nav
|
||||
ref={ref}
|
||||
className={`navbar navbar-expand navbar-dark p-1 bg-primary-transparent d-inline-block ${
|
||||
fixedTop ? "fixed-top" : "w-100"
|
||||
}`}
|
||||
>
|
||||
<ReactResizeDetector handleHeight onResize={onResize} />
|
||||
<span className="navbar-brand p-0 my-0 mx-2 h1 d-none d-sm-block float-left">
|
||||
<OverviewModal alertStore={alertStore} />
|
||||
<FetchIndicator alertStore={alertStore} />
|
||||
|
||||
@@ -12,10 +12,20 @@ import { NavBar, MobileIdleTimeout, DesktopIdleTimeout } from ".";
|
||||
let alertStore;
|
||||
let settingsStore;
|
||||
let silenceFormStore;
|
||||
let resizeCallback;
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.spyOn(React, "useContext").mockImplementation(() => MockThemeContext);
|
||||
|
||||
global.ResizeObserver = jest.fn((cb) => {
|
||||
resizeCallback = cb;
|
||||
return {
|
||||
observe: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
};
|
||||
});
|
||||
global.ResizeObserverEntry = jest.fn();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -96,16 +106,22 @@ describe("<NavBar />", () => {
|
||||
expect(nav.props().className.split(" ")).toContain("w-100");
|
||||
});
|
||||
|
||||
it("body 'padding-top' style is updated after calling NavbarOnResize()", () => {
|
||||
it("body 'padding-top' style is updated after resize", () => {
|
||||
const tree = MountedNavbar();
|
||||
act(() => tree.find("ResizeDetector").props().onResize(0, 10));
|
||||
act(() => {
|
||||
resizeCallback([{ contentRect: { width: 100, height: 10 } }]);
|
||||
});
|
||||
tree.setProps({});
|
||||
expect(
|
||||
window
|
||||
.getComputedStyle(document.body, null)
|
||||
.getPropertyValue("padding-top")
|
||||
).toBe("18px");
|
||||
|
||||
act(() => tree.find("ResizeDetector").props().onResize(0, 36));
|
||||
act(() => {
|
||||
resizeCallback([{ contentRect: { width: 100, height: 36 } }]);
|
||||
});
|
||||
tree.setProps({});
|
||||
expect(
|
||||
window
|
||||
.getComputedStyle(document.body, null)
|
||||
|
||||
@@ -157,6 +157,9 @@ class AlertStore {
|
||||
silences: {},
|
||||
upstreams: { instances: [], clusters: {} },
|
||||
receivers: [],
|
||||
get gridPadding() {
|
||||
return this.grids.filter((g) => g.labelName !== "").length > 0 ? 5 : 0;
|
||||
},
|
||||
getAlertmanagerByName(name) {
|
||||
return this.upstreams.instances.find((am) => am.name === name);
|
||||
},
|
||||
@@ -199,6 +202,7 @@ class AlertStore {
|
||||
},
|
||||
},
|
||||
{
|
||||
gridPadding: computed,
|
||||
readOnlyAlertmanagers: computed,
|
||||
readWriteAlertmanagers: computed,
|
||||
clustersWithoutReadOnly: computed,
|
||||
|
||||
@@ -190,6 +190,7 @@ const MockGrid = (alertStore) => {
|
||||
if (group.alerts[j].state === "suppressed") {
|
||||
group.alerts[j].alertmanager = [
|
||||
{
|
||||
fingerprint: `fp-${i}-${j}`,
|
||||
name: "prod1",
|
||||
cluster: "prod",
|
||||
state: "suppressed",
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
// MUST be first thing we import
|
||||
// https://github.com/facebook/create-react-app/blob/master/packages/react-app-polyfill/README.md
|
||||
// IE is not supported (lacks Proxy) but that pollyfill provides fetch and other needed features
|
||||
import "react-app-polyfill/ie11";
|
||||
import "react-app-polyfill/stable";
|
||||
|
||||
// https://www.npmjs.com/package/react-intersection-observer#polyfill
|
||||
import "intersection-observer";
|
||||
import "./polyfills";
|
||||
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
46
ui/src/polyfill-load.test.js
Normal file
46
ui/src/polyfill-load.test.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import fetchMock from "fetch-mock";
|
||||
|
||||
import { EmptyAPIResponse } from "__mocks__/Fetch";
|
||||
import { mockMatchMedia } from "__mocks__/matchMedia";
|
||||
|
||||
const settingsElement = {
|
||||
dataset: {
|
||||
sentryDsn: "",
|
||||
version: "1.2.3",
|
||||
defaultFiltersBase64: "WyJmb289YmFyIiwiYmFyPX5iYXoiXQ==",
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.matchMedia = mockMatchMedia({});
|
||||
|
||||
global.MutationObserver = class {
|
||||
disconnect() {}
|
||||
observe(element, initObject) {}
|
||||
};
|
||||
});
|
||||
|
||||
it("loads ResizeObserver polyfill if needed", () => {
|
||||
expect(window.ResizeObserver).toBeFalsy();
|
||||
|
||||
const root = document.createElement("div");
|
||||
jest.spyOn(global.document, "getElementById").mockImplementation((name) => {
|
||||
return name === "settings"
|
||||
? settingsElement
|
||||
: name === "defaults"
|
||||
? null
|
||||
: name === "root"
|
||||
? root
|
||||
: null;
|
||||
});
|
||||
const response = EmptyAPIResponse();
|
||||
response.filters = [];
|
||||
|
||||
fetchMock.reset();
|
||||
fetchMock.any({
|
||||
body: JSON.stringify(response),
|
||||
});
|
||||
|
||||
require("./index.tsx");
|
||||
expect(window.ResizeObserver).toBeTruthy();
|
||||
});
|
||||
55
ui/src/polyfill-noop.test.js
Normal file
55
ui/src/polyfill-noop.test.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import fetchMock from "fetch-mock";
|
||||
|
||||
import { EmptyAPIResponse } from "__mocks__/Fetch";
|
||||
import { mockMatchMedia } from "__mocks__/matchMedia";
|
||||
|
||||
const settingsElement = {
|
||||
dataset: {
|
||||
sentryDsn: "",
|
||||
version: "1.2.3",
|
||||
defaultFiltersBase64: "WyJmb289YmFyIiwiYmFyPX5iYXoiXQ==",
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.matchMedia = mockMatchMedia({});
|
||||
|
||||
global.MutationObserver = class {
|
||||
disconnect() {}
|
||||
observe(element, initObject) {}
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("doesn't load ResizeObserver polyfill if not needed", () => {
|
||||
global.window.ResizeObserver = jest.fn((cb) => {
|
||||
return {
|
||||
observe: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const root = document.createElement("div");
|
||||
jest.spyOn(global.document, "getElementById").mockImplementation((name) => {
|
||||
return name === "settings"
|
||||
? settingsElement
|
||||
: name === "defaults"
|
||||
? null
|
||||
: name === "root"
|
||||
? root
|
||||
: null;
|
||||
});
|
||||
const response = EmptyAPIResponse();
|
||||
response.filters = [];
|
||||
|
||||
fetchMock.reset();
|
||||
fetchMock.any({
|
||||
body: JSON.stringify(response),
|
||||
});
|
||||
|
||||
require("./index.tsx");
|
||||
expect(window.ResizeObserver).toBeTruthy();
|
||||
});
|
||||
18
ui/src/polyfills.ts
Normal file
18
ui/src/polyfills.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// https://github.com/facebook/create-react-app/blob/master/packages/react-app-polyfill/README.md
|
||||
// IE is not supported (lacks Proxy) but that pollyfill provides fetch and other needed features
|
||||
import "react-app-polyfill/ie11";
|
||||
import "react-app-polyfill/stable";
|
||||
|
||||
// https://www.npmjs.com/package/react-intersection-observer#polyfill
|
||||
import "intersection-observer";
|
||||
|
||||
let pollyfillsLoaded: string[] = [];
|
||||
|
||||
if ("ResizeObserver" in window === false) {
|
||||
pollyfillsLoaded.push("ResizeObserver");
|
||||
const module = require("@juggle/resize-observer");
|
||||
(window as any).ResizeObserver = module.ResizeObserver;
|
||||
(window as any).ResizeObserverEntry = module.ResizeObserverEntry;
|
||||
}
|
||||
|
||||
export default pollyfillsLoaded;
|
||||
Reference in New Issue
Block a user