From 7ecf4151cd551cf8d8d10a9b1623760a13dfe3db Mon Sep 17 00:00:00 2001 From: CHANDAN PRAKASH Date: Thu, 19 Sep 2024 15:24:33 -0400 Subject: [PATCH] Migrate Dashboard page to component --- assets/js/Ioda/pages/dashboard/Dashboard.js | 601 ++++++++++---------- 1 file changed, 304 insertions(+), 297 deletions(-) diff --git a/assets/js/Ioda/pages/dashboard/Dashboard.js b/assets/js/Ioda/pages/dashboard/Dashboard.js index 5688267..ae7a892 100644 --- a/assets/js/Ioda/pages/dashboard/Dashboard.js +++ b/assets/js/Ioda/pages/dashboard/Dashboard.js @@ -1,5 +1,5 @@ // React Imports -import React, { Component } from "react"; +import React, {useEffect, useState} from "react"; // Internationalization import T from "i18n-react"; // Data Hooks @@ -37,192 +37,221 @@ import { useParams, useNavigate } from "react-router-dom"; const TAB_VIEW_MAP = "map"; const TAB_VIEW_TIME_SERIES = "timeSeries"; -class Dashboard extends Component { - constructor(props) { - super(props); - - this.tabs = { +const Dashboard = (props) =>{ + + const { + tab, + suggestedSearchResults, + summary, + eventSignals, + searchSummaryAction, + totalOutagesAction, + getEventSignalsAction + } = props; + const tabs = { country: country.type, region: region.type, asn: asn.type, }; - this.countryTab = T.translate("dashboard.countryTabTitle"); - this.regionTab = T.translate("dashboard.regionTabTitle"); - this.asnTab = T.translate("dashboard.asnTabTitle"); - this.apiQueryLimit = 170; + const countryTab = String(T.translate("dashboard.countryTabTitle")); + const regionTab = String(T.translate("dashboard.regionTabTitle")); + const asnTab = String(T.translate("dashboard.asnTabTitle")); + const apiQueryLimit = 170; const { urlFromDate, urlUntilDate } = getDateRangeFromUrl(); const urlEntityType = props.entityType; - const entityType = this.tabs[urlEntityType] ? urlEntityType : country.type; - - this.state = { - mounted: false, - // Control Panel - from: urlFromDate ?? getPreviousMinutesAsUTCSecondRange(24 * 60).start, - until: urlUntilDate ?? getNowAsUTCSeconds(), - // Tabs - activeTabType: entityType, - //Tab View Changer Button - tabCurrentView: - entityType === asn.type ? TAB_VIEW_TIME_SERIES : TAB_VIEW_MAP, - // Map Data - topoData: null, - topoScores: null, - // Summary Table - summaryDataRaw: null, - summaryDataProcessed: [], - // Determine when data is available for table so multiple calls to populate the table aren't made - genSummaryTableDataProcessed: false, - totalOutages: 0, - // Summary Table Pagination - apiPageNumber: 0, - // Event Data for Time Series - eventDataRaw: [], - eventDataProcessed: [], - eventOrderByAttr: "score", - eventOrderByOrder: "desc", - eventEndpointCalled: false, - totalEventCount: 0, - }; - } - - componentDidMount() { - // trigger api calls with valid date ranges - const timeDiff = this.state.until - this.state.from; + const entityType = tabs[urlEntityType] ? urlEntityType : country.type; + + const [mounted, setMounted] = useState(false); + const [from, setFrom] = useState(urlFromDate ?? getPreviousMinutesAsUTCSecondRange(24 * 60).start); + const [until, setUntil] = useState(urlUntilDate ?? getNowAsUTCSeconds()); + const [activeTabType, setActiveTabType] = useState(entityType); + //Tab View Changer Button + const [tabCurrentView, setTabCurrentView] = useState(entityType === asn.type ? TAB_VIEW_TIME_SERIES : TAB_VIEW_MAP); + const [topoData, setTopoData] = useState(null); + const [topoScores, setTopoScores] = useState(null); + // Summary Table + const [summaryDataRaw, setSummaryDataRaw] = useState(null); + const [summaryDataProcessed, setSummaryDataProcessed] = useState([]); + // Determine when data is available for table so multiple calls to populate the table aren't made + const [genSummaryTableDataProcessed, setGenSummaryTableDataProcessed] = useState(false); + const [totalOutages, setTotalOutages] = useState(0); + // Summary Table Pagination + const [apiPageNumber, setApiPageNumber] = useState(0); + // Event Data for Time Series + const [eventDataRaw, setEventDataRaw] = useState([]); + const [eventDataProcessed, setEventDataProcessed] = useState([]); + const [eventOrderByAttr, setEventOrderByAttr] = useState("score"); + const [eventOrderByOrder, setEventOrderByOrder] = useState("desc"); + const [eventEndpointCalled, setEventEndpointCalled] = useState(false); + const [totalEventCount, setTotalEventCount] = useState(0); + const [displayDashboardTimeRangeError, setDisplayDashboardTimeRangeError] = useState(false); + const [searchResults, setSearchResults] = useState([]); + + useEffect(() => { + const timeDiff = until - from; if (timeDiff <= 0) { - this.setState({ - displayDashboardTimeRangeError: true, - }); + setDisplayDashboardTimeRangeError(true); } else if (timeDiff < dashboardTimeRangeLimit) { - this.setState({ mounted: true }, () => { - // Set initial tab to load - this.handleSelectTab(this.tabs[this.state.activeTabType]); - // Get topo and outage data to populate map and table - if (this.state.activeTabType !== asn.type) { - this.getDataTopo(this.state.activeTabType); - } - this.getDataOutageSummary(this.state.activeTabType); - this.getTotalOutages(this.state.activeTabType); - }); + setMounted(true); + handleSelectTab(tabs[activeTabType]); + // Get topo and outage data to populate map and table + if (activeTabType !== asn.type) { + getDataTopo(activeTabType); + } + getDataOutageSummary(activeTabType); + getTotalOutages(ctiveTabType); } - } + }, [until]); - componentWillUnmount() { - this.setState({ - mounted: false, - }); - } - - componentDidUpdate(prevProps, prevState) { + useEffect(() => { // A check to prevent repetitive selection of the same tab - if (this.props.tab !== prevProps.tab) { - this.handleSelectTab(this.tabs[prevProps.tab]); + handleSelectTab(tabs[tab]); + if(activeTabType) { + setActiveTabType(activeTabType !== asn.type ? getDataTopo(activeTabType) : null); + getDataOutageSummary(activeTabType); + getTotalOutages(activeTabType); } + }, [tab]); - // Update visualizations when tabs are changed - if ( - this.state.activeTabType && - this.state.activeTabType !== prevState.activeTabType - ) { - // Get updated topo and outage data to populate map, no topo for asns - this.state.activeTabType !== asn.type - ? this.getDataTopo(this.state.activeTabType) - : null; - this.getDataOutageSummary(this.state.activeTabType); - this.getTotalOutages(this.state.activeTabType); + useEffect(() => { + // After API call for suggested search results completes, update suggestedSearchResults state with fresh data + setSearchResults(suggestedSearchResults); + }, [suggestedSearchResults]); + + useEffect(() => { + // After API call for outage summary data completes, pass summary data to map function for data merging + setSummaryDataRaw(summary); + getMapScores(); + _convertValuesForSummaryTable(); + if(activeTabType === asn.type) { + getDataEvents(activeTabType); + } + if(!eventEndpointCalled) { + setEventEndpointCalled(!eventEndpointCalled); + const totalEventCount = summaryDataRaw.reduce( + (acc, item) => acc + item.event_cnt, + 0 + ); + //Get total event count to reference with event data + setTotalEventCount(totalEventCount); } + }, [summary]); + + useEffect(() => { + // After API call for total outages summary data completes, pass total count to table to populate in UI + setTotalOutages(props.totalOutages.length) + }, [props.totalOutages]); + + // Make API call for data to populate time series stacked horizon view + useEffect(() => { + const newEventData = eventSignals; + setEventDataRaw((prevEventDataRaw) => [...prevEventDataRaw, eventSignals]); + convertValuesForHtsViz(); + }, [eventSignals]); + + + // componentDidUpdate(prevProps, prevState) { + // A check to prevent repetitive selection of the same tab + + + // Update visualizations when tabs are changed + // if ( + // this.state.activeTabType && + // this.state.activeTabType !== prevState.activeTabType + // ) { + // // Get updated topo and outage data to populate map, no topo for asns + // this.state.activeTabType !== asn.type + // ? this.getDataTopo(this.state.activeTabType) + // : null; + // this.getDataOutageSummary(this.state.activeTabType); + // this.getTotalOutages(this.state.activeTabType); + // } // After API call for suggested search results completes, update suggestedSearchResults state with fresh data - if ( - this.props.suggestedSearchResults !== prevProps.suggestedSearchResults - ) { - this.setState({ - suggestedSearchResults: this.props.suggestedSearchResults, - }); - } + // if ( + // this.props.suggestedSearchResults !== prevProps.suggestedSearchResults + // ) { + // this.setState({ + // suggestedSearchResults: this.props.suggestedSearchResults, + // }); + // } // After API call for outage summary data completes, pass summary data to map function for data merging - if (this.props.summary !== prevProps.summary) { - this.setState({ summaryDataRaw: this.props.summary }, () => { - this.getMapScores(); - this.convertValuesForSummaryTable(); - if (this.state.activeTabType === asn.type) { - this.getDataEvents(this.state.activeTabType); - } - - if (!this.state.eventEndpointCalled) { - this.setState( - { eventEndpointCalled: !this.state.eventEndpointCalled }, - () => { - const totalEventCount = this.state.summaryDataRaw.reduce( - (acc, item) => acc + item.event_cnt, - 0 - ); - //Get total event count to reference with event data - this.setState({ totalEventCount }); - } - ); - } - }); - } + // if (this.props.summary !== prevProps.summary) { + // this.setState({ summaryDataRaw: this.props.summary }, () => { + // this.getMapScores(); + // this.convertValuesForSummaryTable(); + // if (this.state.activeTabType === asn.type) { + // this.getDataEvents(this.state.activeTabType); + // } + // + // if (!this.state.eventEndpointCalled) { + // this.setState( + // { eventEndpointCalled: !this.state.eventEndpointCalled }, + // () => { + // const totalEventCount = this.state.summaryDataRaw.reduce( + // (acc, item) => acc + item.event_cnt, + // 0 + // ); + // //Get total event count to reference with event data + // this.setState({ totalEventCount }); + // } + // ); + // } + // }); + // } // After API call for total outages summary data completes, pass total count to table to populate in UI - if (this.props.totalOutages !== prevProps.totalOutages) { - this.setState({ - totalOutages: this.props.totalOutages.length, - }); - } + // if (this.props.totalOutages !== prevProps.totalOutages) { + // this.setState({ + // totalOutages: this.props.totalOutages.length, + // }); + // } // Make API call for data to populate time series stacked horizon view - if (this.props.eventSignals !== prevProps.eventSignals) { - let newEventData = this.props.eventSignals; - this.setState( - (prevState) => ({ - eventDataRaw: [...prevState.eventDataRaw, newEventData], - }), - () => { - this.convertValuesForHtsViz(); - } - ); - } - } + // if (this.props.eventSignals !== prevProps.eventSignals) { + // let newEventData = this.props.eventSignals; + // this.setState( + // (prevState) => ({ + // eventDataRaw: [...prevState.eventDataRaw, newEventData], + // }), + // () => { + // this.convertValuesForHtsViz(); + // } + // ); + // } + // } // Control Panel // manage the date selected in the input - handleTimeFrame = ({ from, until }) => { - if (this.state.from === from && this.state.until === until) { + const handleTimeFrame = ({ from, until }) => { + if (from === from && until === until) { return; } - const { navigate } = this.props; - - this.setState( - { - from, - until, - summaryDataRaw: null, - topoData: null, - summaryDataProcessed: [], - tabCurrentView: "map", - eventDataRaw: [], - eventDataProcessed: [], - displayDashboardTimeRangeError: false, - }, - () => { - // Get topo and outage data to repopulate map and table - this.getDataTopo(this.state.activeTabType); - this.getDataOutageSummary(this.state.activeTabType); - this.getTotalOutages(this.state.activeTabType); - } - ); + const {navigate} = props; + + setFrom(from); + setUntil(until); + setsummaryDataRaw(null); + setTopoData(null); + setSummaryDataProcessed([]); + setTabCurrentView("map"); + setEventDataRaw([]); + setEventDataProcessed([]); + setDisplayDashboardTimeRangeError(false); + getDataTopo(activeTabType); + getDataOutageSummary(activeTabType); + getTotalOutages(activeTabType); navigate(`/dashboard?from=${from}&until=${until}`); - }; + } // Tabbing // Function to map active tab to state and manage url - handleSelectTab = (selectedTab) => { + const handleSelectTab = (selectedTab) => { const { navigate } = this.props; // use tab property to determine active tab by index let activeTabType, url; @@ -239,89 +268,85 @@ class Dashboard extends Component { return; } - // set new tab - this.setState({ - activeTabType: activeTabType, - // Trigger Data Update for new tab - tabCurrentView: - activeTabType === asn.type ? TAB_VIEW_TIME_SERIES : TAB_VIEW_MAP, - topoData: null, - topoScores: null, - summaryDataRaw: null, - genSummaryTableDataProcessed: false, - eventDataRaw: [], - eventDataProcessed: null, - eventEndpointCalled: false, - totalEventCount: 0, - }); + setActiveTabType(activeTabType); + // Trigger Data Update for new tab + setTabCurrentView( + activeTabType === asn.type ? TAB_VIEW_TIME_SERIES : TAB_VIEW_MAP); + setTopoData(null); + setTopoScores(null); + setSummaryDataRaw(null); + setGenSummaryTableDataProcessed(false); + setEventDataRaw([]); + setEventDataProcessed(null); + setEventEndpointCalled(false); + setTotalEventCount(0); + if (hasDateRangeInUrl()) { - navigate(`${url}/?from=${this.state.from}&until=${this.state.until}`); + navigate(`${url}/?from=${from}&until=${until}`); } else { navigate(url); } }; - handleTabChangeViewButton = () => { - if (this.state.tabCurrentView === "map") { - this.setState({ tabCurrentView: "timeSeries" }, () => { - this.getDataEvents(this.state.activeTabType); - }); - } else if (this.state.tabCurrentView === "timeSeries") { - this.setState({ tabCurrentView: "map" }); + const handleTabChangeViewButton = () => { + if (tabCurrentView === "map") { + setTabCurrentView("timeSeries"); + getDataEvents(activeTabType); + } else if (tabCurrentView === "timeSeries") { + setTabCurrentView("map"); } }; // Outage Data // Make API call to retrieve summary data to populate on map - getDataOutageSummary = (entityType) => { - if (!this.state.mounted) { + const getDataOutageSummary = (entityType) => { + if (!mounted) { return; - } else if (this.state.until - this.state.from >= dashboardTimeRangeLimit) { + } else if (until - from >= dashboardTimeRangeLimit) { return; } const includeMetadata = true; const entityCode = null; - this.props.searchSummaryAction( - this.state.from, - this.state.until, - entityType, - entityCode, - this.apiQueryLimit, - this.state.apiPageNumber, - includeMetadata - ); - }; + searchSummaryAction( + from, + until, + entityType, + entityCode, + apiQueryLimit, + apiPageNumber, + includeMetadata); + } - getTotalOutages = (entityType) => { - if (!this.state.mounted) { + const getTotalOutages = (entityType) => { + if (!mounted) { return; } - this.props.totalOutagesAction( - this.state.from, - this.state.until, + totalOutagesAction( + from, + until, entityType ); }; // Map - getMapScores = () => { - if (this.state.topoData && this.state.summaryDataRaw) { - let topoData = this.state.topoData; + const getMapScores = () => { + if (topoData && summaryDataRaw) { + const topoData = topoData; let scores = []; // get Topographic info for a country if it has outages - this.state.summaryDataRaw.map((outage) => { + summaryDataRaw.map((outage) => { let topoItemIndex; - this.state.activeTabType === country.type - ? (topoItemIndex = this.state.topoData.features.findIndex( + activeTabType === country.type + ? (topoItemIndex = topoData.features.findIndex( (topoItem) => topoItem.properties.usercode === outage.entity.code )) - : this.state.activeTabType === region.type - ? (topoItemIndex = this.state.topoData.features.findIndex( + : activeTabType === region.type + ? (topoItemIndex = topoData.features.findIndex( (topoItem) => topoItem.properties.name === outage.entity.name )) : null; @@ -336,58 +361,50 @@ class Dashboard extends Component { scores.sort((a, b) => a - b); } }); - this.setState({ topoScores: scores }); + setTopoScores(scores); } }; // Make API call to retrieve topographic data - getDataTopo = (entityType) => { + const getDataTopo = async (entityType) => { let topologyObjectName = - entityType == "country" + entityType === "country" ? "ne_10m_admin_0.countries" : "ne_10m_admin_1.regions"; - if (this.state.mounted) { - getTopoAction(entityType) - .then((data) => - topojson.feature( - data[entityType].topology, - data[entityType].topology.objects[topologyObjectName] - ) - ) - .then((data) => - this.setState( - { - topoData: data, - }, - this.getMapScores - ) - ); + if (mounted) { + const data = await getTopoAction(entityType); + const geoData = topojson.feature( + data[entityType].topology, + data[entityType].topology.objects[topologyObjectName] + ); + setTopoData(data); + getMapScores(); } }; // function to manage when a user clicks a country in the map - handleEntityShapeClick = (entity) => { - const { navigate } = this.props; + const handleEntityShapeClick = (entity) => { + const { navigate } = props; // Use usercode for country, id for other types const entityCode = - this.state.activeTabType === country.type + activeTabType === country.type ? entity.properties.usercode : entity.properties.id; - let path = `/${this.state.activeTabType}/${entityCode}`; + let path = `/${activeTabType}/${entityCode}`; if (hasDateRangeInUrl()) { - path = `${path}/?from=${this.state.from}&until=${this.state.until}`; + path = `${path}/?from=${from}&until=${until}`; } navigate(path); }; // Event Time Series - getDataEvents(entityType) { - let until = this.state.until; - let from = this.state.from; - let attr = this.state.eventOrderByAttr; - let order = this.state.eventOrderByOrder; - let entities = this.state.summaryDataRaw + const getDataEvents = (entityType) => { + let until = until; + let from = from; + let attr = eventOrderByAttr; + let order = eventOrderByOrder; + let entities = summaryDataRaw .map((entity) => { // some entities don't return a code to be used in an api call, seem to default to '??' in that event if (entity.entity.code !== "??") { @@ -395,7 +412,7 @@ class Dashboard extends Component { } }) .toString(); - this.props.getEventSignalsAction( + getEventSignalsAction( entityType, entities, from, @@ -405,29 +422,27 @@ class Dashboard extends Component { ); } - convertValuesForHtsViz() { + const convertValuesForHtsViz = () => { let eventDataProcessed = []; // Create visualization-friendly data objects - this.state.eventDataRaw.map((entity) => { + eventDataRaw.map((entity) => { let series; series = convertTsDataForHtsViz(entity); eventDataProcessed = eventDataProcessed.concat(series); }); // Add data objects to state for each data source - this.setState({ - eventDataProcessed: eventDataProcessed, - }); + setEventDataProcessed(eventDataProcessed); } // Define what happens when user clicks suggested search result entry - handleResultClick = (entity) => { + const handleResultClick = (entity) => { if (!entity) return; const { navigate } = this.props; navigate(`/${entity.type}/${entity.code}`); }; // Function that returns search bar passed into control panel - populateSearchBar = () => { + const populateSearchBar = () => { return ( 0) { - this.setState({ - summaryDataProcessed: - this.state.summaryDataProcessed.concat(summaryData), - }); + const _convertValuesForSummaryTable = () => { + let summaryData = convertValuesForSummaryTable(summaryDataRaw); + if (apiPageNumber === 0) { + setSummaryDataProcessed(summaryData); + setGenSummaryTableDataProcessed(true); } + if (apiPageNumber > 0) { + setSummaryDataProcessed(summaryDataProcessed.concat(summaryData)); + } } - render() { - const { activeTabType } = this.state; - const title = T.translate("entity.pageTitle"); - return ( + const title = T.translate("entity.pageTitle"); + return (
IODA | Dashboard for Monitoring Internet Outages @@ -466,79 +474,79 @@ class Dashboard extends Component { /> this.populateSearchBar()} - from={this.state.from} - until={this.state.until} + onTimeFrameChange={handleTimeFrame} + searchbar={() => populateSearchBar()} + from={from} + until={until} title={title} />
this.handleSelectTab(e?.target?.value)} + onChange={(e) => handleSelectTab(e?.target?.value)} value={activeTabType} className="mb-8" > - {this.countryTab} - {this.regionTab} - {this.asnTab} + {countryTab} + {regionTab} + {asnTab} {activeTabType !== asn.type ? ( - (this.state.topoData && this.state.topoScores) || - this.state.until - this.state.from > dashboardTimeRangeLimit ? ( + (topoData && topoScores) || + until - from > dashboardTimeRangeLimit ? ( - this.handleTabChangeViewButton() + handleTabChangeViewButton() } - tabCurrentView={this.state.tabCurrentView} - from={this.state.from} - until={this.state.until} + tabCurrentView={tabCurrentView} + from={from} + until={until} // display error text if from value is higher than until value displayTimeRangeError={ - this.state.displayDashboardTimeRangeError + displayDashboardTimeRangeError } // to populate summary table - summaryDataProcessed={this.state.summaryDataProcessed} - totalOutages={this.state.totalOutages} - activeTabType={this.state.activeTabType} + summaryDataProcessed={summaryDataProcessed} + totalOutages={totalOutages} + activeTabType={activeTabType} genSummaryTableDataProcessed={ - this.state.genSummaryTableDataProcessed + genSummaryTableDataProcessed } // to populate horizon time series table - eventDataProcessed={this.state.eventDataProcessed} + eventDataProcessed={eventDataProcessed} // to populate map - topoData={this.state.topoData} - topoScores={this.state.topoScores} - handleEntityShapeClick={this.handleEntityShapeClick} - summaryDataRaw={this.state.summaryDataRaw} + topoData={topoData} + topoScores={topoScores} + handleEntityShapeClick={handleEntityShapeClick} + summaryDataRaw={summaryDataRaw} /> - ) : this.state.displayTimeRangeError ? ( + ) : displayTimeRangeError ? ( ) : ( ) - ) : this.state.eventDataProcessed || - this.state.until - this.state.from > dashboardTimeRangeLimit ? ( + ) : eventDataProcessed || + until - from > dashboardTimeRangeLimit ? ( - ) : this.state.displayDashboardTimeRangeError ? ( + ) : displayDashboardTimeRangeError ? ( ) : ( @@ -546,7 +554,6 @@ class Dashboard extends Component {
); - } } // TODO: Migrate file fully to functional component