|
1 | 1 | import * as React from "react";
|
2 |
| -import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, Link } from "@fluentui/react"; |
| 2 | +import { CommandBar, ContextualMenuItemType, ICommandBarItemProps } from "@fluentui/react"; |
| 3 | +import { TreeItemValue } from "@fluentui/react-components"; |
3 | 4 | import * as ESPRequest from "src/ESPRequest";
|
4 | 5 | import nlsHPCC from "src/nlsHPCC";
|
5 | 6 | import { HelperRow, useWorkunitHelpers } from "../hooks/workunit";
|
6 | 7 | import { HolyGrail } from "../layouts/HolyGrail";
|
7 |
| -import { FluentGrid, useCopyButtons, useFluentStoreState, FluentColumns } from "./controls/Grid"; |
| 8 | +import { DockPanel, DockPanelItem, ResetableDockPanel } from "../layouts/DockPanel"; |
| 9 | +import { pushUrl } from "../util/history"; |
8 | 10 | import { ShortVerticalDivider } from "./Common";
|
9 |
| -import { SearchParams } from "../util/hashUrl"; |
10 |
| -import { hashHistory } from "../util/history"; |
| 11 | +import { FlatItem, HelpersTree } from "./HelpersTree"; |
| 12 | +import { FetchEditor } from "./SourceEditor"; |
11 | 13 |
|
12 |
| -function canShowContent(type: string) { |
13 |
| - switch (type) { |
14 |
| - case "dll": |
15 |
| - return false; |
16 |
| - } |
17 |
| - return true; |
18 |
| -} |
19 |
| - |
20 |
| -function getURL(item: HelperRow, option) { |
| 14 | +function getURL(item: HelperRow, option?) { |
21 | 15 | let params = "";
|
22 | 16 |
|
23 | 17 | const uriEncodedParams: { [key: string]: any } = {
|
@@ -82,165 +76,198 @@ function getURL(item: HelperRow, option) {
|
82 | 76 | return ESPRequest.getBaseURL() + params + (option ? `&Option=${encodeURIComponent(option)}` : "&Option=1");
|
83 | 77 | }
|
84 | 78 |
|
85 |
| -function getTarget(id, row: HelperRow) { |
86 |
| - if (canShowContent(row.Type)) { |
87 |
| - let sourceMode = "text"; |
88 |
| - switch (row.Type) { |
89 |
| - case "ECL": |
90 |
| - sourceMode = "ecl"; |
91 |
| - break; |
92 |
| - case "Workunit XML": |
93 |
| - case "Archive Query": |
94 |
| - case "xml": |
95 |
| - sourceMode = "xml"; |
96 |
| - break; |
97 |
| - } |
98 |
| - return { |
99 |
| - sourceMode, |
100 |
| - url: getURL(row, id) |
101 |
| - }; |
102 |
| - } |
103 |
| - return null; |
104 |
| -} |
105 |
| - |
106 |
| -const defaultUIState = { |
107 |
| - hasSelection: false, |
108 |
| - canShowContent: false |
109 |
| -}; |
110 |
| - |
111 | 79 | interface HelpersProps {
|
112 | 80 | wuid: string;
|
| 81 | + mode?: "ecl" | "xml" | "text" | "yaml"; |
| 82 | + url?: string; |
| 83 | + parentUrl?: string; |
113 | 84 | }
|
114 | 85 |
|
115 | 86 | export const Helpers: React.FunctionComponent<HelpersProps> = ({
|
116 |
| - wuid |
| 87 | + wuid, |
| 88 | + mode, |
| 89 | + url, |
| 90 | + parentUrl = `/workunits/${wuid}/helpers` |
117 | 91 | }) => {
|
118 | 92 |
|
119 |
| - const [uiState, setUIState] = React.useState({ ...defaultUIState }); |
| 93 | + const [fullscreen, setFullscreen] = React.useState<boolean>(false); |
| 94 | + const [dockpanel, setDockpanel] = React.useState<ResetableDockPanel>(); |
120 | 95 | const [helpers, refreshData] = useWorkunitHelpers(wuid);
|
121 |
| - const [data, setData] = React.useState<any[]>([]); |
122 |
| - const { |
123 |
| - selection, setSelection, |
124 |
| - setTotal, |
125 |
| - refreshTable } = useFluentStoreState({}); |
126 |
| - |
127 |
| - // Grid --- |
128 |
| - const columns = React.useMemo((): FluentColumns => { |
129 |
| - return { |
130 |
| - sel: { |
131 |
| - width: 27, |
132 |
| - selectorType: "checkbox" |
133 |
| - }, |
134 |
| - Type: { |
135 |
| - label: nlsHPCC.Type, |
136 |
| - width: 160, |
137 |
| - formatter: (Type, row) => { |
138 |
| - const target = getTarget(row.id, row); |
139 |
| - if (target) { |
140 |
| - const searchParams = new SearchParams(hashHistory.location.search); |
141 |
| - searchParams.param("mode", encodeURIComponent(target.sourceMode)); |
142 |
| - searchParams.param("src", encodeURIComponent(target.url)); |
143 |
| - const linkText = Type.replace("Slave", "Worker") + (row?.Description ? " (" + row.Description + ")" : ""); |
144 |
| - return <Link href={`#/workunits/${row?.workunit?.Wuid}/helpers/${row.Type}?${searchParams.serialize()}`}>{linkText}</Link>; |
| 96 | + const [noDataMsg, setNoDataMsg] = React.useState(""); |
| 97 | + const [checkedRows, setCheckedRows] = React.useState([]); |
| 98 | + const [checkedItems, setCheckedItems] = React.useState([]); |
| 99 | + const [selection, setSelection] = React.useState<HelperRow>(); |
| 100 | + const [treeItems, setTreeItems] = React.useState<FlatItem[]>([]); |
| 101 | + const [openItems, setOpenItems] = React.useState<Iterable<TreeItemValue>>([]); |
| 102 | + |
| 103 | + React.useEffect(() => { |
| 104 | + helpers.forEach(helper => { |
| 105 | + helper.Orig = { |
| 106 | + url: getURL(helper), |
| 107 | + ...helper.Orig |
| 108 | + }; |
| 109 | + }); |
| 110 | + }, [helpers]); |
| 111 | + |
| 112 | + React.useEffect(() => { |
| 113 | + setSelection(helpers.filter(item => url === getURL(item))[0]); |
| 114 | + }, [helpers, url]); |
| 115 | + |
| 116 | + React.useEffect(() => { |
| 117 | + if (helpers.length && selection !== undefined) { |
| 118 | + setNoDataMsg(selection?.Type === "dll" ? nlsHPCC.CannotDisplayBinaryData : ""); |
| 119 | + } |
| 120 | + }, [helpers, selection]); |
| 121 | + |
| 122 | + React.useEffect(() => { |
| 123 | + const rows = []; |
| 124 | + checkedItems.forEach(value => { |
| 125 | + const filtered = helpers.filter(row => row.id === value || row?.Name?.indexOf(value) > -1)[0]; |
| 126 | + if (treeItems.filter(item => item.value === value)[0].url && filtered) { |
| 127 | + rows.push(filtered); |
| 128 | + } |
| 129 | + }); |
| 130 | + setCheckedRows(rows); |
| 131 | + }, [checkedItems, helpers, treeItems]); |
| 132 | + |
| 133 | + React.useEffect(() => { |
| 134 | + const flatTreeItems: FlatItem[] = []; |
| 135 | + helpers.forEach(helper => { |
| 136 | + let parentValue; |
| 137 | + const helperPath = helper.Path?.replace("/var/lib/HPCCSystems/", ""); |
| 138 | + const fileName = helper.Name?.split("/").pop(); |
| 139 | + if (helperPath) { |
| 140 | + const pathDirs = helperPath.split("/"); |
| 141 | + let parentFolder; |
| 142 | + let folderName; |
| 143 | + for (let i = 0; i < pathDirs.length; i++) { |
| 144 | + folderName = parentFolder ? parentFolder + "/" + pathDirs[i] : pathDirs[i]; |
| 145 | + if (flatTreeItems.filter(item => item.value === folderName).length === 0) { |
| 146 | + flatTreeItems.push({ |
| 147 | + value: folderName, |
| 148 | + content: pathDirs[i], |
| 149 | + parentValue: parentFolder, |
| 150 | + url: "" |
| 151 | + }); |
| 152 | + } |
| 153 | + if (!parentFolder) { |
| 154 | + parentFolder = pathDirs[i]; |
| 155 | + } else { |
| 156 | + parentFolder += "/" + pathDirs[i]; |
145 | 157 | }
|
146 |
| - return Type; |
147 | 158 | }
|
148 |
| - }, |
149 |
| - Description: { |
150 |
| - label: nlsHPCC.Description |
151 |
| - }, |
152 |
| - FileSize: { |
153 |
| - label: nlsHPCC.FileSize, |
154 |
| - width: 90, |
155 |
| - justify: "right" |
| 159 | + flatTreeItems.push({ |
| 160 | + value: helperPath + "/" + fileName, |
| 161 | + content: fileName, |
| 162 | + fileSize: helper.FileSize, |
| 163 | + parentValue: parentFolder, |
| 164 | + url: helper.Orig.url |
| 165 | + }); |
| 166 | + } else { |
| 167 | + flatTreeItems.push({ |
| 168 | + value: helper.id, |
| 169 | + parentValue: parentValue ?? undefined, |
| 170 | + content: helper.Description ?? helper.Name ?? helper.Type, |
| 171 | + fileSize: helper.FileSize, |
| 172 | + url: helper.Orig.url |
| 173 | + }); |
156 | 174 | }
|
157 |
| - }; |
158 |
| - }, []); |
| 175 | + }); |
| 176 | + setTreeItems(flatTreeItems.sort((a, b) => { |
| 177 | + if (a.parentValue === undefined && b.parentValue === undefined) return 0; |
| 178 | + if (a.parentValue === undefined) return -1; |
| 179 | + if (b.parentValue === undefined) return 1; |
| 180 | + return a.parentValue?.toString().localeCompare(b.parentValue?.toString(), undefined, { ignorePunctuation: false }); |
| 181 | + })); |
| 182 | + setOpenItems(flatTreeItems.map(item => item.value)); |
| 183 | + }, [helpers]); |
| 184 | + |
| 185 | + const treeItemLeafNodes = React.useMemo(() => { |
| 186 | + return treeItems.filter(item => item.url !== ""); |
| 187 | + }, [treeItems]); |
159 | 188 |
|
160 | 189 | // Command Bar ---
|
161 | 190 | const buttons = React.useMemo((): ICommandBarItemProps[] => [
|
162 | 191 | {
|
163 | 192 | key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
|
164 |
| - onClick: () => refreshData() |
| 193 | + onClick: () => { |
| 194 | + refreshData(); |
| 195 | + pushUrl(`${parentUrl}`); |
| 196 | + } |
165 | 197 | },
|
166 | 198 | { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
|
167 | 199 | {
|
168 |
| - key: "open", text: nlsHPCC.Open, disabled: !uiState.canShowContent, iconProps: { iconName: "WindowEdit" }, |
| 200 | + key: "selectAll", text: checkedItems.length === treeItemLeafNodes.length ? nlsHPCC.DeselectAll : nlsHPCC.SelectAll, iconProps: { iconName: checkedItems.length === treeItemLeafNodes.length ? "Checkbox" : "CheckboxComposite" }, |
169 | 201 | onClick: () => {
|
170 |
| - if (selection.length === 1) { |
171 |
| - const target = getTarget(selection[0].id, selection[0]); |
172 |
| - if (target) { |
173 |
| - window.location.href = `#/text?mode=${target.sourceMode}&src=${encodeURIComponent(target.url)}`; |
174 |
| - } |
| 202 | + if (checkedItems.length < treeItemLeafNodes.length) { |
| 203 | + setCheckedItems(treeItemLeafNodes.map(i => i.value)); |
175 | 204 | } else {
|
176 |
| - for (let i = 0; i < selection.length; ++i) { |
177 |
| - const target = getTarget(selection[i].id, selection[i]); |
178 |
| - if (target) { |
179 |
| - window.open(`#/text?mode=${target.sourceMode}&src=${encodeURIComponent(target.url)}`, "_blank"); |
180 |
| - } |
181 |
| - } |
| 205 | + setCheckedItems([]); |
182 | 206 | }
|
183 | 207 | }
|
184 | 208 | },
|
185 |
| - { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> }, |
186 | 209 | {
|
187 |
| - key: "file", text: nlsHPCC.File, disabled: !uiState.hasSelection, iconProps: { iconName: "Download" }, |
| 210 | + key: "file", text: nlsHPCC.File, disabled: checkedRows.filter(item => item.url !== "").length === 0, iconProps: { iconName: "Download" }, |
188 | 211 | onClick: () => {
|
189 |
| - selection.forEach(item => { |
| 212 | + checkedRows.forEach(item => { |
190 | 213 | window.open(getURL(item, 1));
|
191 | 214 | });
|
192 | 215 | }
|
193 | 216 | },
|
194 | 217 | {
|
195 |
| - key: "zip", text: nlsHPCC.Zip, disabled: !uiState.hasSelection, iconProps: { iconName: "Download" }, |
| 218 | + key: "zip", text: nlsHPCC.Zip, disabled: checkedRows.filter(item => item.url !== "").length === 0, iconProps: { iconName: "Download" }, |
196 | 219 | onClick: () => {
|
197 |
| - selection.forEach(item => { |
| 220 | + checkedRows.forEach(item => { |
198 | 221 | window.open(getURL(item, 2));
|
199 | 222 | });
|
200 | 223 | }
|
201 | 224 | },
|
202 | 225 | {
|
203 |
| - key: "gzip", text: nlsHPCC.GZip, disabled: !uiState.hasSelection, iconProps: { iconName: "Download" }, |
| 226 | + key: "gzip", text: nlsHPCC.GZip, disabled: checkedRows.filter(item => item.url !== "").length === 0, iconProps: { iconName: "Download" }, |
204 | 227 | onClick: () => {
|
205 |
| - selection.forEach(item => { |
| 228 | + checkedRows.forEach(item => { |
206 | 229 | window.open(getURL(item, 3));
|
207 | 230 | });
|
208 | 231 | }
|
209 | 232 | }
|
| 233 | + ], [checkedItems, checkedRows, parentUrl, refreshData, treeItemLeafNodes]); |
210 | 234 |
|
211 |
| - ], [refreshData, selection, uiState.canShowContent, uiState.hasSelection]); |
| 235 | + const rightButtons = React.useMemo((): ICommandBarItemProps[] => [ |
| 236 | + { |
| 237 | + key: "fullscreen", title: nlsHPCC.MaximizeRestore, iconProps: { iconName: fullscreen ? "ChromeRestore" : "FullScreen" }, |
| 238 | + onClick: () => setFullscreen(!fullscreen) |
| 239 | + } |
| 240 | + ], [fullscreen]); |
212 | 241 |
|
213 |
| - const copyButtons = useCopyButtons(columns, selection, "helpers"); |
| 242 | + const setSelectedItem = React.useCallback((selId: string) => { |
| 243 | + pushUrl(`${parentUrl}/${selId}`); |
| 244 | + }, [parentUrl]); |
214 | 245 |
|
215 |
| - // Selection --- |
216 | 246 | React.useEffect(() => {
|
217 |
| - const state = { ...defaultUIState }; |
218 |
| - |
219 |
| - selection.forEach(row => { |
220 |
| - state.hasSelection = true; |
221 |
| - if (canShowContent(row.Type)) { |
222 |
| - state.canShowContent = true; |
| 247 | + if (dockpanel) { |
| 248 | + // Should only happen once on startup --- |
| 249 | + const layout: any = dockpanel.layout(); |
| 250 | + if (Array.isArray(layout?.main?.sizes) && layout.main.sizes.length === 2) { |
| 251 | + layout.main.sizes = [0.3, 0.7]; |
| 252 | + dockpanel.layout(layout).lazyRender(); |
223 | 253 | }
|
224 |
| - }); |
225 |
| - setUIState(state); |
226 |
| - }, [selection]); |
227 |
| - |
228 |
| - React.useEffect(() => { |
229 |
| - setData(helpers); |
230 |
| - }, [helpers]); |
| 254 | + } |
| 255 | + }, [dockpanel]); |
231 | 256 |
|
232 | 257 | return <HolyGrail
|
233 |
| - header={<CommandBar items={buttons} farItems={copyButtons} />} |
| 258 | + header={<CommandBar items={buttons} farItems={rightButtons} />} |
234 | 259 | main={
|
235 |
| - <FluentGrid |
236 |
| - data={data} |
237 |
| - primaryID={"id"} |
238 |
| - alphaNumColumns={{ Value: true }} |
239 |
| - columns={columns} |
240 |
| - setSelection={setSelection} |
241 |
| - setTotal={setTotal} |
242 |
| - refresh={refreshTable} |
243 |
| - ></FluentGrid> |
| 260 | + <DockPanel hideSingleTabs onCreate={setDockpanel}> |
| 261 | + <DockPanelItem key="helpersTable" title="Helpers"> |
| 262 | + { // Only render after archive is loaded (to ensure it "defaults to open") --- |
| 263 | + helpers?.length && |
| 264 | + <HelpersTree treeItems={treeItems} openItems={openItems} setOpenItems={setOpenItems} checkedItems={checkedItems} setCheckedItems={setCheckedItems} setSelectedItem={setSelectedItem} selectedUrl={url} /> |
| 265 | + } |
| 266 | + </DockPanelItem> |
| 267 | + <DockPanelItem key="helperEditor" title="Helper" padding={4} location="split-right" relativeTo="helpersTable"> |
| 268 | + <FetchEditor url={helpers && selection?.Type && selection?.Type !== "dll" ? url : null} mode={mode} noDataMsg={noDataMsg}></FetchEditor> |
| 269 | + </DockPanelItem> |
| 270 | + </DockPanel> |
244 | 271 | }
|
245 | 272 | />;
|
246 | 273 | };
|
0 commit comments