From ffc2448542562e141b35bac0760e01d25ef37a7e Mon Sep 17 00:00:00 2001 From: Yue Zhang Date: Thu, 24 Jun 2021 10:15:56 +0100 Subject: [PATCH] Add documentions (#402) --- doc/well_completions_storybooks.PNG | Bin 0 -> 53008 bytes package-lock.json | 143 ++++++++++++++++-- .../GroupTree/components/GroupTreeViewer.tsx | 7 +- .../GroupTree/components/Plot/group_tree.js | 4 + .../WellCompletions/WellCompletions.jsx | 8 +- .../WellCompletions.stories.jsx | 6 +- .../WellCompletions/components/DataLoader.tsx | 5 +- .../Settings/FilterButton.stories.tsx | 13 ++ ...terMenu.test.tsx => FilterButton.test.tsx} | 8 +- .../{FilterMenu.tsx => FilterButton.tsx} | 22 ++- .../Settings/FilterByAttributesButton.tsx | 61 -------- .../Settings/FilterMenu.stories.tsx | 13 -- .../HideZeroCompletionsSwitch.stories.tsx | 1 + .../Settings/HideZeroCompletionsSwitch.tsx | 7 +- .../Settings/SettingsBar.stories.tsx | 2 + .../components/Settings/SettingsBar.tsx | 11 +- .../Settings/SortButton.stories.tsx | 3 +- .../components/Settings/SortButton.tsx | 5 +- .../components/Settings/SortTable.stories.tsx | 1 + .../components/Settings/SortTable.tsx | 26 +++- .../TimeAggregationSelector.stories.tsx | 1 + .../Settings/TimeAggregationSelector.tsx | 7 +- .../Settings/TimeRangeSelector.stories.tsx | 1 + .../components/Settings/TimeRangeSelector.tsx | 26 +++- ...enu.stories.tsx => ViewButton.stories.tsx} | 9 +- ...{ViewMenu.test.tsx => ViewButton.test.tsx} | 6 +- .../Settings/{ViewMenu.tsx => ViewButton.tsx} | 13 +- .../WellAttributesSelector.stories.tsx | 1 + .../Settings/WellAttributesSelector.tsx | 12 +- .../Settings/WellFilter.stories.tsx | 6 +- .../components/Settings/WellFilter.tsx | 6 +- .../Settings/WellPagination.stories.tsx | 3 +- .../components/Settings/WellPagination.tsx | 11 +- .../Settings/WellsPerPageSelector.stories.tsx | 3 +- .../Settings/WellsPerPageSelector.tsx | 7 +- .../Settings/ZoneSelector.stories.tsx | 3 +- .../components/Settings/ZoneSelector.tsx | 13 +- ...st.tsx.snap => FilterButton.test.tsx.snap} | 0 ...test.tsx.snap => ViewButton.test.tsx.snap} | 0 .../components/WellCompletionComponent.tsx | 4 +- .../components/WellCompletionsViewer.tsx | 7 + .../WellCompletions/utils/dataUtil.ts | 33 ++-- 42 files changed, 360 insertions(+), 158 deletions(-) create mode 100644 doc/well_completions_storybooks.PNG create mode 100644 src/lib/components/WellCompletions/components/Settings/FilterButton.stories.tsx rename src/lib/components/WellCompletions/components/Settings/{FilterMenu.test.tsx => FilterButton.test.tsx} (89%) rename src/lib/components/WellCompletions/components/Settings/{FilterMenu.tsx => FilterButton.tsx} (64%) delete mode 100644 src/lib/components/WellCompletions/components/Settings/FilterByAttributesButton.tsx delete mode 100644 src/lib/components/WellCompletions/components/Settings/FilterMenu.stories.tsx rename src/lib/components/WellCompletions/components/Settings/{ViewMenu.stories.tsx => ViewButton.stories.tsx} (58%) rename src/lib/components/WellCompletions/components/Settings/{ViewMenu.test.tsx => ViewButton.test.tsx} (80%) rename src/lib/components/WellCompletions/components/Settings/{ViewMenu.tsx => ViewButton.tsx} (89%) rename src/lib/components/WellCompletions/components/Settings/__snapshots__/{FilterMenu.test.tsx.snap => FilterButton.test.tsx.snap} (100%) rename src/lib/components/WellCompletions/components/Settings/__snapshots__/{ViewMenu.test.tsx.snap => ViewButton.test.tsx.snap} (100%) diff --git a/doc/well_completions_storybooks.PNG b/doc/well_completions_storybooks.PNG new file mode 100644 index 0000000000000000000000000000000000000000..d898e4b17f333594ab12a9eecc51f82f6f3a9b79 GIT binary patch literal 53008 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 literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json index f3ada33fe..84f7648b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "leaflet": "^1.6.0", "leaflet-draw": "^1.0.4", "lodash": "^4.17.21", + "mathjs": "^9.4.2", "nebula.gl": "^0.22.3", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -1500,10 +1501,14 @@ } }, "node_modules/@babel/runtime": { - "version": "7.14.0", - "license": "MIT", + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz", + "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", "dependencies": { "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/runtime-corejs3": { @@ -12590,6 +12595,14 @@ "dev": true, "license": "MIT" }, + "node_modules/complex.js": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.0.13.tgz", + "integrity": "sha512-UEWd3G3/kd3lJmsdLsDh9qfinJlujL4hIFn3Vo4/G5eqehPsgCHf2CLhFs77tVkOp2stt/jbNit7Q1XFANFltA==", + "engines": { + "node": "*" + } + }, "node_modules/component-classes": { "version": "1.2.6", "license": "MIT", @@ -13981,9 +13994,9 @@ } }, "node_modules/decimal.js": { - "version": "10.2.1", - "dev": true, - "license": "MIT" + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.0.tgz", + "integrity": "sha512-MrQRs2gyD//7NeHi9TtsfClkf+cFAewDz+PZHR8ILKglLmBMyVX3ymQ+oeznE3tjrS7beTN+6JXb2C3JDHm7ug==" }, "node_modules/deck.gl": { "version": "8.4.16", @@ -14972,6 +14985,11 @@ "dev": true, "license": "MIT" }, + "node_modules/escape-latex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==" + }, "node_modules/escape-string-regexp": { "version": "1.0.5", "license": "MIT", @@ -16724,6 +16742,18 @@ "node": ">= 0.6" } }, + "node_modules/fraction.js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.1.tgz", + "integrity": "sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg==", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, "node_modules/fragment-cache": { "version": "0.2.1", "dev": true, @@ -19043,6 +19073,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k=" + }, "node_modules/jest": { "version": "26.6.3", "dev": true, @@ -21247,6 +21282,28 @@ "@math.gl/core": "3.4.2" } }, + "node_modules/mathjs": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-9.4.3.tgz", + "integrity": "sha512-UgIalR0ICCu1Me4kVPEXb8Xtw7S62XMpP7qSXfImQsoIc1pOX/IxZkLw33VdFYlmXpma3zcRoMq5auwB2fg1AA==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "complex.js": "^2.0.13", + "decimal.js": "^10.3.0", + "escape-latex": "^1.2.0", + "fraction.js": "^4.1.1", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^2.0.0" + }, + "bin": { + "mathjs": "bin/cli.js" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/md5.js": { "version": "1.3.5", "dev": true, @@ -26198,6 +26255,11 @@ "version": "2.0.1", "license": "ISC" }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" + }, "node_modules/select": { "version": "1.1.2", "dev": true, @@ -28009,9 +28071,7 @@ }, "node_modules/tiny-emitter": { "version": "2.1.0", - "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/tiny-warning": { "version": "1.0.3", @@ -28287,6 +28347,14 @@ "node": ">= 0.6" } }, + "node_modules/typed-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-2.0.0.tgz", + "integrity": "sha512-Hhy1Iwo/e4AtLZNK10ewVVcP2UEs408DS35ubP825w/YgSBK1KVLwALvvIG4yX75QJrxjCpcWkzkVRB0BwwYlA==", + "engines": { + "node": ">= 8" + } + }, "node_modules/typedarray": { "version": "0.0.6", "dev": true, @@ -31159,7 +31227,9 @@ } }, "@babel/runtime": { - "version": "7.14.0", + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz", + "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", "requires": { "regenerator-runtime": "^0.13.4" } @@ -39073,6 +39143,11 @@ "version": "1.0.1", "dev": true }, + "complex.js": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.0.13.tgz", + "integrity": "sha512-UEWd3G3/kd3lJmsdLsDh9qfinJlujL4hIFn3Vo4/G5eqehPsgCHf2CLhFs77tVkOp2stt/jbNit7Q1XFANFltA==" + }, "component-classes": { "version": "1.2.6", "requires": { @@ -40085,8 +40160,9 @@ "dev": true }, "decimal.js": { - "version": "10.2.1", - "dev": true + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.0.tgz", + "integrity": "sha512-MrQRs2gyD//7NeHi9TtsfClkf+cFAewDz+PZHR8ILKglLmBMyVX3ymQ+oeznE3tjrS7beTN+6JXb2C3JDHm7ug==" }, "deck.gl": { "version": "8.4.16", @@ -40802,6 +40878,11 @@ "version": "1.0.3", "dev": true }, + "escape-latex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==" + }, "escape-string-regexp": { "version": "1.0.5" }, @@ -42011,6 +42092,11 @@ "version": "0.2.0", "dev": true }, + "fraction.js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.1.tgz", + "integrity": "sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg==" + }, "fragment-cache": { "version": "0.2.1", "dev": true, @@ -43507,6 +43593,11 @@ "iterate-iterator": "^1.0.1" } }, + "javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k=" + }, "jest": { "version": "26.6.3", "dev": true, @@ -44961,6 +45052,22 @@ "@math.gl/core": "3.4.2" } }, + "mathjs": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-9.4.3.tgz", + "integrity": "sha512-UgIalR0ICCu1Me4kVPEXb8Xtw7S62XMpP7qSXfImQsoIc1pOX/IxZkLw33VdFYlmXpma3zcRoMq5auwB2fg1AA==", + "requires": { + "@babel/runtime": "^7.14.6", + "complex.js": "^2.0.13", + "decimal.js": "^10.3.0", + "escape-latex": "^1.2.0", + "fraction.js": "^4.1.1", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^2.0.0" + } + }, "md5.js": { "version": "1.3.5", "dev": true, @@ -48282,6 +48389,11 @@ "scrollparent": { "version": "2.0.1" }, + "seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" + }, "select": { "version": "1.1.2", "dev": true, @@ -49548,9 +49660,7 @@ "dev": true }, "tiny-emitter": { - "version": "2.1.0", - "dev": true, - "optional": true + "version": "2.1.0" }, "tiny-warning": { "version": "1.0.3" @@ -49731,6 +49841,11 @@ "mime-types": "~2.1.24" } }, + "typed-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-2.0.0.tgz", + "integrity": "sha512-Hhy1Iwo/e4AtLZNK10ewVVcP2UEs408DS35ubP825w/YgSBK1KVLwALvvIG4yX75QJrxjCpcWkzkVRB0BwwYlA==" + }, "typedarray": { "version": "0.0.6", "dev": true diff --git a/src/lib/components/GroupTree/components/GroupTreeViewer.tsx b/src/lib/components/GroupTree/components/GroupTreeViewer.tsx index b6b142bc7..cdd4aa201 100644 --- a/src/lib/components/GroupTree/components/GroupTreeViewer.tsx +++ b/src/lib/components/GroupTree/components/GroupTreeViewer.tsx @@ -1,4 +1,5 @@ import { createStyles, makeStyles } from "@material-ui/core"; +import { cloneDeep } from "lodash"; import React, { useContext, useEffect, useRef } from "react"; import { useSelector } from "react-redux"; import { GroupTreeState } from "../redux/store"; @@ -36,7 +37,11 @@ const GroupTreeViewer: React.FC = () => { (state: GroupTreeState) => state.ui.currentFlowRate ); useEffect(() => { - renderer.current = new GroupTree("#grouptree_tree", data, "oilrate"); + renderer.current = new GroupTree( + "#grouptree_tree", + cloneDeep(data), + "oilrate" + ); }, [data]); useEffect(() => { diff --git a/src/lib/components/GroupTree/components/Plot/group_tree.js b/src/lib/components/GroupTree/components/Plot/group_tree.js index 9b49fac85..03ac7770a 100644 --- a/src/lib/components/GroupTree/components/Plot/group_tree.js +++ b/src/lib/components/GroupTree/components/Plot/group_tree.js @@ -1,3 +1,7 @@ +/** This code is copied directly from + * https://github.com/anders-kiaer/webviz-subsurface-components/blob/dynamic_tree/src/lib/components/DynamicTree/group_tree.js + * This needs to be refactored to develop further + */ import * as d3 from "d3"; /* eslint camelcase: "off" */ /* eslint array-callback-return: "off" */ diff --git a/src/lib/components/WellCompletions/WellCompletions.jsx b/src/lib/components/WellCompletions/WellCompletions.jsx index 0c54375d1..b0665a149 100644 --- a/src/lib/components/WellCompletions/WellCompletions.jsx +++ b/src/lib/components/WellCompletions/WellCompletions.jsx @@ -11,7 +11,13 @@ import WellCompletionComponent from "./components/WellCompletionComponent"; const WellCompletions = (props) => { return ; }; - +/** + * Typescript and PropTypes serve different purposes. Typescript validates types at compile time, + * whereas PropTypes are checked at runtime. + * PropTypes are useful when you test how the components interact with external data, for example + * when you load JSON from an API. + * This is the only place in this component that propTypes definition is really needed. + */ WellCompletions.propTypes = { id: PropTypes.string.isRequired, data: PropTypes.object, diff --git a/src/lib/components/WellCompletions/WellCompletions.stories.jsx b/src/lib/components/WellCompletions/WellCompletions.stories.jsx index 0ed52faad..44c086086 100644 --- a/src/lib/components/WellCompletions/WellCompletions.stories.jsx +++ b/src/lib/components/WellCompletions/WellCompletions.stories.jsx @@ -1,7 +1,9 @@ import React from "react"; import { exampleData } from "./test/storybookDataDecorator"; import WellCompletions from "./WellCompletions"; - +/** + * Storybook test for the whole well completion component + */ export default { component: WellCompletions, title: "WellCompletions/Demo", @@ -16,7 +18,7 @@ export default { const Template = (data) => ; export const WellCompletion = Template.bind({}); - +//Inject test input data WellCompletion.args = { data: exampleData, }; diff --git a/src/lib/components/WellCompletions/components/DataLoader.tsx b/src/lib/components/WellCompletions/components/DataLoader.tsx index 0a8279ca9..bdbfdc183 100644 --- a/src/lib/components/WellCompletions/components/DataLoader.tsx +++ b/src/lib/components/WellCompletions/components/DataLoader.tsx @@ -21,7 +21,9 @@ const defaultData = { timeSteps: [], }; export const DataContext = React.createContext(defaultData); - +/** + * A data loading layer to ready the input data and redux store + */ const DataProvider: React.FC = ({ children, id, @@ -32,6 +34,7 @@ const DataProvider: React.FC = ({ data.stratigraphy.forEach((zone) => findSubzones(zone, subzones)); return subzones.map((zone) => zone.name); }, [data.stratigraphy]); + const preloadedState = useMemo(() => { //Setup attributes const attributeKeys = new Set(); diff --git a/src/lib/components/WellCompletions/components/Settings/FilterButton.stories.tsx b/src/lib/components/WellCompletions/components/Settings/FilterButton.stories.tsx new file mode 100644 index 000000000..85a34b28a --- /dev/null +++ b/src/lib/components/WellCompletions/components/Settings/FilterButton.stories.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { withReduxDecorator } from "../../test/storybookReduxAddon"; +import FilterButton from "./FilterButton"; + +export default { + component: FilterButton, + title: "WellCompletions/Components/Buttons/Filter", +}; + +const Template = () => ; +export const Button = Template.bind({}); +//Wrap with redux store +Button.decorators = [withReduxDecorator]; diff --git a/src/lib/components/WellCompletions/components/Settings/FilterMenu.test.tsx b/src/lib/components/WellCompletions/components/Settings/FilterButton.test.tsx similarity index 89% rename from src/lib/components/WellCompletions/components/Settings/FilterMenu.test.tsx rename to src/lib/components/WellCompletions/components/Settings/FilterButton.test.tsx index 801a6ce79..f321870c5 100644 --- a/src/lib/components/WellCompletions/components/Settings/FilterMenu.test.tsx +++ b/src/lib/components/WellCompletions/components/Settings/FilterButton.test.tsx @@ -3,15 +3,15 @@ import { fireEvent, render, screen } from "@testing-library/react"; import "jest-styled-components"; import React from "react"; import { testStore, Wrapper } from "../../test/TestWrapper"; -import FilterMenu from "./FilterMenu"; +import FilterButton from "./FilterButton"; describe("Test Filter Menu", () => { it("snapshot test", () => { - const { container } = render(Wrapper({ children: })); + const { container } = render(Wrapper({ children: })); expect(container.firstChild).toMatchSnapshot(); }); it("click to open filter menu and dispatch redux action", async () => { - render(, { + render(, { wrapper: Wrapper, }); fireEvent.click(screen.getByTestId("filter_button")); @@ -22,7 +22,7 @@ describe("Test Filter Menu", () => { }); }); it("Test tooltip title when menu not open", async () => { - render(, { + render(, { wrapper: Wrapper, }); fireEvent.mouseOver(screen.getByTestId("filter_button")); diff --git a/src/lib/components/WellCompletions/components/Settings/FilterMenu.tsx b/src/lib/components/WellCompletions/components/Settings/FilterButton.tsx similarity index 64% rename from src/lib/components/WellCompletions/components/Settings/FilterMenu.tsx rename to src/lib/components/WellCompletions/components/Settings/FilterButton.tsx index f23bb20f6..c263b8996 100644 --- a/src/lib/components/WellCompletions/components/Settings/FilterMenu.tsx +++ b/src/lib/components/WellCompletions/components/Settings/FilterButton.tsx @@ -8,23 +8,33 @@ import { WellCompletionsState } from "../../redux/store"; // Use library approach Icon.add({ filter_alt }); // (this needs only be done once) -const FilterMenu: React.FC = React.memo(() => { +/** + * A button for toggle on and off the filter functions in the side drawer + */ +const FilterButton: React.FC = React.memo(() => { + //Redux const dispatch = useDispatch(); - const isDrawerOpen = useSelector( (state: WellCompletionsState) => state.ui.isDrawerOpen ); - const openDrawer = useCallback( + + // Handlers + const onClick = useCallback( () => dispatch(updateIsDrawerOpen(!isDrawerOpen)), [dispatch, isDrawerOpen] ); + + //Render return (
@@ -33,5 +43,5 @@ const FilterMenu: React.FC = React.memo(() => { ); }); -FilterMenu.displayName = "FilterMenu"; -export default FilterMenu; +FilterButton.displayName = "FilterMenu"; +export default FilterButton; diff --git a/src/lib/components/WellCompletions/components/Settings/FilterByAttributesButton.tsx b/src/lib/components/WellCompletions/components/Settings/FilterByAttributesButton.tsx deleted file mode 100644 index 1e8c2a038..000000000 --- a/src/lib/components/WellCompletions/components/Settings/FilterByAttributesButton.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* eslint-disable react/display-name */ -import { Button, Dialog, Icon, Menu, Scrim } from "@equinor/eds-core-react"; -import { sort } from "@equinor/eds-icons"; -import { createStyles, makeStyles } from "@material-ui/core"; -import React, { useState } from "react"; -import WellAttributesSelector from "./WellAttributesSelector"; - -// Use library approach -Icon.add({ sort }); // (this needs only be done once) -const useStyles = makeStyles(() => - createStyles({ - dialog: { - minWidth: "400px", - }, - action: { margin: "5px" }, - }) -); - -const FilterByAttributesButton: React.FC = React.memo(() => { - const classes = useStyles(); - // Dialogs - - const [visibleScrim, setVisibleScrim] = useState(false); - const handleClose = () => { - setVisibleScrim(!visibleScrim); - }; - return ( - <> - setVisibleScrim(true)}> - Filter by Attributes - - {visibleScrim && ( - - - - - - - - - - - - )} - - ); -}); - -FilterByAttributesButton.displayName = "FilterByAttributesButton"; -export default FilterByAttributesButton; diff --git a/src/lib/components/WellCompletions/components/Settings/FilterMenu.stories.tsx b/src/lib/components/WellCompletions/components/Settings/FilterMenu.stories.tsx deleted file mode 100644 index b2c5bed6b..000000000 --- a/src/lib/components/WellCompletions/components/Settings/FilterMenu.stories.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from "react"; -import { exampleDataDecorator } from "../../test/storybookDataDecorator"; -import { withReduxDecorator } from "../../test/storybookReduxAddon"; -import FilterMenu from "./FilterMenu"; - -export default { - component: FilterMenu, - title: "WellCompletions/Components/Menus/Filter", -}; - -const Template = () => ; -export const Menu = Template.bind({}); -Menu.decorators = [exampleDataDecorator, withReduxDecorator]; diff --git a/src/lib/components/WellCompletions/components/Settings/HideZeroCompletionsSwitch.stories.tsx b/src/lib/components/WellCompletions/components/Settings/HideZeroCompletionsSwitch.stories.tsx index f7315ce31..b9a023662 100644 --- a/src/lib/components/WellCompletions/components/Settings/HideZeroCompletionsSwitch.stories.tsx +++ b/src/lib/components/WellCompletions/components/Settings/HideZeroCompletionsSwitch.stories.tsx @@ -8,4 +8,5 @@ export default { const Template = () => ; export const Switch = Template.bind({}); +//Wrap with redux store Switch.decorators = [withReduxDecorator]; diff --git a/src/lib/components/WellCompletions/components/Settings/HideZeroCompletionsSwitch.tsx b/src/lib/components/WellCompletions/components/Settings/HideZeroCompletionsSwitch.tsx index 0984c0f1c..22535dd1e 100644 --- a/src/lib/components/WellCompletions/components/Settings/HideZeroCompletionsSwitch.tsx +++ b/src/lib/components/WellCompletions/components/Settings/HideZeroCompletionsSwitch.tsx @@ -3,14 +3,17 @@ import React, { useCallback } from "react"; import { useDispatch, useSelector } from "react-redux"; import { updateHideZeroCompletions } from "../../redux/actions"; import { WellCompletionsState } from "../../redux/store"; - +/** + * A switch for showing/hiding wells with zero completion + */ const HideZeroCompletionsSwitch: React.FC = React.memo(() => { // Redux const dispatch = useDispatch(); const hideZeroCompletions = useSelector( (st: WellCompletionsState) => st.ui.hideZeroCompletions ); - // handlers + + // Handlers const handleSwitchChange = useCallback( (event) => dispatch(updateHideZeroCompletions(event.target.checked)), [dispatch] diff --git a/src/lib/components/WellCompletions/components/Settings/SettingsBar.stories.tsx b/src/lib/components/WellCompletions/components/Settings/SettingsBar.stories.tsx index 8be7bfb91..1e00fa763 100644 --- a/src/lib/components/WellCompletions/components/Settings/SettingsBar.stories.tsx +++ b/src/lib/components/WellCompletions/components/Settings/SettingsBar.stories.tsx @@ -10,4 +10,6 @@ export default { const Template = () => ; export const TopBar = Template.bind({}); +//Wrap with redux store +//Settings bar also need to use the input data therefore wrapping with exampleDataDecorator TopBar.decorators = [exampleDataDecorator, withReduxDecorator]; diff --git a/src/lib/components/WellCompletions/components/Settings/SettingsBar.tsx b/src/lib/components/WellCompletions/components/Settings/SettingsBar.tsx index 44c8a6949..5cd7ed8e2 100644 --- a/src/lib/components/WellCompletions/components/Settings/SettingsBar.tsx +++ b/src/lib/components/WellCompletions/components/Settings/SettingsBar.tsx @@ -1,9 +1,9 @@ import { TopBar } from "@equinor/eds-core-react"; import { createStyles, makeStyles } from "@material-ui/core"; import React from "react"; -import FilterMenu from "./FilterMenu"; +import FilterButton from "./FilterButton"; import TimeRangeSelector from "./TimeRangeSelector"; -import ViewMenu from "./ViewMenu"; +import ViewButton from "./ViewButton"; const useStyles = makeStyles(() => createStyles({ @@ -17,6 +17,9 @@ const useStyles = makeStyles(() => }, }) ); +/** + * A settings bar that offers time selection and other viewing/filtering functions + */ const SettingsBar: React.FC = React.memo(() => { const classes = useStyles(); return ( @@ -25,8 +28,8 @@ const SettingsBar: React.FC = React.memo(() => { - - + + ); diff --git a/src/lib/components/WellCompletions/components/Settings/SortButton.stories.tsx b/src/lib/components/WellCompletions/components/Settings/SortButton.stories.tsx index 55f0e01a1..7b4d2065a 100644 --- a/src/lib/components/WellCompletions/components/Settings/SortButton.stories.tsx +++ b/src/lib/components/WellCompletions/components/Settings/SortButton.stories.tsx @@ -5,9 +5,10 @@ import SortButton from "./SortButton"; export default { component: SortButton, - title: "WellCompletions/Components/Settings/Sort by Attributes", + title: "WellCompletions/Components/Buttons/Sort by Attributes", }; const Template = () => ; export const Button = Template.bind({}); +//Wrap with example intpu data and redux store Button.decorators = [exampleDataDecorator, withReduxDecorator]; diff --git a/src/lib/components/WellCompletions/components/Settings/SortButton.tsx b/src/lib/components/WellCompletions/components/Settings/SortButton.tsx index cc2f5527b..466c3f1a9 100644 --- a/src/lib/components/WellCompletions/components/Settings/SortButton.tsx +++ b/src/lib/components/WellCompletions/components/Settings/SortButton.tsx @@ -12,11 +12,12 @@ const useStyles = makeStyles(() => action: { margin: "5px" }, }) ); - +/** + * A menu button that shows a dialog for sorting wells by attributes + */ const SortButton: React.FC = React.memo(() => { const classes = useStyles(); // Dialogs - const [visibleScrim, setVisibleScrim] = useState(false); const handleClose = () => { setVisibleScrim(!visibleScrim); diff --git a/src/lib/components/WellCompletions/components/Settings/SortTable.stories.tsx b/src/lib/components/WellCompletions/components/Settings/SortTable.stories.tsx index 3ab0cb01d..6e9e88230 100644 --- a/src/lib/components/WellCompletions/components/Settings/SortTable.stories.tsx +++ b/src/lib/components/WellCompletions/components/Settings/SortTable.stories.tsx @@ -9,4 +9,5 @@ export default { const Template = () => ; export const Table = Template.bind({}); +//Wrap with redux store Table.decorators = [withReduxDecorator]; diff --git a/src/lib/components/WellCompletions/components/Settings/SortTable.tsx b/src/lib/components/WellCompletions/components/Settings/SortTable.tsx index e33fca02f..65495abf9 100644 --- a/src/lib/components/WellCompletions/components/Settings/SortTable.tsx +++ b/src/lib/components/WellCompletions/components/Settings/SortTable.tsx @@ -42,18 +42,29 @@ const useStyles = makeStyles((theme: Theme) => ); Icon.add({ add_box }); // (this needs only be done once) Icon.add({ delete_to_trash }); // (this needs only be done once) - +/** + * A table that allows adding or removing sorting layers. + * There is also an option per row to sort by ascending or descending order + */ const SortTable: React.FC = React.memo(() => { const classes = useStyles(); + // Local states + // Sort key in the placeholder row const [sortKeyToAdd, setSortKeyToAdd] = useState(); + // Sort direction in the placeholder row const [sortDirectionToAdd, setSortDirectionToAdd] = useState("Ascending"); + // Redux const dispatch = useDispatch(); + // The attributes that we are currently sorting by const sortBy = useSelector((st: WellCompletionsState) => st.ui.sortBy); + // All the attribute keys const attributeKeys = useSelector( (st: WellCompletionsState) => st.attributes.attributeKeys ); + // Memo + // Apart from the user defined attribute, we can also sort by well name, stratigraphy depth etc const sortKeys = useMemo(() => { const keys = new Set([ SORT_BY_NAME, @@ -63,11 +74,14 @@ const SortTable: React.FC = React.memo(() => { attributeKeys.forEach((key) => keys.add(key)); return keys; }, [attributeKeys]); - + // The attribute keys that are not yet added to the current sorting set const availableToAdd = useMemo( () => Array.from(sortKeys).filter((key) => !(key in sortBy)), [sortKeys, sortBy] ); + + // Effects + // Update the sort key in the placeholder row to be the first item in the available attribute key set useEffect(() => { if ( availableToAdd.length > 0 && @@ -75,11 +89,14 @@ const SortTable: React.FC = React.memo(() => { ) setSortKeyToAdd(availableToAdd[0]); }, [availableToAdd, sortKeyToAdd]); - // handlers + + // Handlers + // Update the sort key in the placeholder row const onSortKeyToAddChange = useCallback( (event) => setSortKeyToAdd(event.target.value), [setSortKeyToAdd] ); + // Update the sort direction in the placeholder row const onSortDirectionToAddChange = useCallback( () => setSortDirectionToAdd( @@ -88,16 +105,19 @@ const SortTable: React.FC = React.memo(() => { [setSortDirectionToAdd, sortDirectionToAdd] ); + // Add or update sort key and direction const onUpdateSortKey = useCallback( (sortKey, sortDirection) => dispatch(updateSortKey({ sortKey, sortDirection })), [dispatch] ); + // Remove sort key and direction const onDeleteSortKey = useCallback( (sortKey) => dispatch(deleteSortKey(sortKey)), [dispatch] ); + // Render return ( ; export const Selector = Template.bind({}); +//Wrap with redux store Selector.decorators = [withReduxDecorator]; diff --git a/src/lib/components/WellCompletions/components/Settings/TimeAggregationSelector.tsx b/src/lib/components/WellCompletions/components/Settings/TimeAggregationSelector.tsx index d867e75f3..2620323ff 100644 --- a/src/lib/components/WellCompletions/components/Settings/TimeAggregationSelector.tsx +++ b/src/lib/components/WellCompletions/components/Settings/TimeAggregationSelector.tsx @@ -15,7 +15,9 @@ const useStyles = makeStyles((theme: Theme) => }, }) ); - +/** + * A dropdown for selecting the time aggregation mode + */ const TimeAggregationSelector: React.FC = React.memo(() => { const classes = useStyles(); // Redux @@ -23,11 +25,12 @@ const TimeAggregationSelector: React.FC = React.memo(() => { const timeAggregation = useSelector( (st: WellCompletionsState) => st.ui.timeAggregation ); - // handlers + // Handlers const handleSelectedItemChange = useCallback( (event) => dispatch(updateTimeAggregation(event.target.value)), [dispatch] ); + //Render return ( ; export const Selector = Template.bind({}); +//Wrap with example intpu data and redux store Selector.decorators = [exampleDataDecorator, withReduxDecorator]; diff --git a/src/lib/components/WellCompletions/components/Settings/TimeRangeSelector.tsx b/src/lib/components/WellCompletions/components/Settings/TimeRangeSelector.tsx index b97b163e1..44d7e5276 100644 --- a/src/lib/components/WellCompletions/components/Settings/TimeRangeSelector.tsx +++ b/src/lib/components/WellCompletions/components/Settings/TimeRangeSelector.tsx @@ -41,9 +41,14 @@ const EdsSlider = withStyles({ }, }, })(Slider); +/** + * A React component for selecting time step(s) to display in the plot + */ const TimeRangeSelector: React.FC = React.memo(() => { const classes = useStyles(); + // Direct access to the input data const data = useContext(DataContext); + // Redux const dispatch = useDispatch(); const timeAggregation = useSelector( @@ -53,16 +58,25 @@ const TimeRangeSelector: React.FC = React.memo(() => { (state: WellCompletionsState) => state.ui.timeIndexRange, isEqual ) as [number, number]; + + // Memo + // Arry of date time strings const times = useMemo(() => data.timeSteps, [data]); - // handlers + + // Handlers + // Get date time string by index const outputFunction = useCallback((step: number) => times[step], [times]); + // Update time range in redux. When the time aggregation is off, + // only the upper bound of the range will be used in computing the plot data const onChange = useCallback( (range) => dispatch(updateTimeIndexRange(range)), [dispatch] ); - //If number of time step is small + + // Render return (
+ {/* This only appears when time aggregation is on */} {timeAggregation !== "None" && ( { ]) } > + {/* Show the full list of date times */} {times.map((time, index) => ( )} + {/* Slider that is easy to use when the number of time steps is not too large */}
Time Steps { valueLabelDisplay="on" onChange={(_, value) => onChange( + //If time aggregation is off, we only need to know the end date timeAggregation === "None" ? [0, value] - : [ + : // This is due to a feature (or a bug) in EdsSlider that the first + //value in the range is not necessarily the lower bound + [ Math.min(...(value as number[])), Math.max(...(value as number[])), ] @@ -129,6 +148,7 @@ const TimeRangeSelector: React.FC = React.memo(() => { ]) } > + {/* If time aggregation is on, we only show the time steps that >= the start date */} {(timeAggregation === "None" ? times : times.filter( diff --git a/src/lib/components/WellCompletions/components/Settings/ViewMenu.stories.tsx b/src/lib/components/WellCompletions/components/Settings/ViewButton.stories.tsx similarity index 58% rename from src/lib/components/WellCompletions/components/Settings/ViewMenu.stories.tsx rename to src/lib/components/WellCompletions/components/Settings/ViewButton.stories.tsx index 1825af498..15fa04442 100644 --- a/src/lib/components/WellCompletions/components/Settings/ViewMenu.stories.tsx +++ b/src/lib/components/WellCompletions/components/Settings/ViewButton.stories.tsx @@ -1,13 +1,14 @@ import React from "react"; import { exampleDataDecorator } from "../../test/storybookDataDecorator"; import { withReduxDecorator } from "../../test/storybookReduxAddon"; -import ViewMenu from "./ViewMenu"; +import ViewButton from "./ViewButton"; export default { - component: ViewMenu, - title: "WellCompletions/Components/Menus/View", + component: ViewButton, + title: "WellCompletions/Components/Buttons/View", }; -const Template = () => ; +const Template = () => ; export const Menu = Template.bind({}); +//Wrap with example intpu data and redux store Menu.decorators = [exampleDataDecorator, withReduxDecorator]; diff --git a/src/lib/components/WellCompletions/components/Settings/ViewMenu.test.tsx b/src/lib/components/WellCompletions/components/Settings/ViewButton.test.tsx similarity index 80% rename from src/lib/components/WellCompletions/components/Settings/ViewMenu.test.tsx rename to src/lib/components/WellCompletions/components/Settings/ViewButton.test.tsx index b086e6510..353758a74 100644 --- a/src/lib/components/WellCompletions/components/Settings/ViewMenu.test.tsx +++ b/src/lib/components/WellCompletions/components/Settings/ViewButton.test.tsx @@ -3,16 +3,16 @@ import { fireEvent, render, screen } from "@testing-library/react"; import "jest-styled-components"; import React from "react"; import { Wrapper } from "../../test/TestWrapper"; -import ViewMenu from "./ViewMenu"; +import ViewButton from "./ViewButton"; describe("test view menu", () => { it("snapshot test", () => { - const { container } = render(Wrapper({ children: })); + const { container } = render(Wrapper({ children: })); expect(container.firstChild).toMatchSnapshot(); }); it("click to display menu", async () => { - render(, { + render(, { wrapper: Wrapper, }); fireEvent.click(screen.getByRole("button", { name: "" })); diff --git a/src/lib/components/WellCompletions/components/Settings/ViewMenu.tsx b/src/lib/components/WellCompletions/components/Settings/ViewButton.tsx similarity index 89% rename from src/lib/components/WellCompletions/components/Settings/ViewMenu.tsx rename to src/lib/components/WellCompletions/components/Settings/ViewButton.tsx index bd1b63e81..62c54f576 100644 --- a/src/lib/components/WellCompletions/components/Settings/ViewMenu.tsx +++ b/src/lib/components/WellCompletions/components/Settings/ViewButton.tsx @@ -24,19 +24,22 @@ const useStyles = makeStyles((theme: Theme) => }, }) ); -const ViewMenu: React.FC = React.memo(() => { +/** + * A menu button that shows a list of viewing options + */ +const ViewButton: React.FC = React.memo(() => { const classes = useStyles(); const [anchorEl, setAnchorEl] = React.useState(null); - // handlers + // Handlers const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; - const handleClose = () => { setAnchorEl(null); }; + // Render return (
@@ -62,5 +65,5 @@ const ViewMenu: React.FC = React.memo(() => { ); }); -ViewMenu.displayName = "ViewMenu"; -export default ViewMenu; +ViewButton.displayName = "ViewMenu"; +export default ViewButton; diff --git a/src/lib/components/WellCompletions/components/Settings/WellAttributesSelector.stories.tsx b/src/lib/components/WellCompletions/components/Settings/WellAttributesSelector.stories.tsx index 9e377fce5..f6d217646 100644 --- a/src/lib/components/WellCompletions/components/Settings/WellAttributesSelector.stories.tsx +++ b/src/lib/components/WellCompletions/components/Settings/WellAttributesSelector.stories.tsx @@ -10,4 +10,5 @@ export default { const Template = () => ; export const Filter = Template.bind({}); +//Wrap with example intpu data and redux store Filter.decorators = [exampleDataDecorator, withReduxDecorator]; diff --git a/src/lib/components/WellCompletions/components/Settings/WellAttributesSelector.tsx b/src/lib/components/WellCompletions/components/Settings/WellAttributesSelector.tsx index 2bbd0375b..4eec6c289 100644 --- a/src/lib/components/WellCompletions/components/Settings/WellAttributesSelector.tsx +++ b/src/lib/components/WellCompletions/components/Settings/WellAttributesSelector.tsx @@ -23,12 +23,17 @@ const useStyles = makeStyles((theme: Theme) => }, }) ); +/** + * A react component to allow the users to select wells by attribute values + */ const WellAttributesSelector: React.FC = React.memo(() => { // Style const classes = useStyles(); + // Direct access to the input data const data = useContext(DataContext); // Redux const dispatch = useDispatch(); + // All the attribute keys available const attributeKeys = useSelector( (st: WellCompletionsState) => st.attributes.attributeKeys ); @@ -36,10 +41,12 @@ const WellAttributesSelector: React.FC = React.memo(() => { (st: WellCompletionsState) => st.ui.filterByAttributes ); const wells = useMemo(() => data.wells, [data]); + //Create the tree that the SmartNodeSelector can accept as input. const attributesTree = useMemo( () => extractAttributesTree(wells, attributeKeys), [wells] ); + //Create the hint text for the user to better understand how the filter applies. const hintText = useMemo(() => { const allowedValues = computeAllowedAttributeValues(filterByAttributes); return ( @@ -47,21 +54,24 @@ const WellAttributesSelector: React.FC = React.memo(() => { Array.from(allowedValues.entries()) .map( ([key, values]) => + //Within the same attribute, we use OR relation `"${key}" ${ values.size === 1 ? ` is "${Array.from(values)[0]}"` : ` is in [${Array.from(values)}]` }` ) + //In between different attribute key, we use AND relation .join(" and ") ); }, [filterByAttributes]); - // handlers + // Handlers const handleSelectionChange = useCallback( (selection) => dispatch(updateFilterByAttributes(selection.selectedNodes)), [dispatch] ); + // Render return (
; export const Filter = Template.bind({}); -Filter.decorators = [exampleDataDecorator, withReduxDecorator]; +//Wrap with redux store +Filter.decorators = [withReduxDecorator]; diff --git a/src/lib/components/WellCompletions/components/Settings/WellFilter.tsx b/src/lib/components/WellCompletions/components/Settings/WellFilter.tsx index b9e64fa0d..cf9ad8733 100644 --- a/src/lib/components/WellCompletions/components/Settings/WellFilter.tsx +++ b/src/lib/components/WellCompletions/components/Settings/WellFilter.tsx @@ -13,12 +13,16 @@ const useStyles = makeStyles((theme: Theme) => }, }) ); +/** + * A search textfield to search wells by their names + */ const WellFilter: React.FC = React.memo(() => { const classes = useStyles(); // Redux const dispatch = useDispatch(); - // handlers + // Handlers const onChange = useCallback( + // Reduce the update frequency to 0.2 second throttle( (event: React.ChangeEvent) => dispatch(updateWellSearchText(event.target.value)), diff --git a/src/lib/components/WellCompletions/components/Settings/WellPagination.stories.tsx b/src/lib/components/WellCompletions/components/Settings/WellPagination.stories.tsx index 696f603e5..d84379824 100644 --- a/src/lib/components/WellCompletions/components/Settings/WellPagination.stories.tsx +++ b/src/lib/components/WellCompletions/components/Settings/WellPagination.stories.tsx @@ -5,9 +5,10 @@ import WellPagination from "./WellPagination"; export default { component: WellPagination, - title: "WellCompletions/Components/Settings/WellPagination", + title: "WellCompletions/Components/Settings/Well Pagination", }; const Template = () => ; export const Bar = Template.bind({}); +//Wrap with example intpu data and redux store Bar.decorators = [exampleDataDecorator, withReduxDecorator]; diff --git a/src/lib/components/WellCompletions/components/Settings/WellPagination.tsx b/src/lib/components/WellCompletions/components/Settings/WellPagination.tsx index 4186777de..7b9250de8 100644 --- a/src/lib/components/WellCompletions/components/Settings/WellPagination.tsx +++ b/src/lib/components/WellCompletions/components/Settings/WellPagination.tsx @@ -27,6 +27,9 @@ const useStyles = makeStyles(() => }, }) ); +/** + * Divide wells into pages + */ const WellPagination: React.FC = React.memo(() => { const classes = useStyles(); // Redux @@ -38,6 +41,7 @@ const WellPagination: React.FC = React.memo(() => { const wellsPerPage = useSelector( (st: WellCompletionsState) => st.ui.wellsPerPage ); + // Memo const wellsCount = useMemo(() => plotData.wells.length, [plotData]); const pageCount = useMemo( () => Math.ceil(plotData.wells.length / wellsPerPage), @@ -56,19 +60,22 @@ const WellPagination: React.FC = React.memo(() => { () => Math.min(wellsCount, currentClampedPage * wellsPerPage), [currentClampedPage, wellsPerPage, wellsCount] ); - // handlers + // Handlers const onCurrentPageChange = useCallback( (...arg) => dispatch(updateCurrentPage(arg[1])), [dispatch] ); - //effects + + // Effects useEffect(() => { dispatch(updateCurrentPage(currentClampedPage)); }, [currentClampedPage]); + // Render return (
+ {/* Indicates what the current page is displaying*/} {`${startItem} - ${endItem} of ${wellsCount} items`} diff --git a/src/lib/components/WellCompletions/components/Settings/WellsPerPageSelector.stories.tsx b/src/lib/components/WellCompletions/components/Settings/WellsPerPageSelector.stories.tsx index a36310685..1032290d4 100644 --- a/src/lib/components/WellCompletions/components/Settings/WellsPerPageSelector.stories.tsx +++ b/src/lib/components/WellCompletions/components/Settings/WellsPerPageSelector.stories.tsx @@ -4,9 +4,10 @@ import WellsPerPageSelector from "./WellsPerPageSelector"; export default { component: WellsPerPageSelector, - title: "WellCompletions/Components/Settings/WellsPerPage", + title: "WellCompletions/Components/Settings/Wells Per Page", }; const Template = () => ; export const Selector = Template.bind({}); +//Wrap with redux store Selector.decorators = [withReduxDecorator]; diff --git a/src/lib/components/WellCompletions/components/Settings/WellsPerPageSelector.tsx b/src/lib/components/WellCompletions/components/Settings/WellsPerPageSelector.tsx index 4e564460d..a277414b2 100644 --- a/src/lib/components/WellCompletions/components/Settings/WellsPerPageSelector.tsx +++ b/src/lib/components/WellCompletions/components/Settings/WellsPerPageSelector.tsx @@ -14,7 +14,9 @@ const useStyles = makeStyles((theme: Theme) => }, }) ); - +/** + * A drop down for selecting how many wells to display per page + */ const WellsPerPageSelector: React.FC = React.memo(() => { const classes = useStyles(); // Redux @@ -22,11 +24,12 @@ const WellsPerPageSelector: React.FC = React.memo(() => { const wellsPerPage = useSelector( (st: WellCompletionsState) => st.ui.wellsPerPage ); - // handlers + // Handlers const onWellsPerPageChange = useCallback( (event) => dispatch(updateWellsPerPage(event.target.value)), [dispatch] ); + // Render return ( ; export const Selector = Template.bind({}); +//Wrap with example intpu data and redux store Selector.decorators = [exampleDataDecorator, withReduxDecorator]; diff --git a/src/lib/components/WellCompletions/components/Settings/ZoneSelector.tsx b/src/lib/components/WellCompletions/components/Settings/ZoneSelector.tsx index d61f32c03..1e8a9f1fd 100644 --- a/src/lib/components/WellCompletions/components/Settings/ZoneSelector.tsx +++ b/src/lib/components/WellCompletions/components/Settings/ZoneSelector.tsx @@ -8,6 +8,7 @@ import { Zone } from "../../redux/types"; import { findSubzones } from "../../utils/dataUtil"; import { DataContext } from "../DataLoader"; +//Construct a stratigraphy tree as the input of react-dropdown-tree const extractStratigraphyTree = (stratigraphy: Zone[]): TreeNodeProps => { const root: TreeNodeProps = { label: "All", @@ -32,7 +33,7 @@ const extractStratigraphyTree = (stratigraphy: Zone[]): TreeNodeProps => { return root; }; -//DFS +//Find an array of the selected subzones names from the given selectedNodes export const findSelectedZones = ( stratigraphy: Zone[], selectedNodes: TreeNodeProps[] @@ -61,18 +62,21 @@ const useStyles = makeStyles((theme: Theme) => }, }) ); - +/** + * A react component for selecting zones to display in the completions plot + */ const ZoneSelector: React.FC = React.memo(() => { const classes = useStyles(); + // Use input data directly const data = useContext(DataContext); // Redux const dispatch = useDispatch(); - + // Memo const stratigraphyTree = useMemo( () => extractStratigraphyTree(data.stratigraphy), [data.stratigraphy] ); - // handlers + // Handlers const handleSelectionChange = useCallback( (_, selectedNodes) => dispatch( @@ -82,6 +86,7 @@ const ZoneSelector: React.FC = React.memo(() => { ), [dispatch, data.stratigraphy] ); + // Render return ( = React.memo( ({ id, data }: Props) => { const validate = useMemo(() => ajv.compile(inputSchema), []); diff --git a/src/lib/components/WellCompletions/components/WellCompletionsViewer.tsx b/src/lib/components/WellCompletions/components/WellCompletionsViewer.tsx index b8d5f4864..771f7d96c 100644 --- a/src/lib/components/WellCompletions/components/WellCompletionsViewer.tsx +++ b/src/lib/components/WellCompletions/components/WellCompletionsViewer.tsx @@ -80,9 +80,12 @@ const useStyles = makeStyles((theme: Theme) => const WellCompletionsViewer: React.FC = () => { const classes = useStyles(); + // Use input data directly const data = useContext(DataContext); + // Create plot data with the selected time step(s) const plotData = usePlotData(); + // Redux const isDrawerOpen = useSelector( (state: WellCompletionsState) => state.ui.isDrawerOpen ); @@ -92,6 +95,7 @@ const WellCompletionsViewer: React.FC = () => { const currentPage = useSelector( (state: WellCompletionsState) => state.ui.currentPage ); + // Memo const dataInCurrentPage = useMemo(() => { return { ...plotData, @@ -111,8 +115,10 @@ const WellCompletionsViewer: React.FC = () => { //If no data is available if (!data) return
; + // Render return (
+ {/* We detect the resize of the element and resize the plot accordingly */} {({ width }) => ( <> @@ -142,6 +148,7 @@ const WellCompletionsViewer: React.FC = () => { />
+ {/* Drawer on the right-hand side (hidden by default) that shows the filter options */} { return { ...data, wells: data.wells.map((well) => { + //The earliest completion date for the given well let earliestCompDateIndex = Number.POSITIVE_INFINITY; subzones.forEach((zone) => { if (zone in well.completions) { - //store earliest completion date const completion = well.completions[zone]; + //Find the earliest date for the given completion const earliestDate = completion.t.find( (_, index) => completion.open[index] > 0 ); @@ -33,11 +34,11 @@ export const preprocessData = (subzones: string[], data: Data): Data => { ); } }); + //store the earliest completion date return { ...well, earliestCompDateIndex }; }), }; }; - export interface AttributeNode { name: string; children: { name: AttributeType; key: string }[]; @@ -59,13 +60,14 @@ export const extractAttributesTree = ( ); wells.forEach((well) => - attributes.forEach((values, key) => - values.add( + attributes.forEach((valuesSet, key) => + //If the well doesnt have the given attribute key, it means the attribute is undefined for this well + valuesSet.add( key in well.attributes ? well.attributes[key] : "undefined" ) ) ); - + //Return an array of nodes return Array.from(attributes.entries()).map(([key, values]) => ({ name: key, children: Array.from(values) @@ -76,7 +78,11 @@ export const extractAttributesTree = ( })), })); }; - +/** + * Compute the selected nodes from the node selector into a map of attribute keys with their allowed values + * @param filterByAttributes an array of selected node, e.g ["name:Ann","age:37"] + * @returns + */ export const computeAllowedAttributeValues = ( filterByAttributes: string[] ): Map> => { @@ -89,7 +95,11 @@ export const computeAllowedAttributeValues = ( }); return allowValues; }; - +/** + * Create a attribute predicate for well selection + * @param filterByAttributes an array of selected node, e.g ["name:Ann","age:37"] + * @returns + */ export const createAttributePredicate = ( filterByAttributes: string[] ): ((well: Well) => boolean) => { @@ -111,12 +121,18 @@ export const createAttributePredicate = ( } }); }); + //Use an AND logic between different attribute keys return (well: Well) => { return filters.every((filter) => filter(well)); }; }; -//DFS +/** + * DFS to find all leaf nodes + * @param zone + * @param result + * @returns + */ export const findSubzones = (zone: Zone, result: Zone[]): void => { if (zone === undefined) return; if (zone.subzones === undefined || zone.subzones.length === 0) @@ -130,7 +146,6 @@ export const findSubzones = (zone: Zone, result: Zone[]): void => { * @param range * @param timeAggregation * @param hideZeroCompletions - * @param filterByAttributes * @returns */ export const computeDataToPlot = (