Skip to content

Commit

Permalink
feat: note and ticket assignment and presets from alerts selection (#762
Browse files Browse the repository at this point in the history
)

Co-authored-by: Tal Borenstein <tal@keephq.dev>
  • Loading branch information
shahargl and talboren committed Jan 31, 2024
1 parent 6ac271e commit 593161a
Show file tree
Hide file tree
Showing 20 changed files with 932 additions and 305 deletions.
60 changes: 55 additions & 5 deletions keep-ui/app/alerts/alert-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,29 @@ import { getSession } from "next-auth/react";
import { getApiURL } from "utils/apiUrl";
import { AlertDto } from "./models";
import { useAlerts } from "utils/hooks/useAlerts";
import { PlusIcon } from "@radix-ui/react-icons";
import { toast } from "react-toastify";
import { usePresets } from "utils/hooks/usePresets";
import { usePathname, useRouter } from "next/navigation";

interface Props {
selectedRowIds: string[];
alerts: AlertDto[];
}

export default function AlertActions({ selectedRowIds, alerts }: Props) {
const pathname = usePathname();
const router = useRouter();
const { useAllAlerts } = useAlerts();
const { mutate } = useAllAlerts();
const { mutate } = useAllAlerts({ revalidateOnFocus: false });
const { useAllPresets } = usePresets();
const { mutate: presetsMutator } = useAllPresets({
revalidateOnFocus: false,
});

const selectedAlerts = alerts.filter((_alert, index) =>
selectedRowIds.includes(index.toString())
);

const onDelete = async () => {
const confirmed = confirm(
Expand All @@ -23,10 +37,6 @@ export default function AlertActions({ selectedRowIds, alerts }: Props) {
const session = await getSession();
const apiUrl = getApiURL();

const selectedAlerts = alerts.filter((_alert, index) =>
selectedRowIds.includes(index.toString())
);

for await (const alert of selectedAlerts) {
const { fingerprint } = alert;

Expand All @@ -51,6 +61,36 @@ export default function AlertActions({ selectedRowIds, alerts }: Props) {
}
};

async function addOrUpdatePreset() {
const presetName = prompt("Enter new preset name");
if (presetName) {
const distinctAlertNames = Array.from(
new Set(selectedAlerts.map((alert) => alert.name))
);
const options = distinctAlertNames.map((name) => {
return { value: `name=${name}`, label: `name=${name}` };
});
const session = await getSession();
const apiUrl = getApiURL();
const response = await fetch(`${apiUrl}/preset`, {
method: "POST",
headers: {
Authorization: `Bearer ${session?.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: presetName, options: options }),
});
if (response.ok) {
toast(`Preset ${presetName} created!`, {
position: "top-left",
type: "success",
});
presetsMutator();
router.replace(`${pathname}?selectedPreset=${presetName}`);
}
}
}

return (
<div className="w-full flex justify-end items-center py-0.5">
<Button
Expand All @@ -62,6 +102,16 @@ export default function AlertActions({ selectedRowIds, alerts }: Props) {
>
Delete {selectedRowIds.length} alert(s)
</Button>
<Button
icon={PlusIcon}
size="xs"
color="orange"
className="ml-2.5"
onClick={async () => await addOrUpdatePreset()}
tooltip="Save current filter as a view"
>
Create Preset
</Button>
</div>
);
}
199 changes: 199 additions & 0 deletions keep-ui/app/alerts/alert-assign-ticket-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import React from 'react';
import Select, { components } from 'react-select';
import { Dialog } from '@headlessui/react';
import { Button, TextInput } from '@tremor/react';
import { PlusIcon } from '@heroicons/react/20/solid'
import { useForm, Controller, SubmitHandler } from 'react-hook-form';
import { Providers } from "./../providers/providers";
import { useSession } from "next-auth/react";
import { getApiURL } from 'utils/apiUrl';

interface AlertAssignTicketModalProps {
isOpen: boolean;
onClose: () => void;
ticketingProviders: Providers; // Replace 'ProviderType' with the actual type of ticketingProviders
alertFingerprint: string; // Replace 'string' with the actual type of alertFingerprint
}

interface OptionType {
value: string;
label: string;
id: string;
type: string;
icon?: string;
isAddProvider?: boolean;
}

interface FormData {
provider: {
id: string;
value: string;
type: string;
};
ticket_url: string;
}

const AlertAssignTicketModal = ({ isOpen, onClose, ticketingProviders, alertFingerprint }: AlertAssignTicketModalProps) => {
const { handleSubmit, control, formState: { errors } } = useForm<FormData>();
// get the token
const { data: session } = useSession();

const onSubmit: SubmitHandler<FormData> = async (data) => {
try {
// build the formData
const requestData = {
enrichments: {
ticket_type: data.provider.type,
ticket_url: data.ticket_url,
ticket_provider_id: data.provider.value,
},
fingerprint: alertFingerprint,
};


const response = await fetch(`${getApiURL()}/alerts/enrich`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${session?.accessToken}`,
},
body: JSON.stringify(requestData),
});

if (response.ok) {
// Handle success
console.log("Ticket assigned successfully");
onClose();
} else {
// Handle error
console.error("Failed to assign ticket");
}
} catch (error) {
// Handle unexpected error
console.error("An unexpected error occurred");
}
};

const providerOptions: OptionType[] = ticketingProviders.map((provider) => ({
id: provider.id,
value: provider.id,
label: provider.details.name || '',
type: provider.type,
}));

const customOptions: OptionType[] = [
...providerOptions,
{
value: 'add_provider',
label: 'Add another ticketing provider',
icon: 'plus',
isAddProvider: true,
id: 'add_provider',
type: '',
},
];

const handleOnChange = (option: any) => {
if (option.value === 'add_provider') {
window.open('/providers?labels=ticketing', '_blank');
}
};


const Option = (props: any) => {
// Check if the option is 'add_provider'
const isAddProvider = props.data.isAddProvider;

return (
<components.Option {...props}>
<div className="flex items-center">
{isAddProvider ? (
<PlusIcon className="h-5 w-5 text-gray-400 mr-2" />
) : (
props.data.type && <img src={`/icons/${props.data.type}-icon.png`} alt="" style={{ height: '20px', marginRight: '10px' }} />
)}
<span style={{ color: isAddProvider ? 'gray' : 'inherit' }}>{props.data.label}</span>
</div>
</components.Option>
);
};

const SingleValue = (props: any) => {
const { children, data } = props;

return (
<components.SingleValue {...props}>
<div className="flex items-center">
{data.isAddProvider ? (
<PlusIcon className="h-5 w-5 text-gray-400 mr-2" />
) : (
data.type && <img src={`/icons/${data.type}-icon.png`} alt="" style={{ height: '20px', marginRight: '10px' }} />
)}
{children}
</div>
</components.SingleValue>
);
};

return (
<Dialog open={isOpen} onClose={onClose} className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex items-center justify-center min-h-screen px-4">
<Dialog.Overlay className="fixed inset-0 bg-black opacity-30" />
<div className="relative bg-white p-6 rounded-lg" style={{ width: "400px", maxWidth: "90%" }}>
<Dialog.Title className="text-lg font-semibold">Assign Ticket</Dialog.Title>
{ticketingProviders.length > 0 ? (
<form onSubmit={handleSubmit(onSubmit)} className="mt-4">
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700">Ticket Provider</label>
<Controller
name="provider"
control={control}
rules={{ required: 'Provider is required' }}
render={({ field }) => (
<Select {...field} options={customOptions} onChange={(option) => { field.onChange(option); handleOnChange(option); }} components={{ Option, SingleValue }} />
)}
/>
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700">Ticket URL</label>
<Controller
name="ticket_url"
control={control}
rules={{
required: 'URL is required',
pattern: {
value: /^(https?|http):\/\/[^\s/$.?#].[^\s]*$/i,
message: 'Invalid URL format',
},
}}
render={({ field }) => (
<>
<TextInput {...field} className="w-full mt-1" placeholder="Ticket URL" />
{errors.ticket_url && <span className="text-red-500">{errors.ticket_url.message}</span>}
</>
)}
/>
</div>
<div className="mt-6 flex gap-2">
<Button color="orange" type="submit">Assign Ticket</Button>
<Button onClick={onClose} variant="secondary" className="border border-orange-500 text-orange-500">Cancel</Button>
</div>
</form>
) : (
<div className="text-center mt-4">
<p className="text-gray-700 text-sm">
Please connect at least one ticketing provider to use this feature.
</p>
<Button onClick={() => window.open('/providers?labels=ticketing', '_blank')} color="orange" className="mt-4 mr-4">
Connect Ticketing Provider
</Button>
<Button onClick={onClose} variant="secondary" className="mt-4 border border-orange-500 text-orange-500">Close</Button>
</div>
)}
</div>
</div>
</Dialog>
);
};

export default AlertAssignTicketModal;
2 changes: 1 addition & 1 deletion keep-ui/app/alerts/alert-extra-payload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default function AlertExtraPayload({
Extra Payload
</AccordionHeader>
<AccordionBody ref={ref}>
<pre className="overflow-y-scroll">
<pre className="overflow-scroll max-w-lg">
{JSON.stringify(extraPayload, null, 2)}
</pre>
</AccordionBody>
Expand Down
Loading

0 comments on commit 593161a

Please sign in to comment.