Skip to content

Commit

Permalink
feat: wip
Browse files Browse the repository at this point in the history
  • Loading branch information
shahargl committed Aug 10, 2024
1 parent 2b0835d commit bb7859b
Show file tree
Hide file tree
Showing 10 changed files with 342 additions and 54 deletions.
74 changes: 65 additions & 9 deletions keep-ui/app/alerts/alert-presets.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { AlertDto, Preset } from "./models";
import Modal from "@/components/ui/Modal";
import { Button, TextInput, Switch,Text } from "@tremor/react";
import { Button, TextInput, Switch, Text } from "@tremor/react";
import { getApiURL } from "utils/apiUrl";
import { toast } from "react-toastify";
import { useSession } from "next-auth/react";
import { usePresets } from "utils/hooks/usePresets";
import { useTags } from "utils/hooks/useTags";
import { useRouter } from "next/navigation";
import { Table } from "@tanstack/react-table";
import { AlertsRulesBuilder } from "./alerts-rules-builder";
import QueryBuilder, {
formatQuery,
parseCEL,
} from "react-querybuilder";
import QueryBuilder, { formatQuery, parseCEL } from "react-querybuilder";
import CreatableSelect from "react-select/creatable";
import { MultiValue } from "react-select";


// Define types for the tags
interface TagOption {
id?: number;
name: string;
}

interface Props {
presetNameFromApi: string;
Expand All @@ -35,6 +42,7 @@ export default function AlertPresets({
revalidateOnFocus: false,
});
const { data: session } = useSession();
const { data: tags = [], mutate: mutateTags } = useTags();
const router = useRouter();

const [isModalOpen, setIsModalOpen] = useState(false);
Expand All @@ -46,13 +54,23 @@ export default function AlertPresets({
const [isPrivate, setIsPrivate] = useState(presetPrivate);
const [isNoisy, setIsNoisy] = useState(presetNoisy);
const [presetCEL, setPresetCEL] = useState("");
const [selectedTags, setSelectedTags] = useState<TagOption[]>([]);
const [newTags, setNewTags] = useState<string[]>([]); // New tags created during the session

const selectedPreset = savedPresets.find(
(savedPreset) =>
savedPreset.name.toLowerCase() ===
decodeURIComponent(presetNameFromApi).toLowerCase()
) as Preset | undefined;

useEffect(() => {
if (selectedPreset) {
setSelectedTags(
selectedPreset.tags.map((tag) => ({ id: tag.id, name: tag.name }))
);
}
}, [selectedPreset]);

async function deletePreset(presetId: string) {
if (
confirm(
Expand All @@ -78,8 +96,12 @@ export default function AlertPresets({

async function addOrUpdatePreset() {
if (presetName) {
// translate the CEL to SQL
const sqlQuery = formatQuery(parseCEL(presetCEL), { format: 'parameterized_named', parseNumbers: true });
// Translate the CEL to SQL
const sqlQuery = formatQuery(parseCEL(presetCEL), {
format: "parameterized_named",
parseNumbers: true,
});

const response = await fetch(
selectedPreset?.id
? `${apiUrl}/preset/${selectedPreset?.id}`
Expand All @@ -100,10 +122,14 @@ export default function AlertPresets({
{
label: "SQL",
value: sqlQuery,
}
},
],
is_private: isPrivate,
is_noisy: isNoisy,
tags: selectedTags.map((tag) => ({
id: tag.id,
name: tag.name,
})),
}),
}
);
Expand All @@ -124,6 +150,21 @@ export default function AlertPresets({
}
}

const handleCreateTag = (inputValue: string) => {
const newTag = { name: inputValue };
setNewTags((prevTags) => [...prevTags, inputValue]);
setSelectedTags((prevTags) => [...prevTags, newTag]);
};

const handleChange = (newValue: MultiValue<{ value: string; label: string }>) => {
setSelectedTags(
newValue.map((tag) => ({
id: tags.find((t) => t.name === tag.value)?.id,
name: tag.value,
}))
);
};

return (
<>
<Modal
Expand Down Expand Up @@ -153,6 +194,21 @@ export default function AlertPresets({
/>
</div>

<CreatableSelect
isMulti
value={selectedTags.map((tag) => ({
value: tag.name,
label: tag.name,
}))}
onChange={handleChange}
onCreateOption={handleCreateTag}
options={tags.map((tag) => ({
value: tag.name,
label: tag.name,
}))}
placeholder="Select or create tags"
/>

<div className="flex items-center space-x-2">
<Switch
id={"private"}
Expand Down
6 changes: 6 additions & 0 deletions keep-ui/app/alerts/models.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ interface Option {
readonly value: string;
}

export interface Tag {
id: number;
name: string;
}

export interface Preset {
id: string;
name: string;
Expand All @@ -77,6 +82,7 @@ export interface Preset {
should_do_noise_now: boolean;
alerts_count: number;
created_by?: string;
tags: Tag[];
}

export function getTabsFromPreset(preset: Preset): any[] {
Expand Down
18 changes: 18 additions & 0 deletions keep-ui/utils/hooks/useTags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useSession } from "next-auth/react";
import { SWRConfiguration } from "swr";
import useSWRImmutable from "swr/immutable";
import { getApiURL } from "utils/apiUrl";
import { fetcher } from "utils/fetcher";
import { Tag } from "app/alerts/models";


export const useTags = (options: SWRConfiguration = {}) => {
const apiUrl = getApiURL();
const { data: session } = useSession();

return useSWRImmutable<Tag[]>(
() => (session ? `${apiUrl}/tags` : null),
(url) => fetcher(url, session?.accessToken),
options
);
};
2 changes: 2 additions & 0 deletions keep/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
rules,
settings,
status,
tags,
topology,
users,
whoami,
Expand Down Expand Up @@ -197,6 +198,7 @@ def get_app(
extraction.router, prefix="/extraction", tags=["enrichment", "extraction"]
)
app.include_router(dashboard.router, prefix="/dashboard", tags=["dashboard"])
app.include_router(tags.router, prefix="/tags", tags=["tags"])

# if its single tenant with authentication, add signin endpoint
logger.info(f"Starting Keep with authentication type: {AUTH_TYPE}")
Expand Down
95 changes: 65 additions & 30 deletions keep/api/core/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -1640,7 +1640,8 @@ def get_presets(tenant_id: str, email) -> List[Dict[str, Any]]:
)
)
)
presets = session.exec(statement).all()
result = session.exec(statement)
presets = result.unique().all()
return presets


Expand Down Expand Up @@ -1840,9 +1841,7 @@ def update_preset_options(tenant_id: str, preset_id: str, options: dict) -> Pres
return preset


def assign_alert_to_incident(
alert_id: UUID, incident_id: UUID, tenant_id: str
):
def assign_alert_to_incident(alert_id: UUID, incident_id: UUID, tenant_id: str):
return add_alerts_to_incident_by_incident_id(tenant_id, incident_id, [alert_id])


Expand Down Expand Up @@ -2082,7 +2081,9 @@ def get_incidents_count(
)


def get_incident_alerts_by_incident_id(tenant_id: str, incident_id: str, limit: int, offset: int) -> (List[Alert], int):
def get_incident_alerts_by_incident_id(
tenant_id: str, incident_id: str, limit: int, offset: int
) -> (List[Alert], int):
with Session(engine) as session:
query = (
session.query(
Expand All @@ -2102,10 +2103,8 @@ def get_incident_alerts_by_incident_id(tenant_id: str, incident_id: str, limit:


def get_alerts_data_for_incident(
alert_ids: list[str | UUID],
session: Optional[Session] = None
alert_ids: list[str | UUID], session: Optional[Session] = None
) -> dict:

"""
Function to prepare aggregated data for incidents from the given list of alert_ids
Logic is wrapped to the inner function for better usability with an optional database session
Expand All @@ -2120,22 +2119,20 @@ def get_alerts_data_for_incident(
def inner(db_session: Session):

fields = (
get_json_extract_field(session, Alert.event, 'service'),
Alert.provider_type
get_json_extract_field(session, Alert.event, "service"),
Alert.provider_type,
)

alerts_data = db_session.exec(
select(
*fields
).where(
select(*fields).where(
col(Alert.id).in_(alert_ids),
)
).all()

sources = []
services = []

for (service, source) in alerts_data:
for service, source in alerts_data:
if source:
sources.append(source)
if service:
Expand All @@ -2144,7 +2141,7 @@ def inner(db_session: Session):
return {
"sources": set(sources),
"services": set(services),
"count": len(alerts_data)
"count": len(alerts_data),
}

# Ensure that we have a session to execute the query. If not - make new one
Expand Down Expand Up @@ -2176,16 +2173,17 @@ def add_alerts_to_incident_by_incident_id(
)
).all()

new_alert_ids = [alert_id for alert_id in alert_ids
if alert_id not in existed_alert_ids]
new_alert_ids = [
alert_id for alert_id in alert_ids if alert_id not in existed_alert_ids
]

alerts_data_for_incident = get_alerts_data_for_incident(new_alert_ids, session)

incident.sources = list(
set(incident.sources) | set(alerts_data_for_incident["sources"])
set(incident.sources) | set(alerts_data_for_incident["sources"])
)
incident.affected_services = list(
set(incident.affected_services) | set(alerts_data_for_incident["services"])
set(incident.affected_services) | set(alerts_data_for_incident["services"])
)
incident.alerts_count += alerts_data_for_incident["count"]

Expand Down Expand Up @@ -2231,7 +2229,7 @@ def remove_alerts_to_incident_by_incident_id(
# Getting aggregated data for incidents for alerts which just was removed
alerts_data_for_incident = get_alerts_data_for_incident(alert_ids, session)

service_field = get_json_extract_field(session, Alert.event, 'service')
service_field = get_json_extract_field(session, Alert.event, "service")

# checking if services of removed alerts are still presented in alerts
# which still assigned with the incident
Expand All @@ -2240,7 +2238,7 @@ def remove_alerts_to_incident_by_incident_id(
.join(AlertToIncident, Alert.id == AlertToIncident.alert_id)
.filter(
AlertToIncident.incident_id == incident_id,
service_field.in_(alerts_data_for_incident["services"])
service_field.in_(alerts_data_for_incident["services"]),
)
).scalars()

Expand All @@ -2251,21 +2249,31 @@ def remove_alerts_to_incident_by_incident_id(
.join(AlertToIncident, Alert.id == AlertToIncident.alert_id)
.filter(
AlertToIncident.incident_id == incident_id,
col(Alert.provider_type).in_(alerts_data_for_incident["sources"])
col(Alert.provider_type).in_(alerts_data_for_incident["sources"]),
)
).scalars()

# Making lists of services and sources to remove from the incident
services_to_remove = [service for service in alerts_data_for_incident["services"]
if service not in services_existed]
sources_to_remove = [source for source in alerts_data_for_incident["sources"]
if source not in sources_existed]
services_to_remove = [
service
for service in alerts_data_for_incident["services"]
if service not in services_existed
]
sources_to_remove = [
source
for source in alerts_data_for_incident["sources"]
if source not in sources_existed
]

# filtering removed entities from affected services and sources in the incident
incident.affected_services = [service for service in incident.affected_services
if service not in services_to_remove]
incident.sources = [source for source in incident.sources
if source not in sources_to_remove]
incident.affected_services = [
service
for service in incident.affected_services
if service not in services_to_remove
]
incident.sources = [
source for source in incident.sources if source not in sources_to_remove
]

incident.alerts_count -= alerts_data_for_incident["count"]
session.add(incident)
Expand Down Expand Up @@ -2440,3 +2448,30 @@ def get_all_topology_data(
service_dtos = [TopologyServiceDtoOut.from_orm(service) for service in services]

return service_dtos


def get_tags(tenant_id):
with Session(engine) as session:
tags = session.exec(select(Tag).where(Tag.tenant_id == tenant_id)).all()
return tags


def create_tag(tag: Tag):
with Session(engine) as session:
session.add(tag)
session.commit()
session.refresh(tag)
return tag


def assign_tag_to_preset(tenant_id: str, tag_id: str, preset_id: str):
with Session(engine) as session:
tag_preset = PresetTagLink(
tenant_id=tenant_id,
tag_id=tag_id,
preset_id=preset_id,
)
session.add(tag_preset)
session.commit()
session.refresh(tag_preset)
return tag_preset
Loading

0 comments on commit bb7859b

Please sign in to comment.