· 5 years ago · Jul 01, 2020, 05:08 PM
1import React, { Component, Fragment, useState, useRef, useEffect } from "react";
2import {
3 Button,
4 Form,
5 Input,
6 InputNumber,
7 Modal,
8 notification,
9 Select,
10 Spin,
11 Table,
12 Row,
13 Col
14} from "antd";
15import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
16import Highlighter from "react-highlight-words";
17import { setTimeout } from "timers";
18import style from "../style/TestMatrixGen.module.css";
19
20/* These are required to use cookies. */
21import { instanceOf } from 'prop-types';
22import { withCookies, Cookies } from 'react-cookie';
23
24const { Search } = Input;
25const { Option } = Select;
26
27/* Error notification window, used for when SQL is unable to complete fetch call or receives BadRequest() return. */
28const ErrorNotification = function (type, functionName, duration = 0) {
29 var message =
30 "Please refresh the page and try again. If the problem persists, please contact the system administrator.";
31 if (type === "Warning")
32 message = "The item you have tried to add already exists in the table.";
33
34 /* Notify user of error. */
35 notification[type.toLowerCase()]({
36 message: type + " in " + functionName,
37 description: message,
38 duration: duration,
39 placement: "topRight",
40 style: { width: 500, marginLeft: -120, marginTop: 30 }
41 });
42};
43
44/* Form for creating a new Tag in the Tag_List database table. */
45const TagForm = ({
46 visible,
47 operation,
48 type,
49 dataSource,
50 selectedVal,
51 onCreate,
52 onCancel,
53 buttonIcon
54}) => {
55 /* Define form to be able to manipulate values. */
56 const [form] = Form.useForm();
57
58 /* Getters/Setters for some state variables. */
59 var [testTools, setTestTools] = useState([]);
60 var [testTypes, setTestTypes] = useState([]);
61 var [tagCategories, setTagCategories] = useState([]);
62 var [tagCompatibilities, setTagCompatibilities] = useState([]);
63 var [compatType, setCompatType] = useState("");
64 var [validType, setValidType] = useState("");
65
66 /* Is called once after initial render, sort of like componentDidMount.
67 * Gets list of test tools/types and tag categories/compatibilities. */
68 useEffect(() => {
69 setTestTools(dataSource
70 .filter(item => { return item.tagCategory === "Test Tool" })
71 .map(item => item.tagName)
72 );
73
74 setTestTypes(dataSource
75 .filter(item => { return item.tagCategory === "Test Type" })
76 .map(item => item.tagName)
77 );
78
79 setTagCategories([...new Set(dataSource.map(item => item["tagCategory"]))]);
80
81 setTagCompatibilities([]);
82 if (selectedVal.compatibility !== "") {
83 /* Split compatibility string on delimiter ("|"),
84 * remove last item because it's an empty string. */
85 var temp = selectedVal.compatibility.split("|");
86 temp.pop();
87 setTagCompatibilities(temp);
88 }
89
90 /* Only show the additional form field if the tag type is "Test Type". */
91 var tempValid = false;
92 if (
93 selectedVal.tagCategory === "Test Type" ||
94 selectedVal.tagCategory === "Test Tool"
95 )
96 tempValid = true;
97
98 /* If tag type is "Test Type", show Test Tools and vice versa. */
99 var tempType = "";
100 if (selectedVal.tagCategory === "Test Type")
101 tempType = "Test Tool";
102 else if (selectedVal.tagCategory === "Test Tool")
103 tempType = "Test Type";
104
105 setCompatType(tempType);
106 setValidType(tempValid);
107 }, []);
108
109 /* Generates a list of attributes from the TagCategories in the Tag_List table. */
110 const selectBuilder = (tags = null) => {
111 if (tags === null) {
112 tags = [...testTools];
113 if (compatType === "Test Type")
114 tags = [...testTypes];
115 }
116
117 var tempArray = [];
118 for (let i = 0; i < tags.length; i++)
119 tempArray.push(
120 <Option key={i} value={tags[i]}>
121 {tags[i]}
122 </Option>
123 );
124
125 return tempArray;
126 }
127
128 /* Checks the currently selected type. If the type if 'Test' or 'Tool'
129 * then another select box is displayed that allows the user to enter
130 * compatibility (tool compatible with x,y,z types, vice versa). */
131 const handleTypeChange = (e) => {
132 if (e === "Test Type") {
133 setValidType(true);
134 setCompatType("Test Tool")
135 } else if (e === "Test Tool") {
136 setValidType(true);
137 setCompatType("Test Type");
138 } else {
139 setValidType(false);
140 setCompatType("");
141 }
142 }
143
144 return (
145 <Modal
146 visible={visible}
147 title={<strong>Enter information about Tag</strong>}
148 cancelText="Cancel"
149 okText={buttonIcon === "plus" ?
150 <>
151 <PlusOutlined />
152 {operation}
153 </>
154 :
155 <>
156 <EditOutlined />
157 {operation}
158 </>
159 }
160 onCancel={onCancel}
161 onOk={() => {
162 form
163 .validateFields()
164 .then(values => {
165 form.resetFields()
166 onCreate(values);
167 })
168 .catch(info => {
169 console.log(operation + " " + type + " failed: ", info);
170 });
171 }}
172 width="400px"
173 centered
174 bodyStyle={{ padding: "10px" }}
175 >
176 <Form form={form} layout="vertical" name="TagForm">
177 <Form.Item
178 className={style.formItem}
179 name="tagName"
180 label="Tag Name"
181 initialValue={selectedVal.tagName ? selectedVal.tagName : undefined}
182 rules={[{ required: true, message: "Please enter a name for the tag" }]}
183 >
184 <Input placeholder="Enter a name for the tag..." />
185 </Form.Item>
186
187 <Form.Item
188 className={style.formItem}
189 name="tagCategory"
190 label="Tag Type"
191 initialValue={selectedVal.tagCategory ? selectedVal.tagCategory : undefined}
192 rules={[{ required: true, message: "Please choose a tag type" }]}
193 >
194 <Select placeholder="Select a category for the tag..." onChange={e => handleTypeChange(e)}>
195 {selectBuilder(tagCategories)}
196 </Select>
197 </Form.Item>
198
199 <Form.Item
200 className={style.formItem}
201 name="tagCompatibility"
202 label={"Compatible " + compatType + "s"}
203 initialValue={tagCompatibilities}
204 rules={[{ required: validType, message: "Please choose compatible " + compatType.toLowerCase() + "s" }]}
205 style={{ display: validType ? 'inline' : 'none' }}
206 >
207 <Select
208 mode="multiple"
209 placeholder={
210 "Select compatible " +
211 compatType.toLowerCase() +
212 "s"
213 }
214 >
215 {selectBuilder()}
216 </Select>
217 </Form.Item>
218 </Form>
219 </Modal>
220 );
221};
222
223/* Form for creating a new NIC in the NIC_Catalog database table. */
224const NicForm = ({
225 visible,
226 operation,
227 type,
228 selectedVal,
229 onCreate,
230 onCancel,
231 buttonIcon
232}) => {
233 /* Define form to be able to manipulate values. */
234 const [form] = Form.useForm();
235
236 /* Creates an Input form item. Saves space in the form definition. */
237 const createInput = (
238 colSpan,
239 itemLength,
240 itemName,
241 itemDI,
242 itemPlaceholder,
243 longMessage = true,
244 num = false
245 ) => {
246 /* Choose whether input is a number or a string. */
247 var inputVar;
248 if (num) {
249 inputVar = (
250 <InputNumber
251 placeholder={itemPlaceholder}
252 min={1}
253 style={{ width: "100%" }}
254 />
255 );
256 } else {
257 inputVar = (
258 <Input
259 placeholder={itemPlaceholder}
260 maxLength={itemLength}
261 />
262 );
263 }
264
265 /* Choose whether to print a long required message or just 'required'. */
266 var itemMessage = "\xa0Required";
267 if (longMessage) {
268 itemMessage =
269 "\xa0Please enter a " +
270 itemName.toLowerCase() +
271 " for the NIC";
272 }
273
274 return (
275 <Col span={colSpan}>
276 <Form.Item
277 className={style.formItem}
278 name={itemDI}
279 label={itemName}
280 initialValue={selectedVal[itemDI]}
281 rules={[{ required: true, message: itemMessage }]}
282 >
283 {inputVar}
284 </Form.Item>
285 </Col>
286 );
287 }
288
289 return (
290 <Modal
291 visible={visible}
292 title={<strong>Enter information about NIC</strong>}
293 cancelText="Cancel"
294 okText={
295 <div>
296 {buttonIcon === "plus" ?
297 <PlusOutlined />
298 :
299 <EditOutlined />
300 }
301 {operation} {type}
302 </div>
303 }
304 onCancel={onCancel}
305 onOk={() => {
306 form
307 .validateFields()
308 .then(values => {
309 form.resetFields();
310 onCreate(values);
311 })
312 .catch(info => {
313 console.log(operation + " " + type + " failed: ", info);
314 });
315 }}
316 width="500px"
317 centered
318 bodyStyle={{ padding: "10px" }}
319 >
320 <Form form={form} layout="vertical">
321 <Row gutter={10}>
322 {createInput(
323 17,
324 255,
325 "Common Name",
326 "commonName",
327 "e.g. 'X810-XXV-4'"
328 )}
329 {createInput(
330 7,
331 0,
332 "Port Count",
333 "portCount",
334 "e.g. '2'",
335 false,
336 true
337 )}
338 </Row>
339 <Row gutter={10}>
340 {createInput(
341 17,
342 255,
343 "Code Name",
344 "codeName",
345 "e.g. 'Salem Channel QP SFP'"
346 )}
347 {createInput(
348 7,
349 0,
350 "Link Speed (Gb/s)",
351 "linkSpeed",
352 "e.g. '10'",
353 false,
354 true
355 )}
356 </Row>
357 <Row gutter={16}>
358 {createInput(
359 24,
360 255,
361 "Branding String",
362 "brandingString",
363 "e.g. 'Intel(R) Ethernet Converged Network Adapter X710'"
364 )}
365 </Row>
366 <Row gutter={16}>
367 <div style={{ marginTop: 10, marginBottom: 10 }}>
368 <h2 style={{ display: "inline" }}>
369 PCI ID
370 </h2>
371 Note: If unknown,
372 enter '####'.
373 </div>
374 </Row>
375 <Row gutter={16}>
376 {createInput(
377 6,
378 4,
379 "Vendor ID",
380 "vendorId",
381 "####",
382 false
383 )}
384 {createInput(
385 6,
386 4,
387 "Device ID",
388 "deviceId",
389 "####",
390 false
391 )}
392 {createInput(
393 6,
394 4,
395 "Sub Ven ID",
396 "subVendorId",
397 "####",
398 false
399 )}
400 {createInput(
401 6,
402 4,
403 "Sub Dev ID",
404 "subDeviceId",
405 "####",
406 false
407 )}
408 </Row>
409 </Form>
410 </Modal>
411 );
412}
413
414/* Simple function for handling which form to use, NIC or Tag. */
415class FormHandler extends Component {
416 state = { visible: false };
417
418 static defaultProps = {
419 defaultTag: {
420 tagName: "",
421 tagCategory: "",
422 compatibility: ""
423 },
424 defaultNic: {
425 commonName: "",
426 brandingString: "",
427 codeName: "",
428 portCount: null,
429 linkSpeed: null,
430 vendorId: "",
431 deviceId: "",
432 subVendorId: "",
433 subDeviceId: ""
434 }
435 };
436
437 /* Get validated form values, pass to parent through callback. Also close form. */
438 onCreate = values => {
439 console.log('Received values of form: ', values);
440 this.setState({ visible: false });
441
442 //this.props.callback(values);
443 };
444
445 /* Close the form, destroying its contents. */
446 onCancel = () => {
447 this.setState({ visible: false });
448 }
449
450 render() {
451 var { visible } = this.state;
452 var {
453 operation,
454 type,
455 dataSourceProp,
456 selectedVal,
457 defaultTag,
458 defaultNic
459 } = this.props;
460
461 /* Determine the type of button (regular or primary) and button icon (plus or edit) */
462 var buttonType = "primary";
463 var buttonIcon = "plus";
464 if (operation === "Edit") {
465 buttonType = "default";
466 buttonIcon = "edit";
467 }
468
469 /* If an item is not currently selected when the button is hit,
470 * or there is more than one item selected, give the defaultTag/NIC
471 * (an empty tag/NIC object) to the form so there will be no initial values.
472 * This is so add and edit can share the same form. */
473 var tagProp = selectedVal;
474 var nicProp = selectedVal;
475 if (typeof selectedVal === "undefined") {
476 tagProp = defaultTag;
477 nicProp = defaultNic;
478 }
479
480 return (
481 <div>
482 <Button
483 className={style.settingsTableButton}
484 type={buttonType}
485 onClick={() => {
486 this.setState({ visible: true });
487 }}
488 >
489 {buttonIcon === "edit" ?
490 <EditOutlined />
491 :
492 <PlusOutlined />
493 }
494 {operation + " " + type}
495 </Button>
496 {type === "Tag" ?
497 <TagForm
498 visible={visible}
499 operation={operation}
500 type={type}
501 dataSource={dataSourceProp}
502 selectedVal={tagProp}
503 onCreate={this.onCreate}
504 onCancel={this.onCancel}
505 buttonIcon={buttonIcon}
506 />
507 :
508 <NicForm
509 visible={visible}
510 operation={operation}
511 type={type}
512 selectedVal={nicProp}
513 onCreate={this.onCreate}
514 onCancel={this.onCancel}
515 buttonIcon={buttonIcon}
516 />
517 }
518 </div>
519 );
520 }
521}
522
523/* Stateful class for tables in the settings page, so that the whole
524 * page doesn't have to be rerendered when a row in the table is selected. */
525class SettingsTable extends Component {
526 state = {
527 dataSource: this.props.dataSourceProp,
528 dataSourceTemp: this.props.dataSourceProp,
529 selectedRowKeys: [],
530 searchText: "",
531 };
532
533 /* This causes this child component to rerender when the dataSourceProp (which is equal to a state in the parent class, SettingsPage) is changed.
534 * This allows for a very fast update of the table data when a tag is added/edited/archive is toggled. */
535 static getDerivedStateFromProps(props, state) {
536 if (props.dataSourceProp !== state.dataSource) {
537 return { dataSourceTemp: props.dataSourceProp };
538 }
539 return null;
540 }
541
542 constructor(props) {
543 super(props);
544
545 //this.getColumns(null);
546 }
547
548 /* Wait until the component mounts to update the columns. */
549 componentDidMount = () => {
550 this.getColumns(null, true);
551 }
552
553 /* Re-sets the table columns. This allows filters and highlighting to be applied. */
554 getColumns = (filteredInfoParam, update = false) => {
555 if (update) {
556 this.setState({ filteredInfo: filteredInfoParam });
557 }
558 var filteredInfo = filteredInfoParam || {};
559
560 var columnsTemp = [...this.props.columnsProp];
561
562 for (let i = 0; i < columnsTemp.length; i++) {
563 /* Correctly handles applying filters to child columns (like in the PCI ID column of the NIC_Catalog table). */
564 if (typeof columnsTemp[i].children !== "undefined") {
565 for (let j = 0; j < columnsTemp[i].children.length; j++) {
566 if (
567 typeof columnsTemp[i].children[j].filters !==
568 "undefined"
569 )
570 columnsTemp[i].children[j].filters = this.filterList(
571 columnsTemp[i].children[j].dataIndex
572 );
573
574 columnsTemp[i].children[j].filteredValue =
575 filteredInfo[columnsTemp[i].children[j].dataIndex] ||
576 null;
577
578 columnsTemp[i].children[j].render = (text, record) => {
579 if (record.isArchive === true) {
580 record.color = "#ff0000";
581 }
582 return (
583 <div
584 style={{
585 textAlign: columnsTemp[i].children[j]
586 .alignText
587 }}
588 >
589 <Highlighter
590 highlightClassName={style.highlighter}
591 searchWords={[this.state.searchText]}
592 autoEscape
593 textToHighlight={text.toString()}
594 />
595 </div>
596 );
597 };
598 }
599 } else {
600 if (
601 typeof columnsTemp[i].filters !== "undefined" &&
602 columnsTemp[i].dataIndex !== "isArchive"
603 )
604 columnsTemp[i].filters = this.filterList(
605 columnsTemp[i].dataIndex
606 );
607
608 columnsTemp[i].filteredValue =
609 filteredInfo[columnsTemp[i].dataIndex] || null;
610
611 if (columnsTemp[i].dataIndex === "isArchive") {
612 columnsTemp[i].render = (text, record) => {
613 /* IsArchive in SQL table, but "Active?" in tool, so switching true/false
614 * so that it makes sense. Data is still stored the correct way. */
615 if (record["isArchive"] === true) text = "False";
616 else text = "True";
617
618 return (
619 <div style={{ textAlign: "center" }}>{text}</div>
620 );
621 };
622 } else {
623 columnsTemp[i].render = (text, record) => {
624 if (text === "Virtualization Technology") {
625 text = "Virt. Tech.";
626 } else if (text === "PCIe Generation") {
627 text = "PCIe Gen.";
628 }
629 if (record.isArchive === true) {
630 record.color = "#ff0000";
631 }
632 return (
633 <div
634 style={{ textAlign: columnsTemp[i].alignText }}
635 >
636 <Highlighter
637 highlightClassName={style.highlighter}
638 searchWords={[this.state.searchText]}
639 autoEscape
640 textToHighlight={text.toString()}
641 />
642 </div>
643 );
644 };
645 }
646 }
647 }
648
649 if (update) {
650 this.setState({ columns: columnsTemp });
651 this.forceUpdate();
652 }
653 };
654
655 /* Make a list of all of the unique values in a column, then convert the list into a properly-formatted object.
656 * This feels like a roundabout way of doing it, but the alternative is to list all of the filters manually */
657 filterList = columnName => {
658 /* Sort function treats all numbers as if they were hexadecimal so that PCIID can be sorted. */
659 var title = [
660 ...new Set(
661 this.props.dataSourceProp
662 .map(item => item[columnName])
663 .sort(function(a, b) {
664 return parseInt(a, 16) - parseInt(b, 16);
665 })
666 )
667 ];
668
669 for (var i = 0; i < title.length; i++)
670 title[i] = { text: title[i], value: title[i] };
671
672 return title;
673 };
674
675 /* Called when search button is pressed. Searches all fields within
676 * the table and displays only rows with entries that match. */
677 tableSearch = searchString => {
678 /* Check to see if any of the attributes contain searched text. */
679 this.setState({ searchText: searchString });
680 var temp = [...this.props.dataSourceProp];
681
682 var locations = [];
683 var found = false;
684 let i = 0;
685 Object.values(temp).forEach(function(val) {
686 Object.values(val).forEach(function(str) {
687 if (typeof str === "string") {
688 if (
689 str.toLowerCase().includes(searchString.toLowerCase())
690 ) {
691 found = true;
692 return;
693 }
694 }
695 });
696
697 if (found) {
698 locations.push(temp[i]);
699 found = false;
700 }
701
702 i++;
703 });
704
705 if (searchString.length > 0) this.setState({ dataSource: locations });
706 else this.setState({ dataSource: temp });
707
708 setTimeout(() => {
709 this.getColumns(this.state.filteredInfo, true);
710 }, 5000);
711 };
712
713 /* Called when a filter is selected. Updates filteredInfo state and re-fetches columns. */
714 handleFilterChange = (pagination, filters) => {
715 this.getColumns(filters, true);
716 };
717
718 /* Called when the 'clear filters' button next to the search bar is pressed. Does exactly what it says. */
719 clearFilters = () => {
720 this.getColumns(null, true);
721 };
722
723 /* Called when a row checkbox is checked. Updates selectedRowKeys state. */
724 onRowSelect = selectedRowKeys => {
725 this.setState({ selectedRowKeys });
726 };
727
728 /* Handler function to call the toggleIsArchive function in the SettingsPage class. */
729 archiveCallback = async () => {
730 /* Wait until the callback function has finished. */
731 await this.props.toggleArchiveCallback(
732 this.state.selectedRowKeys,
733 this.props.type
734 );
735
736 /* Once the callback is dont, update the datasource to the newly-passed one. */
737 await this.setState({
738 selectedRowKeys: [],
739 dataSource: this.state.dataSourceTemp
740 });
741 };
742
743 /* Handler function to call the addItem function in the SettingsPage class. */
744 addCallback = async (callbackItem, id) => {
745 await this.props.addItemCallback(callbackItem, this.props.type);
746 await this.setState({ dataSource: this.state.dataSourceTemp });
747 };
748
749 /* Handler function to call editItem functions in the SettingsPage class. */
750 editCallback = async callbackItem => {
751 await this.props.editItemCallback(
752 callbackItem,
753 this.props.type,
754 this.state.selectedRowKeys
755 );
756 await this.setState({ dataSource: this.state.dataSourceTemp });
757
758 /* Fixes a memory leak where setState occurs while the component is re-rendering upon dataSource update. */
759 setTimeout(() => {
760 this.setState({ selectedRowKeys: [] });
761 }, 10);
762 };
763
764 render() {
765 const { selectedRowKeys } = this.state;
766
767 const rowSelection = {
768 selectedRowKeys,
769 onChange: this.onRowSelect,
770 fixed: this.props.fixRowSelect,
771 columnWidth: 35
772 };
773
774 return (
775 <Fragment>
776 <Search
777 style={{
778 width: 'calc(100% - 107px)',
779 display: 'inline-block',
780 marginBottom: 5,
781 marginRight: 5
782 }}
783 placeholder="Search"
784 onSearch={record => this.tableSearch(record)}
785 enterButton
786 allowClear={true}
787 />
788 <Button
789 className={style.clearFilterButton}
790 onClick={this.clearFilters}
791 >
792 Clear filters
793 </Button>
794 <Table
795 rowClassName={record => {
796 if (record.isArchive) {
797 return style.tableArchivedRow;
798 }
799 }}
800 size="small"
801 bordered
802 rowSelection={rowSelection}
803 dataSource={this.state.dataSource}
804 columns={this.state.columns}
805 pagination={{
806 defaultPageSize: 30,
807 showSizeChanger: true,
808 pageSizeOptions: ["10", "30", "100"]
809 }}
810 style={this.props.tableStyle}
811 scroll={this.props.tableScroll}
812 rowKey="id"
813 onChange={this.handleFilterChange}
814 />
815
816 {/* Add button. Brings up form to add new item to respective table. */}
817 <FormHandler
818 type={this.props.type}
819 callBack={this.addCallback}
820 dataSourceProp={this.props.dataSourceProp}
821 operation="Add"
822 />
823
824 {/* Shows how many items in the table are selected. Only shows if there is more than one item selected. */}
825 {selectedRowKeys.length > 1 ? (
826 <div className={style.itemsSelected}>
827 {selectedRowKeys.length} {this.props.type}s selected
828 </div>
829 ) : null}
830 {/* Button to archive currently selected items. Only shows if there is one or more items selected. */}
831 {selectedRowKeys.length > 0 ? (
832 <Button
833 className={style.settingsTableButton}
834 onClick={() => this.archiveCallback()}
835 type="danger"
836 ghost
837 >
838 <DeleteOutlined />
839 Toggle Active
840 </Button>
841 ) : null}
842 {/* Edit button. Brings up form to edit item in respective table. Only shows if exactly one item is selected. */}
843 {selectedRowKeys.length === 1 ? (
844 <FormHandler
845 type={this.props.type}
846 callBack={this.editCallback}
847 selectedVal={this.state.dataSource.find(
848 key => key.id === this.state.selectedRowKeys[0]
849 )}
850 dataSourceProp={this.props.dataSourceProp}
851 operation="Edit"
852 />
853 ) : null}
854 </Fragment>
855 );
856 }
857}
858
859/* Settings page for the test matrix generator. Allows manipulation of the Tag_List and NIC_Catalog SQL tables. */
860class SettingsPage extends Component {
861 /* Required to use cookies in this component. */
862 static propTypes = {
863 cookies: instanceOf(Cookies).isRequired
864 };
865
866 constructor(props) {
867 super(props);
868
869 /* Define cookies, can now use cookies.set and cookies.get */
870 const { cookies } = props;
871
872 if (typeof cookies.get('testCookie') !== 'undefined') {
873 var temp = cookies.get('testCookie');
874 console.log("temp cookie: ", temp);
875 }
876
877 /* This is an example of how to set a cookie:
878 * 1st arg: the name of the cookie
879 * 2nd arg: the value being stored in the cookie (string)
880 * 3rd arg: settings for the cookie:
881 * - path: what page(s) can access the cookie. '/' means all pages
882 * - maxAge: the duration of the cookie in seconds
883 */
884 cookies.set('testCookie', "This is a test cookie. Looks like it works!", { path: '/', maxAge: 1000 });
885
886 this.state = {
887 tags: [],
888 nics: [],
889 finishedLoading: false,
890 fetchError: false,
891 windowWidth: this.props.windowWidth,
892 windowHeight: this.props.windowHeight,
893 tableHeight: this.props.windowHeight * 0.99 - 355
894 };
895 }
896
897 /* Fetches tables from SQL then builds table columns. */
898 componentDidMount = async () => {
899 /* Fetch the NIC_Catalog and Tag_List tables from SQL. */
900 await this.fetchSQL("getTagList", "tags");
901 await this.fetchSQL("getNicCatalog", "nics");
902
903 /* Build the columns for each table so they can be passed as props to the SettingsTable class.
904 * This timeout is necessary. Must wait until data is fetched from SQL before attempting to build this.nicColumns,
905 * as it requires said data. Cannot use async because it's in the constructor, so this is the best alternative.*/
906 this.getColumns();
907 this.setState({ finishedLoading: true });
908 };
909
910 /* Called when the window is resized. Gets window dimensions passed from App.js. */
911 componentDidUpdate = (prevProps) => {
912 if (prevProps.windowWidth !== this.props.windowWidth || prevProps.windowHeight !== this.props.windowHeight) {
913 this.setState({
914 windowWidth: this.props.windowWidth,
915 windowHeight: this.props.windowHeight,
916 tableHeight: this.props.windowHeight * 0.99 - 355
917 });
918 }
919 }
920
921 /* Builds the columns. Column definitions are in this function instead of the constructor
922 * so that they may be rebuilt after adding an item to the database so that the filter lists update. */
923 getColumns = () => {
924 let { filteredInfo } = this.state;
925 filteredInfo = filteredInfo || {};
926
927 /* Column definitions for the Tag_List table. */
928 this.tagColumns = [
929 {
930 id: 1,
931 title: "Tag Type",
932 dataIndex: "tagCategory",
933 width: 100,
934 alignText: "left",
935 filters: null,
936 filteredValue: filteredInfo.tagCategory || null,
937 onFilter: (value, record) => {
938 if (record["tagCategory"] === value) {
939 return true;
940 } else {
941 return false;
942 }
943 }
944 },
945 {
946 id: 2,
947 title: "Tag Name",
948 dataIndex: "tagName",
949 sorter: (a, b) => {
950 return a["tagName"].localeCompare(b["tagName"]);
951 },
952 alignText: "left"
953 },
954 {
955 id: 3,
956 title: "Active?",
957 dataIndex: "isArchive",
958 filters: [
959 { text: "True", value: "True" },
960 { text: "False", value: "False" }
961 ],
962 onFilter: (value, record) => {
963 if (record.isArchive.toString() === value.toLowerCase()) {
964 return false;
965 } else {
966 return true;
967 }
968 },
969 width: 85
970 }
971 ];
972
973 /* Column definitions for NIC selection table. */
974 this.nicColumns = [
975 {
976 ...this.getColumnProps(
977 1,
978 "Common Name",
979 "commonName",
980 110,
981 false,
982 false,
983 "left"
984 )
985 },
986 {
987 ...this.getColumnProps(
988 2,
989 "Branding String",
990 "brandingString",
991 340,
992 false,
993 false,
994 "left"
995 )
996 },
997 {
998 ...this.getColumnProps(
999 3,
1000 "Code Name",
1001 "codeName",
1002 220,
1003 false,
1004 false,
1005 "left"
1006 )
1007 },
1008 {
1009 ...this.getColumnProps(
1010 4,
1011 "Port Count",
1012 "portCount",
1013 90,
1014 true,
1015 true
1016 )
1017 },
1018 {
1019 ...this.getColumnProps(
1020 5,
1021 "Link Speed (Gb/s)",
1022 "linkSpeed",
1023 100,
1024 true,
1025 true
1026 )
1027 },
1028 {
1029 title: "PCI ID",
1030 children: [
1031 {
1032 ...this.getColumnProps(
1033 6,
1034 "Ven",
1035 "vendorId",
1036 80,
1037 true,
1038 false
1039 )
1040 },
1041 {
1042 ...this.getColumnProps(
1043 7,
1044 "Dev",
1045 "deviceId",
1046 80,
1047 false,
1048 false
1049 )
1050 },
1051 {
1052 ...this.getColumnProps(
1053 8,
1054 "Sub V",
1055 "subVendorId",
1056 80,
1057 true,
1058 false
1059 )
1060 },
1061 {
1062 ...this.getColumnProps(
1063 9,
1064 "Sub D",
1065 "subDeviceId",
1066 80,
1067 false,
1068 false
1069 )
1070 }
1071 ]
1072 },
1073 {
1074 id: 10,
1075 title: "Active?",
1076 dataIndex: "isArchive",
1077 filters: [
1078 { text: "True", value: "True" },
1079 { text: "False", value: "False" }
1080 ],
1081 onFilter: (value, record) => {
1082 if (record.isArchive.toString() === value.toLowerCase()) {
1083 return false;
1084 } else {
1085 return true;
1086 }
1087 },
1088 width: 80,
1089 fixed: "right"
1090 }
1091 ];
1092 };
1093
1094 /* Dynamically creates all of the props for each column. Saves on space. */
1095 getColumnProps = (
1096 idNum,
1097 colName,
1098 colDI,
1099 colWidth,
1100 hasFilter,
1101 isNum,
1102 colAlign = "center"
1103 ) => {
1104 let { filteredInfo } = this.state;
1105 filteredInfo = filteredInfo || {};
1106
1107 var colProps = {};
1108 colProps.id = idNum;
1109 colProps.title = colName;
1110 colProps.dataIndex = colDI;
1111 colProps.width = colWidth;
1112 colProps.alignText = colAlign;
1113
1114 if (!hasFilter && !isNum) {
1115 colProps.sorter = (a, b) => {
1116 return a[colDI].localeCompare(b[colDI]);
1117 };
1118 } else if (hasFilter && isNum) {
1119 colProps.filters = null;
1120 colProps.filteredValue = filteredInfo[colDI] || null;
1121 colProps.onFilter = (value, record) => {
1122 if (record[colDI] === value) {
1123 return true;
1124 } else {
1125 return false;
1126 }
1127 };
1128 colProps.sorter = (a, b) => {
1129 return a[colDI] - b[colDI];
1130 };
1131 } else if (hasFilter && !isNum) {
1132 colProps.filters = null;
1133 colProps.filteredValue = filteredInfo[colDI] || null;
1134 colProps.onFilter = (value, record) => {
1135 if (record[colDI] === value) {
1136 return true;
1137 } else {
1138 return false;
1139 }
1140 };
1141 }
1142
1143 return colProps;
1144 };
1145
1146 /* Makes GET API call to fetch the data from the table specified in the urlName parameter. */
1147 fetchSQL = async (urlName, stateName) => {
1148 await fetch("api/testMatrixController/" + urlName, {
1149 headers: {
1150 "Content-Type": "application/json",
1151 Accept: "application/json"
1152 }
1153 })
1154 .then(response => {
1155 if (response.status === 200) return response.json();
1156 else {
1157 this.setState({ fetchError: true });
1158 return null;
1159 }
1160 })
1161 .then(data => {
1162 /* Delete dateCreated and dateModified, since they aren't used. */
1163 if (data) {
1164 data.forEach(obj => {
1165 delete obj.dateCreated;
1166 delete obj.dateModified;
1167 });
1168 this.setState({ [stateName]: data });
1169 }
1170 });
1171 };
1172
1173 /* Makes PATCH API call to update the IsArchive column of the entry(s) specified
1174 * by the 'items' parameter in the table specified by the 'type' parameter. */
1175 toggleIsArchive = async (items, type) => {
1176 var error = null;
1177 var uri = "tagArchiveToggle";
1178 if (type === "NIC") {
1179 uri = "nicArchiveToggle";
1180 }
1181
1182 await fetch("api/testMatrixController/" + uri, {
1183 method: "PATCH",
1184 body: JSON.stringify(items),
1185 headers: {
1186 "Content-Type": "application/json",
1187 Accept: "application/json"
1188 }
1189 }).then(response => {
1190 if (response.status === 200) {
1191 return response.json();
1192 } else {
1193 error = response;
1194 return null;
1195 }
1196 });
1197
1198 if (error !== null) {
1199 /* Throw an error notification if the fetch call was not successful. */
1200 ErrorNotification(
1201 "toggleIsArchive(): " + error.status + " " + error.statusText
1202 );
1203 } else {
1204 /* Chooses which table to re-fetch from SQL after changes are made. */
1205 if (type === "Tag") await this.fetchSQL("getTagList", "tags");
1206 else await this.fetchSQL("getNicCatalog", "nics");
1207 }
1208 };
1209
1210 addItem = async (callbackItem, type) => {
1211 var error = null;
1212 var uri = "addTag";
1213 if (type === "NIC") {
1214 uri = "addNic";
1215 }
1216 await fetch("api/testMatrixController/" + uri, {
1217 method: "POST",
1218 body: JSON.stringify(callbackItem),
1219 headers: {
1220 "Content-Type": "application/json",
1221 Accept: "application/json"
1222 }
1223 }).then(response => {
1224 if (response.status === 200) {
1225 return response.json();
1226 } else {
1227 error = response;
1228 return null;
1229 }
1230 });
1231
1232 /* Throw an error notification if the fetch call was not successful. If item already exists, notify user of conflict. */
1233 if (error !== null && error.status !== 409) {
1234 ErrorNotification(
1235 "Error",
1236 "addItem(): " + error.status + " " + error.statusText
1237 );
1238 } else if (error !== null && error.status === 409) {
1239 ErrorNotification(
1240 "Warning",
1241 "addItem(): " + error.status + " " + error.statusText,
1242 5
1243 );
1244 } else {
1245 var successMessage = "";
1246
1247 /* Chooses which table to re-fetch from SQL after changes are made. */
1248 if (type === "Tag") {
1249 successMessage = "Tag Added!";
1250 await this.fetchSQL("getTagList", "tags");
1251 } else {
1252 successMessage = "NIC Added!";
1253 await this.fetchSQL("getNicCatalog", "nics");
1254 }
1255
1256 /* Notify user of success. */
1257 notification["success"]({
1258 message: successMessage,
1259 duration: 2,
1260 placement: "topRight",
1261 style: { width: 260, marginLeft: 150, marginTop: 40 }
1262 });
1263
1264 this.forceUpdate();
1265 }
1266 };
1267
1268 editItem = async (callbackItem, type, id) => {
1269 var error = null;
1270 var uri = "editTag";
1271 if (type === "NIC") {
1272 uri = "editNic";
1273 }
1274 await fetch("api/testMatrixController/" + uri + "/" + id, {
1275 method: "PATCH",
1276 body: JSON.stringify(callbackItem),
1277 headers: {
1278 "Content-Type": "application/json",
1279 Accept: "application/json"
1280 }
1281 }).then(response => {
1282 if (response.status === 200) {
1283 return response.json();
1284 } else {
1285 error = response;
1286 return null;
1287 }
1288 });
1289
1290 if (error !== null && error.status !== 204) {
1291 /* Throw an error notification if the fetch call was not successful. */
1292 ErrorNotification(
1293 "Error",
1294 "editItem(): " + error.status + " " + error.statusText
1295 );
1296 } else if (error === null) {
1297 var successMessage = "";
1298 /* Chooses which table to re-fetch from SQL after changes are made. */
1299 if (type === "Tag") {
1300 successMessage = "Tag Edited!";
1301 await this.fetchSQL("getTagList", "tags");
1302 } else {
1303 successMessage = "NIC Edited!";
1304 await this.fetchSQL("getNicCatalog", "nics");
1305 }
1306
1307 /* Notify user of success. */
1308 notification["success"]({
1309 message: successMessage,
1310 duration: 2,
1311 placement: "topRight",
1312 style: { width: 260, marginLeft: 150, marginTop: 40 }
1313 });
1314
1315 this.forceUpdate();
1316 }
1317 };
1318
1319 /* If there was an error fetching the tables, throw an alert to tell the user. */
1320 didTablesFetch() {
1321 if (!this.alertPresent) {
1322 this.alertPresent = true;
1323 if (this.state.fetchError) {
1324 alert(
1325 "Unable to fetch tables from SQL. Try refreshing the page.",
1326 [
1327 {
1328 text: "OK",
1329 onPress: () => {
1330 this.alertPresent = false;
1331 }
1332 }
1333 ],
1334 { cancelable: false }
1335 );
1336 } else {
1337 this.alertPresent = false;
1338 }
1339 }
1340 }
1341
1342 render() {
1343 const { nics, tags, finishedLoading, fetchError } = this.state;
1344
1345 /* Renders an empty page with just the title, a non-functioning return button, and a spinner.
1346 * This is so there's time to fetch the data from SQL before rendering the components, without
1347 * leaving a blank screen. Rendering before data is fetched would crash the site. */
1348 if (!finishedLoading || fetchError) {
1349 this.didTablesFetch();
1350
1351 return (
1352 <Fragment>
1353 <div className={style.matrixHeader}></div>
1354 <Spin
1355 size="large"
1356 style={{
1357 position: "absolute",
1358 top: "45%",
1359 left: "48.8%"
1360 }}
1361 />
1362 </Fragment>
1363 );
1364 } else {
1365 return (
1366 <Fragment>
1367 <div className={style.matrixHeader}>
1368 <span style={{ fontSize: 30, marginLeft: 10 }}><strong>Settings</strong></span>
1369 </div>
1370
1371 {/* Tag and NIC tables. Must use conditional rendering so data actually exists when components attempt to use it. */}
1372 <div className={style.settingsContainer}>
1373 {/* Flex container for the tag table/buttons. */}
1374 <div className={style.settingsTagTableContainer}>
1375 <h2>
1376 <strong>Tags</strong>
1377 </h2>
1378
1379 <SettingsTable
1380 addItemCallback={this.addItem}
1381 editItemCallback={this.editItem}
1382 toggleArchiveCallback={this.toggleIsArchive}
1383 dataSourceProp={tags}
1384 columnsProp={this.tagColumns}
1385 fixRowSelect={false}
1386 tableScroll={{ y: this.state.tableHeight }}
1387 type="Tag"
1388 />
1389 </div>
1390
1391 {/* Flex container for the NIC table/buttons. */}
1392 <div className={style.settingsNicTableContainer}>
1393 <h2>
1394 <strong>NIC Catalog</strong>
1395 </h2>
1396
1397 <SettingsTable
1398 addItemCallback={this.addItem}
1399 editItemCallback={this.editItem}
1400 toggleArchiveCallback={this.toggleIsArchive}
1401 dataSourceProp={nics}
1402 columnsProp={this.nicColumns}
1403 fixRowSelect={true}
1404 tableScroll={{
1405 x: 1290,
1406 y: this.state.tableHeight - 40
1407 }}
1408 type="NIC"
1409 />
1410 </div>
1411 </div>
1412 </Fragment>
1413 );
1414 }
1415 }
1416}
1417
1418/* Must export the component withCookies to be able to use cookies. */
1419export default withCookies(SettingsPage);