Skip to content

Commit 4f57ab1

Browse files
authored
feat: add filter tabs for alert and history views (#162)
1 parent 0ab2d8f commit 4f57ab1

6 files changed

Lines changed: 527 additions & 1 deletion

File tree

alerta/database/backends/postgres/base.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,91 @@ def get_alert_tags(self, query=None, topn=1000):
945945
""".format(where=query.where)
946946
return [{'environment': t.environment, 'tag': t.tag, 'count': t.count} for t in self._fetchall(select, query.vars, limit=topn)]
947947

948+
# FILTER TABS
949+
950+
def get_filter_tabs(self):
951+
select = """SELECT * FROM filter_tabs ORDER BY index"""
952+
return self._fetchall(select, {}, 'ALL')
953+
954+
def get_filter_tab(self, name):
955+
select = """SELECT * FROM filter_tabs WHERE name=%(name)s"""
956+
return self._fetchone(select, {'name': name})
957+
958+
def update_filter_tabs(self, tabs):
959+
tab_updates = ','.join([
960+
f"""
961+
(%(name{i})s, %(index{i})s, %(filter{i})s)
962+
""" for i in range(len(tabs))
963+
])
964+
update = f"""
965+
UPDATE filter_tabs as tab
966+
SET name=update.name, index=update.index, filter=update.filter::jsonb
967+
FROM (VALUES
968+
{tab_updates}
969+
) AS update(name, index, filter)
970+
WHERE tab.name = update.name
971+
RETURNING tab.*
972+
"""
973+
objs = {}
974+
for i, tab in enumerate(tabs):
975+
objs.update({f'{key}{i}': value for key, value in tab.items()})
976+
977+
return self._updateall(update, objs, True)
978+
979+
def update_filter_tab_indexes(self, tabs):
980+
tab_updates = ','.join([
981+
f"""
982+
(%(name{i})s, %(index{i})s)
983+
""" for i in range(len(tabs))
984+
])
985+
update = f"""
986+
UPDATE filter_tabs as tab
987+
SET index=update.index
988+
FROM (VALUES
989+
{tab_updates}
990+
) AS update(name, index)
991+
WHERE tab.name = update.name
992+
RETURNING tab.*
993+
"""
994+
objs = {}
995+
for i, tab in enumerate(tabs):
996+
objs.update({f'{key}{i}': value for key, value in tab.items()})
997+
998+
return self._updateall(update, objs, True)
999+
1000+
def delete_filter_tab(self, name):
1001+
select = """DELETE FROM filter_tabs WHERE name=%(name)s RETURNING name"""
1002+
return self._deleteone(select, {'name': name})
1003+
1004+
def delete_filter_tabs(self, names):
1005+
select = """DELETE FROM filter_tabs WHERE name=ANY(%(names)s) RETURNING name"""
1006+
return self._deleteall(select, {'names': names}, returning=True)
1007+
1008+
def create_filter_tab(self, filter):
1009+
insert = """
1010+
INSERT INTO filter_tabs (name, index, filter)
1011+
VALUES (%(name)s, %(index)s, %(filter)s)
1012+
RETURNING *
1013+
"""
1014+
return self._insert(insert, vars(filter))
1015+
1016+
def create_filter_tabs(self, tabs):
1017+
tab_values = ','.join([
1018+
f"""
1019+
(%(name{i})s, %(index{i})s, %(filter{i})s)
1020+
""" for i in range(len(tabs))
1021+
])
1022+
insert = f"""
1023+
INSERT INTO filter_tabs (name, index, filter)
1024+
VALUES {tab_values}
1025+
RETURNING *
1026+
"""
1027+
1028+
objs = {}
1029+
for i, tab in enumerate(tabs):
1030+
objs.update({f'{key}{i}': value for key, value in tab.items()})
1031+
return self._insert_all(insert, objs)
1032+
9481033
# BLACKOUTS
9491034

9501035
def create_blackout(self, blackout):

alerta/database/base.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,32 @@ def get_services(self, query=None, topn=1000):
244244
def get_alert_tags(self, query=None, topn=1000):
245245
raise NotImplementedError
246246

247+
# FILTER TABS
248+
249+
def get_filter_tabs(self):
250+
raise NotImplementedError
251+
252+
def get_filter_tab(self):
253+
raise NotImplementedError
254+
255+
def update_filter_tabs(self, tabs):
256+
raise NotImplementedError
257+
258+
def update_filter_tab_indexes(self, tabs):
259+
raise NotImplementedError
260+
261+
def create_filter_tab(self):
262+
raise NotImplementedError
263+
264+
def create_filter_tabs(self):
265+
raise NotImplementedError
266+
267+
def delete_filter_tab(self, id):
268+
raise NotImplementedError
269+
270+
def delete_filter_tabs(self, ids: list[str]):
271+
raise NotImplementedError
272+
247273
# BLACKOUTS
248274

249275
def create_blackout(self, blackout):

alerta/models/filter_tab.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
from datetime import UTC, datetime, timedelta
2+
3+
from werkzeug.datastructures import MultiDict
4+
5+
from alerta.app import db
6+
7+
VALID_PARAMS = [
8+
'id',
9+
'resource',
10+
'event',
11+
'environment',
12+
'severity',
13+
'status',
14+
'service',
15+
'value',
16+
'text',
17+
'tag',
18+
'tags',
19+
'customTags',
20+
'attributes',
21+
'origin',
22+
'createTime',
23+
'timeout',
24+
'rawData',
25+
'customer',
26+
'duplicateCount',
27+
'previousSeverity',
28+
'receiveTime',
29+
'lastReceiveId',
30+
'lastReceiveTime',
31+
'updateTime',
32+
]
33+
34+
35+
class FilterTab:
36+
37+
def __init__(self,name: str, index: int, **kwargs) -> None:
38+
if name is None:
39+
raise ValueError('Missing mandatory value for "name"')
40+
if index is None:
41+
raise ValueError('Missing mandatory value for "index"')
42+
43+
self.name = name
44+
self.index = index
45+
self.filter = kwargs.get('filter') or {}
46+
47+
@classmethod
48+
def parse(cls, json: dict[str, str | int | dict[str, str]]) -> 'FilterTab':
49+
if not isinstance(json.get('index'), int):
50+
raise ValueError('index must be an int')
51+
if not isinstance(json.get('name'), str):
52+
raise ValueError('name must be a string')
53+
54+
return FilterTab(
55+
name=json['name'],
56+
index=json['index'],
57+
filter=json.get('filter', None),
58+
)
59+
60+
@property
61+
def serialize(self):
62+
return {
63+
'name': self.name,
64+
'index': self.index,
65+
'filter': self.filter,
66+
}
67+
68+
@property
69+
def filter_args(self):
70+
def to_isoformat(date: datetime):
71+
return date.isoformat(timespec='milliseconds').replace('+00:00', 'Z')
72+
data = []
73+
for key, value in self.filter.items():
74+
if key == 'dateRange':
75+
if value == {}:
76+
continue
77+
if 'from' in value:
78+
if value.get('select'):
79+
from_time = datetime.fromtimestamp(value['from'], tz=UTC)
80+
else:
81+
from_time = (datetime.now(UTC) + timedelta(seconds=int(value['from'])))
82+
data.append(('from-date', to_isoformat(from_time)))
83+
if 'to' in value:
84+
to_time = datetime.fromtimestamp(value['to'], tz=UTC)
85+
data.append(('to-date', to_isoformat(to_time)))
86+
if key == 'attributes':
87+
for attr_key, attr_value in value.items():
88+
if isinstance(attr_value, list):
89+
for item in attr_value:
90+
data.append((f'attributes.{attr_key}', item))
91+
else:
92+
data.append((f'attributes.{attr_key}', attr_value))
93+
elif key not in VALID_PARAMS or isinstance(value, dict):
94+
continue
95+
elif isinstance(value, list):
96+
for val in value:
97+
data.append((key, val))
98+
else:
99+
data.append(key, value)
100+
101+
return MultiDict(data)
102+
103+
def __repr__(self) -> str:
104+
return f'AlertTab(name={self.name}, index={self.index}, filter={self.filter},'
105+
106+
@ classmethod
107+
def from_db(cls, rec) -> 'FilterTab':
108+
return FilterTab(
109+
name=rec.name,
110+
index=rec.index,
111+
filter=rec.filter
112+
)
113+
114+
# create a filter tab
115+
def create(self) -> 'FilterTab':
116+
return FilterTab.from_db(db.create_filter_tab(self))
117+
118+
# create a filter tabs
119+
@staticmethod
120+
def create_all(tabs: list['FilterTab']) -> list['FilterTab']:
121+
return [FilterTab.from_db(tab) for tab in db.create_filter_tabs(tabs)]
122+
123+
# get a filter tab
124+
@ staticmethod
125+
def find_by_id(id: str):
126+
return FilterTab.from_db(db.get_filter_tab(id))
127+
128+
@staticmethod
129+
def delete_all(ids: list[str]):
130+
return db.delete_filter_tabs(ids)
131+
132+
@staticmethod
133+
def update_all(tabs: list['FilterTab']):
134+
return db.update_filter_tabs(tabs)
135+
136+
@staticmethod
137+
def update_indexes(tabs):
138+
return db.update_filter_tab_indexes(tabs)
139+
140+
@ staticmethod
141+
def find_all() -> list['FilterTab']:
142+
return [
143+
FilterTab.from_db(notification_channel)
144+
for notification_channel in db.get_filter_tabs()
145+
]
146+
147+
# def update(self, **kwargs) -> 'FilterTab':
148+
# return FilterTab.from_db(db.update(self.id, **kwargs))
149+
150+
def delete(self) -> bool:
151+
return db.delete_filter_tab(self.id)

alerta/sql/schema.sql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,3 +628,9 @@ CREATE UNIQUE INDEX IF NOT EXISTS env_res_evt_cust_key ON alerts USING btree (en
628628

629629

630630
CREATE UNIQUE INDEX IF NOT EXISTS org_cust_key ON heartbeats USING btree (origin, (COALESCE(customer, ''::text)));
631+
632+
CREATE TABLE IF NOT EXISTS filter_tabs (
633+
name text PRIMARY KEY,
634+
"index" integer NOT NULL,
635+
"filter" jsonb
636+
);

alerta/views/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
api = Blueprint('api', __name__)
99

10-
from . import alerta, alerts, blackouts, config, customers, groups, keys, oembed, permissions, users, notification_rules, notification_channels, notification_history, on_call, escalation_rules, notification_groups, notification_delays, notification_sends # noqa isort:skip
10+
from . import alerta, alerts, blackouts, config, customers, groups, keys, oembed, permissions, users, notification_rules, notification_channels, notification_history, on_call, escalation_rules, notification_groups, notification_delays, notification_sends, filter_tabs # noqa isort:skip
1111
if get_config('HEARTBEAT_URL') is None:
1212
from . import heartbeats # noqa
1313
else:

0 commit comments

Comments
 (0)