diff --git a/src/app.py b/src/app.py index 8a40e20..b9a0848 100644 --- a/src/app.py +++ b/src/app.py @@ -13,7 +13,9 @@ # cache the data in /parquet. ParquetUpdater() -external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css', dbc.themes.BOOTSTRAP, dbc.icons.BOOTSTRAP] +external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css', + "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css", + dbc.themes.BOOTSTRAP, dbc.icons.BOOTSTRAP] app = Dash(__name__, external_stylesheets=external_stylesheets, external_scripts=[ diff --git a/src/assets/style.css b/src/assets/style.css index 09621f9..a588407 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -16,8 +16,7 @@ body{ .table-container .cell-markdown>p { display: flex; height: 100%; - justify-content: center; - align-items: center; + text-align: center; } @@ -138,6 +137,11 @@ body{ --selected-background: rgb(227, 234, 244) !important; } +.dash-spreadsheet-container.dash-spreadsheet.dash-empty-01.dash-fill-width { + border: none !important; + box-shadow: none !important; +} + .container { margin-bottom: 20px; max-width: 1550px !important; @@ -168,24 +172,25 @@ body{ } .cls-site-map { - height: 740px; + height: 640px; /* padding: 20px; border: 1px solid rgba(0, 0, 0, .125); border-radius: 1rem */ } +.mapboxgl-ctrl-bottom-right { + display: none; +} + .table-container th { border-left: 0px !important; border-right: 0px !important; border-top-color: white !important; border-bottom-color: white !important; - background-color: #bac4d4 !important; + background-color: #f1f1f1 !important; color: rgb(45, 44, 44); } -.table-container { - border: 1px solid #dfdfdf; -} .table-container .previous-next-container { padding-top: 1% !important; @@ -197,12 +202,6 @@ body{ border-color: #cbd4e7; } -@media (max-width: 1580px) { - .sidebysite-cont { - width: 100% !important; - } -} - .card { border-radius: 1rem; } @@ -568,12 +567,17 @@ body{ background-color: #ffffff; } +count-box { + padding: 30px; + text-align: center; +} +/* @media (max-width: 1500px) { .column-margin { margin-top: 10px; padding: 0 20px; } -} +} */ @media (max-width: 1340px) { .status-box { @@ -633,7 +637,6 @@ body{ .how-status-div { padding: 0 20px; - margin-top: 10px; } .how-status-div button { @@ -650,4 +653,13 @@ body{ .next-10-btn:disabled, .all-btn:disabled { background-color: gray; color: white; +} + +.fa-search { + font-size: 1em; + color: white; + background-color: #7f7f7f; + padding: 10px; + margin-right: 10px; + border-radius: 5px; } \ No newline at end of file diff --git a/src/model/Alarms.py b/src/model/Alarms.py index 1100af4..900536f 100644 --- a/src/model/Alarms.py +++ b/src/model/Alarms.py @@ -62,6 +62,7 @@ def unpackAlarms(self, alarmsData): elif event in ['high packet loss', 'path changed', + 'path changed v2', 'destination cannot be reached from any', 'source cannot reach any', 'bandwidth decreased', @@ -374,11 +375,15 @@ def formatDfValues(self, df, event): df.drop(columns=['avg_value'], inplace=True) # TODO: create pages/visualizatios for the following events then remove the df.drop('alarm_link') below - if event not in ['unresolvable host', 'hosts not found']: + if event not in ['unresolvable host', 'hosts not found', 'path changed v2']: df = self.createAlarmURL(df, event) else: df.drop('alarm_link', axis=1, inplace=True) - df['site'] = df['site'].fillna("Unknown site") + if 'asn_list' in df.columns: + df = self.replaceCol('asn_list', df, '\n') + + if 'site' in df.columns: + df['site'] = df['site'].fillna("Unknown site") # Reorder 'from' and 'to' columns to be the first two columns if they exist df = self.reorder_columns(df, ['from', 'to']) diff --git a/src/model/queries.py b/src/model/queries.py index 5aa1fb3..ffbf14a 100644 --- a/src/model/queries.py +++ b/src/model/queries.py @@ -161,8 +161,8 @@ def queryPathChanged(dateFrom, dateTo): "must": [{ "range": { "to_date": { - "from": dateFrom, - "to": dateTo, + "gte": dateFrom, + "lte": dateTo, "format": "strict_date_optional_time" } } @@ -413,8 +413,8 @@ def queryTraceChanges(dateFrom, dateTo, asn=None): { "range": { "to_date": { - "from": dateFrom, - "to": dateTo, + "gte": dateFrom, + "lte": dateTo, "format": "strict_date_optional_time" } } diff --git a/src/pages/home.py b/src/pages/home.py index b7c50c6..bfe74d1 100644 --- a/src/pages/home.py +++ b/src/pages/home.py @@ -2,6 +2,7 @@ import dash from dash import Dash, dash_table, dcc, html import dash_bootstrap_components as dbc +import dash_bootstrap_components as dbc from dash.dependencies import Input, Output, State import plotly.graph_objects as go @@ -9,6 +10,7 @@ import pandas as pd from flask import request +from datetime import date from model.Alarms import Alarms import utils.helpers as hp @@ -116,8 +118,9 @@ def give_status(site): return '🟢' df_pivot['Status'] = df_pivot['site'].apply(give_status) + df_pivot['site name'] = df_pivot.apply(lambda row: f"{row['Status']} {row['site']}", axis=1) - df_pivot = df_pivot[['site', 'Status', 'Network', 'Infrastructure', 'Other']] + df_pivot = df_pivot[['site', 'site name', 'Status', 'Network', 'Infrastructure', 'Other']] url = f'{request.host_url}site' df_pivot['url'] = df_pivot['site'].apply(lambda name: @@ -125,37 +128,55 @@ def give_status(site): status_order = ['🔴', '🟡', '🟢', '⚪'] df_pivot = df_pivot.sort_values(by='Status', key=lambda x: x.map({status: i for i, status in enumerate(status_order)})) + display_columns = [col for col in df_pivot.columns.tolist() if col not in ['Status', 'site']] + print(display_columns) if len(df_pivot) > 0: element = html.Div([ - dash_table.DataTable( - df_pivot.to_dict('records'),[{"name": i.upper(), "id": i, "presentation": "markdown"} for i in df_pivot.columns], - filter_action="native", - filter_options={"case": "insensitive"}, - sort_action="native", - is_focused=True, - markdown_options={"html": True}, - page_size=16, - style_cell={ - 'padding': '2px', - 'font-size': '1.5em', - 'textAlign': 'center', - }, - style_header={ - 'backgroundColor': 'white', - 'fontWeight': 'bold' - }, - style_data={ - 'height': 'auto', - 'overflowX': 'auto', - }, - style_table={ - 'overflowY': 'auto', - 'overflowX': 'auto' - }, - style_data_conditional=[], - id='status-tbl') - ], className='table-container') + dash_table.DataTable( + df_pivot.to_dict('records'),[{"name": i.upper(), "id": i, "presentation": "markdown"} for i in display_columns], + filter_action="native", + filter_options={"case": "insensitive"}, + sort_action="native", + is_focused=True, + markdown_options={"html": True}, + page_size=10, + style_cell={ + 'padding': '10px', + 'font-size': '1.2em', + 'textAlign': 'center', + 'backgroundColor': '#ffffff', + 'border': '1px solid #ddd', + }, + style_header={ + 'backgroundColor': '#ffffff', + 'fontWeight': 'bold', + 'color': 'black', + 'border': '1px solid #ddd', + }, + style_data={ + 'height': 'auto', + 'overflowX': 'auto', + }, + style_table={ + 'overflowY': 'auto', + 'overflowX': 'auto', + 'border': '1px solid #ddd', + 'borderRadius': '5px', + 'boxShadow': '0 2px 5px rgba(0,0,0,0.1)', + }, + style_data_conditional=[ + { + 'if': {'row_index': 'odd'}, + 'backgroundColor': '#f7f7f7', + }, + { + 'if': {'column_id': 'SITE NAME'}, + 'textAlign': 'left !important', + } + ], + id='status-tbl') + ], className='table-container') else: element = html.Div(html.H3('No alarms for this site in the past day'), style={'textAlign': 'center'}) @@ -182,44 +203,39 @@ def total_number_of_alarms(sitesDf): highest_country = country_totals.sum(axis=1).idxmax() highest_country_alarms = country_totals.sum(axis=1).max() - status = ['🔴', '🟡', '🟢', '⚪'] + status = {'critical': '🔴', 'warning': '🟡', 'ok': '🟢', 'unknown':'⚪'} status_count = sitesDf[['Status', 'site']].groupby('Status').count().to_dict()['site'] - for s in status: - if s not in status_count: - status_count[s] = 0 - + for s, icon in status.items(): + if icon not in status_count: + status_count[icon] = 0 html_elements = [dbc.Col([ dbc.Row( - dbc.Col( html.H1('Status of all sites in the past 48 hours', - className='card-title') - , align="center") - , align="center", justify='center', className='h-100'), - ], className='status-box boxwithshadow', md=3, xs=12)] - # add the status count to the html - total_status = [dbc.Col(html.H1('Summary', className='h-100'), - md=12, className='status-count-header')] - for s in status: - total_status.append( - dbc.Col( - dbc.Card( - dbc.CardBody( - [ - html.H4(f'{s}', className='card-title'), - html.P(f'{status_count[s]}', className='card-text'), - ] + className='card-title align-items-stretch'), + align="center", className='w-100 p-2', style={"text-align": "center"} + ), + dbc.Row(children=[ + *[dbc.Col( + dbc.Card( + dbc.CardBody( + [ + html.H4(f'{icon}', className='card-title'), + html.H3(f'{s}', className='card-title'), + html.H3(f'{status_count[icon]}', className='card-text'), + ] + ), + className='mb-3', ), - className='mb-3', - ), - md=3, xs=6, className='status-count-numbers' - ) - ) + md=3, xs=3, xl=3, className='status-count-numbers' + ) for s, icon in status.items()] + ], className='w-100 status-box p-1 gx-4', align="center", justify='center'), + ], className='boxwithshadow g-0 mb-1')] - html_elements.append(dbc.Col([ - dbc.Row(children=total_status, justify="center", align="center", className='h-100')], - className='status-box boxwithshadow col-md-auto', md=3, xs=12)) + # html_elements.append(dbc.Col([ + # dbc.Row(children=total_status, justify="center", align="center", className='h-100')], + # className='status-box boxwithshadow col-md-auto', md=6, xs=12)) # # add the total number of alarms to the html # for k,v in sitesDf.sum(numeric_only=True).to_dict().items(): @@ -230,20 +246,22 @@ def total_number_of_alarms(sitesDf): # add the highest number of alarms based on site name to the html country_code = get_country_code(sitesDf[sitesDf['site']==highest_site]['country'].values[0]) - html_elements.append(dbc.Col([ - dbc.Row([ - html.H3(f'Highest number of alarms from site', className='status-title'), - html.H1(f' {highest_site} ({country_code}): {highest_site_alarms}', className='status-number') - ], align="center", className='h-100'), - ], className='status-box boxwithshadow', md=3, xs=12)) - - # add the highest number of alarms based on country to the html - html_elements.append(dbc.Col([ - dbc.Row([ - html.H3(f'Highest number of alarms from country', className='status-title'), - html.H1(f'{highest_country}: {highest_country_alarms}', className='status-number'), - ], align="center", className='h-100'), - ], className='status-box boxwithshadow', md=3, xs=12)) + html_elements.append( + dbc.Row([ + dbc.Col([ + dbc.Row([ + html.H3(f'Highest number of alarms from site', className='status-title'), + html.H1(f' {highest_site} ({country_code}): {highest_site_alarms}', className='status-number') + ], align="center", className='h-100'), + ], className='status-box boxwithshadow mb-1', md=6, sm=12), + dbc.Col([ + dbc.Row([ + html.H3(f'Highest number of alarms from country', className='status-title'), + html.H1(f'{highest_country}: {highest_country_alarms}', className='status-number'), + ], align="center", className='h-100'), + ], className='status-box boxwithshadow mb-1', md=6, sm=12) + ], className='g-0') + ) return html_elements @@ -342,6 +360,7 @@ def explainStatuses(): def layout(**other_unknown_query_strings): dateFrom, dateTo = hp.defaultTimeRange(1) + now = hp.defaultTimeRange(days=2, datesOnly=True) alarmCnt = pq.readFile('parquet/alarmsGrouped.parquet') statusTable, sitesDf = generate_status_table(alarmCnt) print("Period:", dateFrom," - ", dateTo) @@ -350,20 +369,31 @@ def layout(**other_unknown_query_strings): total_number = total_number_of_alarms(sitesDf) return html.Div([ - dbc.Row( - children=total_number, className='g-0 d-flex align-items-stretch h-100', align="center", justify='center', style={"padding": "0.5% 1.5%"}), - dbc.Row([ - dbc.Row([ - dbc.Col( - [ - html.Div(children=statusTable, id='site-status', className='datatables-cont'), - ], className='page-cont sidebysite-cont', xl=6 - ), - dbc.Col(dcc.Graph(figure=builMap(sitesDf), id='site-map', - className='cls-site-map'), - className='page-cont sidebysite-cont column-margin', xl=6 + dbc.Row([ + + # top left column with the map and the stacked bar chart + dbc.Col([ + dbc.Col(dcc.Graph(figure=builMap(sitesDf), id='site-map', + className='cls-site-map'), + className='boxwithshadow page-cont mb-1 g-0 p-2 column-margin', + xl=12, style={"background-color": "#b9c4d4;", "padding-top": "3%"}, align="center" ), - dbc.Col( + dbc.Col( + dcc.Loading( + html.Div(id="alarms-stacked-bar"), + style={'height': '1rem'}, color='#00245A' + ), + className="boxwithshadow page-cont mb-1 p-2",), + ], lg=6, md=12, className='d-flex flex-column'), # d-flex and flex-column make the columns the same size + # end of top left column + + # top right column with status table, status statistics and the search fields + dbc.Col([ + dbc.Row(children=total_number, className="h-100"), + dbc.Row([ + dbc.Col( + [ + html.Div(children=statusTable, id='site-status', className='p-2'), html.Div( [ dbc.Button( @@ -379,40 +409,154 @@ def layout(**other_unknown_query_strings): is_open=False, ), ], className="how-status-div", - ), lg=12, md=12, - ), - ], className='boxwithshadow page-cont mb-1 g-0 p-1', justify="center", align="center"), - html.Div(id='page-content-noloading'), - html.Br(), + ), + ], className='page-cont mb-1 p-1', xl=12 + ) + ], className="boxwithshadow page-cont mb-1" + ), + dbc.Row([ + dbc.Row([ + dbc.Col([ + dbc.Row([ + dbc.Col([ + html.H3([ + html.I(className="fas fa-search"), + "Search the Networking Alarms" + ], className="l-h-3"), + ], align="center", className="text-left rounded-border-1 pl-1" + , md=6, xl=6), + dbc.Col([ + dcc.DatePickerRange( + id='date-picker-range', + month_format='M-D-Y', + min_date_allowed=date(2022, 8, 1), + initial_visible_month=now[0], + start_date=now[0], + end_date=now[1] + ) + ], style={"display": "flex", "justify-content": "flex-end", "align-items": "center"}) + ], justify="between", align="center"), + dbc.Row([ + dbc.Col([ + dcc.Dropdown(multi=True, id='sites-dropdown', placeholder="Search for a site"), + ]), + ]), + html.Br(), + dbc.Row([ + dbc.Col([ + dcc.Dropdown(multi=True, id='events-dropdown', placeholder="Search for an event type"), + ]), + ]), + html.Br(), + dbc.Row([ + dbc.Col([ + dbc.Button("Search", id="search-button", color="secondary", + className="mr-2", style={"width": "100%", "font-size": "1.5em"}) + ]) + ]), + ], lg=12, md=12, className="pl-1 pr-1 p-1"), + ], className="p-1 site", justify="center", align="center"), + ], className='boxwithshadow page-cont mb-1 row', align="center")], + lg=6, sm=12, className='d-flex flex-column h-100'), - ], className='g-0', align="start", style={"padding": "0 1.5%"}) - ], className='main-cont') + # end of top right column + ], className='h-100 gx-4'), + + # bottom part with the list of alarms + dbc.Row([ + dbc.Col([ + html.H1(f"List of alarms", className="text-center"), + html.Hr(className="my-2"), + html.Br(), + dcc.Loading( + html.Div(id='results-table'), + style={'height': '0.5rem'}, color='#00245A') + ], className="boxwithshadow page-cont p-2",), + ], className=""), + ], className='', style={"padding": "0.5% 1% 0 1%"}) @dash.callback( [ - Output("how-status-collapse", "is_open"), - Output("how-status-collapse", "children"), + Output("sites-dropdown", "options"), + Output("events-dropdown", "options"), + Output('alarms-stacked-bar', 'children'), + Output('results-table', 'children'), ], - [Input("how-status-collapse-button", "n_clicks")], - [State("how-status-collapse", "is_open")], + [ + Input('search-button', 'n_clicks'), + Input('date-picker-range', 'start_date'), + Input('date-picker-range', 'end_date'), + Input("sites-dropdown", "search_value"), + Input("sites-dropdown", "value"), + Input("events-dropdown", "search_value"), + Input("events-dropdown", "value") + ], + State("sites-dropdown", "value"), + State("events-dropdown", "value") ) -def toggle_collapse(n, is_open): - catTable, statusExplainedTable = explainStatuses() - data = dbc.Row([ - dbc.Col(children=[ - html.H3('Category & Alarm types', className='status-title'), - html.Div(statusExplainedTable, className='how-status-table') - ], lg=6, md=12, sm=12, className='page-cont pr-1 how-status-cont'), - dbc.Col(children=[ - html.H3('Status color rules', className='status-title'), - html.Div(catTable, className='how-status-table')], lg=6, md=12, sm=12, className='page-cont how-status-cont') - ], className='pt-1') - - if n: - return not is_open, data - return is_open, data +def update_output(n_clicks, start_date, end_date, sites, all, events, allevents, sitesState, eventsState): + ctx = dash.callback_context + + if not ctx.triggered or ctx.triggered[0]['prop_id'].split('.')[0] == 'search-button': + if start_date and end_date: + start_date, end_date = [f'{start_date}T00:01:00.000Z', f'{end_date}T23:59:59.000Z'] + else: start_date, end_date = hp.defaultTimeRange(2) + alarmsInst = Alarms() + frames, pivotFrames = alarmsInst.loadData(start_date, end_date) + + scntdf = pd.DataFrame() + for e, df in pivotFrames.items(): + if len(df) > 0: + if e != 'unresolvable host': # the tag is hostname for unresolvable hosts + df = df[df['tag'] != ''].groupby('tag')[['id']].count().reset_index().rename(columns={'id': 'cnt', 'tag': 'site'}) + else: df = df[df['site'] != ''].groupby('site')[['id']].count().reset_index().rename(columns={'id': 'cnt'}) + + if e != 'path changed': + df['event'] = e + scntdf = pd.concat([scntdf, df]) + + # sites + graphData = scntdf + if (sitesState is not None and len(sitesState) > 0): + graphData = graphData[graphData['site'].isin(sitesState)] + + sites_dropdown_items = [] + for s in sorted(scntdf['site'].unique()): + if s: + sites_dropdown_items.append({"label": s.upper(), "value": s.upper()}) + + # events + if eventsState is not None and len(eventsState) > 0: + graphData = graphData[graphData['event'].isin(eventsState)] + + events_dropdown_items = [] + for e in sorted(scntdf['event'].unique()): + events_dropdown_items.append({"label": e, "value": e}) + + + bar_chart = create_bar_chart(graphData) + + dataTables = [] + events = list(pivotFrames.keys()) if not eventsState or events else eventsState + + for event in sorted(events): + df = pivotFrames[event] + if 'site' in df.columns: + df = df[df['site'].isin(sitesState)] if sitesState is not None and len(sitesState) > 0 else df + elif 'tag' in df.columns: + df = df[df['tag'].isin(sitesState)] if sitesState is not None and len(sitesState) > 0 else df + + if len(df) > 0: + dataTables.append(generate_tables(frames[event], df, event, alarmsInst)) + dataTables = html.Div(dataTables) + + + return [sites_dropdown_items, events_dropdown_items, dcc.Graph(figure=bar_chart), dataTables] + else: + raise dash.exceptions.PreventUpdate + # # # '''Takes selected site from the Geo map and displays the relevant information''' # @dash.callback( @@ -435,3 +579,183 @@ def toggle_collapse(n, is_open): # return [] + + + + + + + + + +def colorMap(eventTypes): + colors = ['#75cbe6', '#3b6d8f', '#75E6DA', '#189AB4', '#2E8BC0', '#145DA0', '#05445E', '#0C2D48', + '#5EACE0', '#d6ebff', '#498bcc', '#82cbf9', + '#2894f8', '#fee838', '#3e6595', '#4adfe1', '#b14ae1' + '#1f77b4', '#ff7f0e', '#2ca02c','#00224e', '#123570', '#3b496c', '#575d6d', '#707173', '#8a8678', '#a59c74', + ] + + paletteDict = {} + for i,e in enumerate(eventTypes): + paletteDict[e] = colors[i] + + return paletteDict + + + +# @dash.callback( +# [ +# Output("sites-dropdown", "options"), +# Output("events-dropdown", "options"), +# Output('alarms-stacked-bar', 'children'), +# Output('results-table', 'children'), +# ], +# [ +# Input('date-picker-range', 'start_date'), +# Input('date-picker-range', 'end_date'), +# Input("sites-dropdown", "search_value"), +# Input("sites-dropdown", "value"), +# Input("events-dropdown", "search_value"), +# Input("events-dropdown", "value"), +# ], +# State("sites-dropdown", "value"), +# State("events-dropdown", "value")) +# def update_output(start_date, end_date, sites, all, events, allevents, sitesState, eventsState ): + +# if start_date and end_date: +# start_date, end_date = [f'{start_date}T00:01:00.000Z', f'{end_date}T23:59:59.000Z'] +# else: start_date, end_date = hp.defaultTimeRange(2) +# alarmsInst = Alarms() +# frames, pivotFrames = alarmsInst.loadData(start_date, end_date) + +# scntdf = pd.DataFrame() +# for e, df in pivotFrames.items(): +# if len(df) > 0: +# if e != 'unresolvable host': # the tag is hostname for unresolvable hosts +# df = df[df['tag'] != ''].groupby('tag')[['id']].count().reset_index().rename(columns={'id': 'cnt', 'tag': 'site'}) +# else: df = df[df['site'] != ''].groupby('site')[['id']].count().reset_index().rename(columns={'id': 'cnt'}) + +# if e != 'path changed': +# df['event'] = e +# scntdf = pd.concat([scntdf, df]) + +# # sites +# graphData = scntdf.copy() +# if (sitesState is not None and len(sitesState) > 0): +# graphData = graphData[graphData['site'].isin(sitesState)] + +# sites_dropdown_items = [] +# for s in sorted(scntdf['site'].unique()): +# if s: +# sites_dropdown_items.append({"label": s.upper(), "value": s.upper()}) + +# # events +# if eventsState is not None and len(eventsState) > 0: +# graphData = graphData[graphData['event'].isin(eventsState)] + +# events_dropdown_items = [] +# for e in sorted(scntdf['event'].unique()): +# events_dropdown_items.append({"label": e, "value": e}) + + +# bar_chart = create_bar_chart(graphData) + +# dataTables = [] +# events = list(pivotFrames.keys()) if not eventsState or events else eventsState + +# for event in sorted(events): +# df = pivotFrames[event] +# df = df[df['site'].isin(sitesState)] if sitesState is not None and len(sitesState) > 0 else df +# if len(df) > 0: +# dataTables.append(generate_tables(frames[event], df, event, alarmsInst)) +# dataTables = html.Div(dataTables) + + +# return [sites_dropdown_items, events_dropdown_items, dcc.Graph(figure=bar_chart), dataTables] + + +def create_bar_chart(graphData): + + fig = px.bar(graphData, x='site', y='cnt', color='event', + # title='Alarms by Site and Event Type', + labels={'cnt': 'Number of Alarms', 'site': '', 'event': 'Event Type'}, + barmode='stack', + color_discrete_sequence=px.colors.qualitative.Prism) + fig.update_layout( + margin=dict(t=20, b=20, l=0, r=0), + showlegend=True, + legend_orientation='h', + legend_title_text=f'Alarm Type', + legend=dict( + traceorder="normal", + font=dict( + family="sans-serif", + size=12, + ), + x=0, + y=1.9, + xanchor='left', + yanchor='top' + ), + height=600, + plot_bgcolor='#fff', + autosize=True, + width=None, + title={ + # 'text': 'Alarms by Site and Event Type', + 'y': 0.01, + 'x': 0.95, + 'xanchor': 'right', + 'yanchor': 'bottom' + } + ) + + return fig + + +# '''Takes selected site from the Geo map and generates a Dash datatable''' +def generate_tables(frame, unpacked, event, alarmsInst): + ids = unpacked['id'].values + dfr = frame[frame.index.isin(ids)] + dfr = alarmsInst.formatDfValues(dfr, event) + dfr.sort_values('to', ascending=False, inplace=True) + + try: + element = html.Div([ + html.Br(), + html.H3(event.upper()), + dash_table.DataTable( + data=dfr.to_dict('records'), + columns=[{"name": i, "id": i, "presentation": "markdown"} for i in dfr.columns], + markdown_options={"html": True}, + id=f'search-tbl-{event}', + page_current=0, + page_size=10, + style_cell={ + 'padding': '2px', + 'font-size': '13px', + 'whiteSpace': 'pre-line' + }, + style_header={ + 'backgroundColor': 'white', + 'fontWeight': 'bold' + }, + style_data={ + 'height': 'auto', + 'lineHeight': '15px', + 'overflowX': 'auto' + }, + style_table={ + 'overflowY': 'auto', + 'overflowX': 'auto' + }, + filter_action="native", + filter_options={"case": "insensitive"}, + sort_action="native", + ), + ], className='single-table') + return element + except Exception as e: + print('dash_table.DataTable expects each cell to contain a string, number, or boolean value', e) + return html.Div() + diff --git a/src/pages/search.py b/src/pages/search.py index d3b1aca..3e091df 100644 --- a/src/pages/search.py +++ b/src/pages/search.py @@ -1,15 +1,13 @@ import dash -from dash import Dash, dash_table, dcc, html +from dash import dash_table, dcc, html import dash_bootstrap_components as dbc from dash.dependencies import Input, Output, State - -import plotly.graph_objects as go import plotly.express as px -from datetime import datetime + from datetime import date import pandas as pd -from elasticsearch.helpers import scan + import utils.helpers as hp from model.Alarms import Alarms @@ -45,7 +43,7 @@ def layout(**other_unknown_query_strings): dbc.Row([ dbc.Col( dcc.Loading( - html.Div(id="alarms-sunburst"), + html.Div(id="alarms-sunburst-tab"), style={'height':'0.5rem'}, color='#00245A'), align="start", lg=7, md=12, className="pl-1 pr-1"), dbc.Col([ @@ -59,7 +57,7 @@ def layout(**other_unknown_query_strings): html.Br(), dbc.Row([ dcc.DatePickerRange( - id='date-picker-range', + id='date-picker-range-tab', month_format='M-D-Y', min_date_allowed=date(2022, 8, 1), initial_visible_month=now[0], @@ -70,13 +68,13 @@ def layout(**other_unknown_query_strings): ]), dbc.Row([ dbc.Col([ - dcc.Dropdown(multi=True, id='sites-dropdown', placeholder="Search for a site"), + dcc.Dropdown(multi=True, id='sites-dropdown-tab', placeholder="Search for a site"), ]), ]), html.Br(), dbc.Row([ dbc.Col([ - dcc.Dropdown(multi=True, id='events-dropdown', placeholder="Search for an event type"), + dcc.Dropdown(multi=True, id='events-dropdown-tab', placeholder="Search for an event type"), ]), ]), @@ -90,7 +88,7 @@ def layout(**other_unknown_query_strings): html.Hr(className="my-2"), html.Br(), dcc.Loading( - html.Div(id='results-table'), + html.Div(id='results-table-tab'), style={'height':'0.5rem'}, color='#00245A') ], className="m-2"), ], className="p-2 site boxwithshadow page-cont mb-1 g-0", justify="center", align="center"), @@ -117,21 +115,21 @@ def colorMap(eventTypes): @dash.callback( [ - Output("sites-dropdown", "options"), - Output("events-dropdown", "options"), - Output('alarms-sunburst', 'children'), - Output('results-table', 'children'), + Output("sites-dropdown-tab", "options"), + Output("events-dropdown-tab", "options"), + Output('alarms-sunburst-tab', 'children'), + Output('results-table-tab', 'children'), ], [ - Input('date-picker-range', 'start_date'), - Input('date-picker-range', 'end_date'), - Input("sites-dropdown", "search_value"), - Input("sites-dropdown", "value"), - Input("events-dropdown", "search_value"), - Input("events-dropdown", "value"), + Input('date-picker-range-tab', 'start_date'), + Input('date-picker-range-tab', 'end_date'), + Input("sites-dropdown-tab", "search_value"), + Input("sites-dropdown-tab", "value"), + Input("events-dropdown-tab", "search_value"), + Input("events-dropdown-tab", "value"), ], - State("sites-dropdown", "value"), - State("events-dropdown", "value")) + State("sites-dropdown-tab", "value"), + State("events-dropdown-tab", "value")) def update_output(start_date, end_date, sites, all, events, allevents, sitesState, eventsState ): if start_date and end_date: @@ -205,7 +203,7 @@ def generate_tables(frame, unpacked, event, alarmsInst): data=dfr.to_dict('records'), columns=[{"name": i, "id": i, "presentation": "markdown"} for i in dfr.columns], markdown_options={"html": True}, - id=f'search-tbl-{event}', + id=f'search-tbl-{event}-tab', page_current=0, page_size=10, style_cell={ @@ -234,6 +232,5 @@ def generate_tables(frame, unpacked, event, alarmsInst): return element except Exception as e: print('dash_table.DataTable expects each cell to contain a string, number, or boolean value', e) - return html.Div() + return html.Div() - \ No newline at end of file