added reports page

This commit is contained in:
PxlLoewe
2025-05-16 22:49:44 -07:00
parent da5ec8470d
commit 40ca6b1bd9
25 changed files with 1355 additions and 1363 deletions

View File

@@ -0,0 +1,24 @@
import { Router } from "express";
import { prisma } from "@repo/db";
const router = Router();
router.put("/", async (req, res) => {
try {
const report = await prisma.report.create({
data: req.body,
});
// TODO: send link to report on admin page to user
res.json(report);
} catch (error) {
res.status(500).json({
message: "Error creating report",
error: error instanceof Error ? error.message : String(error),
});
}
});
export default router;

View File

@@ -4,6 +4,7 @@ import dispatcherRotuer from "./dispatcher";
import missionRouter from "./mission";
import statusRouter from "./status";
import aircraftsRouter from "./aircraft";
import reportRouter from "./report";
const router = Router();
@@ -12,5 +13,6 @@ router.use("/dispatcher", dispatcherRotuer);
router.use("/mission", missionRouter);
router.use("/status", statusRouter);
router.use("/aircrafts", aircraftsRouter);
router.use("/report", reportRouter);
export default router;

View File

@@ -3,11 +3,12 @@ import { ExclamationTriangleIcon, PaperPlaneIcon } from "@radix-ui/react-icons";
import { useSession } from "next-auth/react";
import { useEffect, useState } from "react";
import { cn } from "helpers/cn";
import { serverApi } from "helpers/axios";
import { toast } from "react-hot-toast";
import { useLeftMenuStore } from "_store/leftMenuStore";
import { asPublicUser } from "@repo/db";
import { useQuery } from "@tanstack/react-query";
import { getConnectedUserAPI } from "querys/connected-user";
import { sendReportAPI } from "querys/report";
export const Report = () => {
const { setChatOpen, setReportTabOpen, reportTabOpen, setOwnId } =
@@ -106,18 +107,18 @@ export const Report = () => {
e.preventDefault();
if (message.length < 1 || !selectedPlayer) return;
setSending(true);
serverApi("/report", {
method: "POST",
data: {
message,
to: selectedPlayer,
},
sendReportAPI({
text: message,
senderUserId: session.data!.user.id,
reportedUserId: selectedPlayer,
})
.then(() => {
toast.success("Report gesendet");
setMessage("");
setSending(false);
})
.catch(() => {
.catch((err) => {
toast.error(`Fehler beim Senden des Reports: ${err}`);
setSending(false);
});
}}

View File

@@ -0,0 +1,19 @@
import { Prisma, Report } from "@repo/db";
import { serverApi } from "helpers/axios";
export const sendReportAPI = async (
report:
| (Prisma.Without<
Prisma.ReportCreateInput,
Prisma.ReportUncheckedCreateInput
> &
Prisma.ReportUncheckedCreateInput)
| (Prisma.Without<
Prisma.ReportUncheckedCreateInput,
Prisma.ReportCreateInput
> &
Prisma.ReportCreateInput),
) => {
const repsonse = await serverApi.put("/report", report);
return repsonse.data as Report;
};

View File

@@ -1,81 +1,44 @@
"use client";
import { useEffect, useState } from "react";
import { Eye } from "lucide-react";
import { fetchReportDetails, handleMarkAsResolved } from "../actions";
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import { prisma } from "@repo/db";
import { Error } from "_components/Error";
import {
ReportAdmin,
ReportSenderInfo,
} from "(app)/admin/report/_components/form";
export default function ReportDetailsPage({
params: paramsPromise,
export default async function ReportDetailsPage({
params,
}: {
params: Promise<{ id: number }>;
params: { id: string };
}) {
const [params, setParams] = useState<{ id: number } | null>(null);
const [report, setReport] = useState<any>(null);
const [loading, setLoading] = useState(true);
const { id } = await params;
useEffect(() => {
async function unwrapParams() {
const resolvedParams = await paramsPromise;
setParams(resolvedParams);
}
unwrapParams();
}, [paramsPromise]);
const report = await prisma.report.findUnique({
where: {
id: Number(id),
},
include: {
Reported: true,
Reviewer: true,
Sender: true,
},
});
useEffect(() => {
if (!params) return;
async function loadReport() {
if (!params) return;
const fetchedReport = await fetchReportDetails(parseInt(params.id));
setReport(fetchedReport);
setLoading(false);
}
loadReport();
}, [params]);
if (!params || loading) return <div>Loading...</div>;
if (!report) return <div>Report not found</div>;
if (!report) return <Error statusCode={404} title="User not found" />;
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4 flex items-center gap-2">
<Eye className="w-6 h-6" /> Report Details
</h1>
<div className="grid grid-cols-2 gap-4">
<div>
<p>
<b>Sender:</b> {report.sender.firstname} {report.sender.lastname} (
{report.sender.publicId})
</p>
<p>
<b>Reported:</b> {report.reported.firstname}{" "}
{report.reported.lastname} ({report.reported.publicId})
</p>
<p>
<b>Timestamp:</b> {new Date(report.timestamp).toLocaleString()}
</p>
</div>
<div>
<p>
<b>Message:</b>
</p>
<p>{report.text}</p>
</div>
<div className="grid grid-cols-6 gap-4">
<div className="col-span-full">
<p className="text-2xl font-semibold text-left flex items-center gap-2">
<ExclamationTriangleIcon className="w-5 h-5" />
Report #{report.id}
</p>
</div>
<div className="mt-4 flex gap-2">
<button
className="btn btn-success btn-outline"
onClick={async () => {
await handleMarkAsResolved(report.id);
}}
>
Erledigen
</button>
<button
className="btn btn-primary btn-outline"
onClick={() => window.history.back()}
>
Zurück
</button>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<ReportSenderInfo report={report} />
</div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<ReportAdmin report={report} />
</div>
</div>
);

View File

@@ -0,0 +1,138 @@
"use client";
import { editReport } from "(app)/admin/report/actions";
import { zodResolver } from "@hookform/resolvers/zod";
import { Report as IReport, User } from "@repo/db";
import { ReportSchema, Report as IReportZod } from "@repo/db/zod";
import { Button } from "_components/ui/Button";
import { Switch } from "_components/ui/Switch";
import { Trash } from "lucide-react";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
export const ReportSenderInfo = ({
report,
}: {
report: IReport & {
Reported?: User;
Sender?: User;
};
}) => {
const { Reported, Sender } = report;
return (
<div className="card-body">
<Link
href={`/admin/user/${Reported?.id}`}
className="card-title link link-hover"
>
{Reported?.firstname} {Reported?.lastname} ({Reported?.publicId})
</Link>
<div className="textarea w-full text-left">{report.text}</div>
<Link
href={`/admin/user/${Reported?.id}`}
className="text-sm text-gray-600 text-right link link-hover"
>
Meldet Nutzer {Sender?.firstname} {Sender?.lastname} ({Sender?.publicId}
) am {new Date(report.timestamp).toLocaleString()}
</Link>
</div>
);
};
export const ReportAdmin = ({
report,
}: {
report: IReport & {
Reported?: User;
Sender?: User;
Reviewer?: User | null;
};
}) => {
const { Reviewer } = report;
const [isEditLoading, setIsEditLoading] = useState(false);
const session = useSession();
const router = useRouter();
const form = useForm<IReportZod>({
resolver: zodResolver(ReportSchema),
defaultValues: report,
});
return (
<form
className="card-body"
onSubmit={form.handleSubmit(async (values) => {
setIsEditLoading(true);
const newReport = await editReport(values.id, {
reviewerUserId: session.data?.user.id,
reviewerComment: values.reviewerComment,
reviewed: values.reviewed,
});
form.reset(newReport);
setIsEditLoading(false);
router.refresh();
toast.success("Deine Änderungen wurden gespeichert!", {
style: {
background: "var(--color-base-100)",
color: "var(--color-base-content)",
},
});
})}
>
<h2 className="card-title">Staff Kommentar</h2>
<textarea
{...form.register("reviewerComment")}
className="textarea w-full"
placeholder=""
/>
<p className="text-sm text-gray-600 text-right">
{report.Reviewer &&
`Kommentar von ${Reviewer?.firstname} ${Reviewer?.lastname} (${Reviewer?.publicId})`}
</p>
<Switch
form={form}
name="reviewed"
label="Report als geklärt markieren"
/>
<div className="card-actions flex justify-between">
<Button
role="submit"
className="btn-sm btn-wide btn-outline btn-primary"
disabled={!form.formState.isDirty}
isLoading={isEditLoading}
>
Speichern
</Button>
<Button
role="button"
className="btn btn-warning"
onSubmit={() => false}
onClick={async () => {
await editReport(report.id, {
reviewerUserId: null,
reviewerComment: null,
reviewed: false,
});
form.reset({
...report,
reviewerComment: null,
reviewed: false,
});
router.refresh();
toast.success("Kommentar gelöscht!", {
style: {
background: "var(--color-base-100)",
color: "var(--color-base-content)",
},
});
}}
>
<Trash /> Kommentar löschen
</Button>
</div>
</form>
);
};

View File

@@ -1,48 +1,14 @@
"use server";
import { prisma, User } from "@repo/db";
import { Prisma, prisma } from "@repo/db";
export const markAsResolved = async (id: number) => {
await prisma.report.update({
where: { id: id },
data: { erledigt: true },
});
};
// New function to handle marking a report as resolved
export const handleMarkAsResolved = async (id: number) => {
try {
await markAsResolved(id);
return { success: true };
} catch (error) {
console.error("Error marking report as resolved:", error);
return { success: false, error: error };
}
};
export const getReports = async () => {
return prisma.report.findMany({
include: {
sender: true,
reported: true,
},
});
};
export const getUserReports = async (user: User) => {
return prisma.report.findMany({
export const editReport = async (
id: number,
data: Prisma.ReportUncheckedUpdateInput,
) => {
return await prisma.report.update({
where: {
reportedUserId: user.id,
},
include: {
sender: true,
reported: true,
id: id,
},
});
};
export const fetchReportDetails = async (id: number) => {
return prisma.report.findUnique({
where: { id },
include: { sender: true, reported: true },
data,
});
};

View File

@@ -1,122 +1,71 @@
"use client";
import { useEffect, useState } from "react";
import { Check, Eye, X } from "lucide-react";
import Link from "next/link";
import { getReports, handleMarkAsResolved } from "./actions";
import { PaginatedTable } from "_components/PaginatedTable";
import { Report, User } from "@repo/db";
import { ColumnDef } from "@tanstack/react-table";
export default function ReportPage() {
const [reports, setReports] = useState<
{
id: number;
timestamp: string;
erledigt: boolean;
sender: {
id: string;
firstname: string;
lastname: string;
publicId: string;
};
reported: {
id: string;
firstname: string;
lastname: string;
publicId: string;
};
}[]
>([]);
useEffect(() => {
const fetchReports = async () => {
const reps = await getReports();
const transformedReports = reps.map((report) => ({
id: report.id,
timestamp: report.timestamp.toISOString(),
erledigt: report.erledigt,
sender: {
id: report.sender.id,
firstname: report.sender.firstname,
lastname: report.sender.lastname,
publicId: report.sender.publicId,
},
reported: {
id: report.reported.id,
firstname: report.reported.firstname,
lastname: report.reported.lastname,
publicId: report.reported.publicId,
},
}));
setReports(transformedReports);
};
fetchReports();
}, []);
return (
<>
<div className="flex items-center gap-2 mb-4">
<Eye className="w-5 h-5" />{" "}
<span className="text-lg font-bold">Reports</span>
</div>
<div className="overflow-x-auto">
<table className="table table-zebra w-full">
<thead>
<tr>
<th>Erledigt</th>
<th>Sender</th>
<th>Reported</th>
<th>Time</th>
<th>ID</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{reports.map((report) => (
<tr key={report.id}>
<td className="text-center">
{report.erledigt ? (
<PaginatedTable
prismaModel="report"
include={{
Sender: true,
Reported: true,
}}
columns={
[
{
accessorKey: "reviewed",
header: "Erledigt",
cell: ({ row }) => {
return (
<div className="text-center">
{row.getValue("reviewed") ? (
<Check className="text-green-500 w-5 h-5" />
) : (
<X className="text-red-500 w-5 h-5" />
)}
</td>
<td>{`${report.sender.firstname} ${report.sender.lastname} (${report.sender.publicId})`}</td>
<td>{`${report.reported.firstname} ${report.reported.lastname} (${report.reported.publicId})`}</td>
<td>{new Date(report.timestamp).toLocaleString()}</td>
<td>{report.id}</td>
<td>
<div className="flex gap-2">
<Link href={`/admin/report/${report.id}`}>
<button className="btn btn-sm btn-outline btn-info flex items-center gap-2">
<Eye className="w-4 h-4" /> Anzeigen
</button>
</Link>
{!report.erledigt && (
<button
className="btn btn-sm btn-outline btn-success flex items-center gap-2"
onClick={async () => {
const result = await handleMarkAsResolved(report.id);
if (result.success) {
setReports((prevReports) =>
prevReports.map((r) =>
r.id === report.id
? { ...r, erledigt: true }
: r,
),
);
} else {
alert("Error: " + result.error);
}
}}
>
<Check className="w-4 h-4" /> Erledigen
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
</div>
);
},
},
{
accessorKey: "Sender",
header: "Sender",
cell: ({ row }) => {
const user = row.getValue("Sender") as User;
return `${user.firstname} ${user.lastname} (${user.publicId})`;
},
},
{
accessorKey: "Reported",
header: "Reported",
cell: ({ row }) => {
const user = row.getValue("Reported") as User;
return `${user.firstname} ${user.lastname} (${user.publicId})`;
},
},
{
accessorKey: "timestamp",
header: "Time",
cell: ({ row }) =>
new Date(row.getValue("timestamp")).toLocaleString(),
},
{
accessorKey: "actions",
header: "Actions",
cell: ({ row }) => (
<Link href={`/admin/report/${row.original.id}`}>
<button className="btn btn-sm btn-outline btn-info flex items-center gap-2">
<Eye className="w-4 h-4" /> Anzeigen
</button>
</Link>
),
},
] as ColumnDef<Report>[]
}
/>
);
}

View File

@@ -1,47 +1,49 @@
import { DatabaseBackupIcon } from 'lucide-react';
import { PaginatedTable } from '../../../_components/PaginatedTable';
import Link from 'next/link';
import { DatabaseBackupIcon } from "lucide-react";
import { PaginatedTable } from "../../../_components/PaginatedTable";
import Link from "next/link";
export default () => {
return (
<>
<PaginatedTable
showEditButton
prismaModel="station"
searchFields={['bosCallsign', 'bosUse', 'country', 'operator']}
columns={[
{
header: 'BOS Name',
accessorKey: 'bosCallsign',
},
{
header: 'Bos Use',
accessorKey: 'bosUse',
},
{
header: 'Country',
accessorKey: 'country',
},
{
header: 'operator',
accessorKey: 'operator',
},
]}
leftOfSearch={
<span className="flex items-center gap-2">
<DatabaseBackupIcon className="w-5 h-5" /> Stationen
</span>
}
rightOfSearch={
<p className="text-2xl font-semibold text-left flex items-center gap-2 justify-between">
<Link href={'/admin/station/new'}>
<button className="btn btn-sm btn-outline btn-primary">
Erstellen
</button>
</Link>
</p>
}
/>
</>
);
const page = () => {
return (
<>
<PaginatedTable
showEditButton
prismaModel="station"
searchFields={["bosCallsign", "bosUse", "country", "operator"]}
columns={[
{
header: "BOS Name",
accessorKey: "bosCallsign",
},
{
header: "Bos Use",
accessorKey: "bosUse",
},
{
header: "Country",
accessorKey: "country",
},
{
header: "operator",
accessorKey: "operator",
},
]}
leftOfSearch={
<span className="flex items-center gap-2">
<DatabaseBackupIcon className="w-5 h-5" /> Stationen
</span>
}
rightOfSearch={
<p className="text-2xl font-semibold text-left flex items-center gap-2 justify-between">
<Link href={"/admin/station/new"}>
<button className="btn btn-sm btn-outline btn-primary">
Erstellen
</button>
</Link>
</p>
}
/>
</>
);
};
export default page;

View File

@@ -1,98 +0,0 @@
import { User } from "@repo/db";
import { Check, Eye, X } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { getUserReports, handleMarkAsResolved } from "../../../report/actions";
export default function UserReports({ user }: { user: User }) {
const [reports, setReports] = useState<
{
id: number;
timestamp: string;
erledigt: boolean;
sender: {
id: string;
firstname: string;
lastname: string;
publicId: string;
};
}[]
>([]);
useEffect(() => {
const fetchReports = async (user: User) => {
const reps = await getUserReports(user);
const transformedReports = reps.map((report) => ({
id: report.id,
timestamp: report.timestamp.toISOString(),
erledigt: report.erledigt,
sender: {
id: report.sender.id,
firstname: report.sender.firstname,
lastname: report.sender.lastname,
publicId: report.sender.publicId,
},
}));
setReports(transformedReports);
};
fetchReports(user);
}, [user]);
return (
<div className="overflow-x-auto">
<table className="table table-zebra w-full">
<thead>
<tr>
<th>Erledigt</th>
<th>Sender</th>
<th>Time</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{reports.map((report) => (
<tr key={report.id}>
<td className="text-center">
{report.erledigt ? (
<Check className="text-green-500 w-5 h-5" />
) : (
<X className="text-red-500 w-5 h-5" />
)}
</td>
<td>{`${report.sender.firstname} ${report.sender.lastname} (${report.sender.publicId})`}</td>
<td>{new Date(report.timestamp).toLocaleString()}</td>
<td>
<div className="flex gap-2">
<Link href={`/admin/report/${report.id}`}>
<button className="btn btn-sm btn-outline btn-info flex items-center gap-2">
<Eye className="w-4 h-4" /> Anzeigen
</button>
</Link>
{!report.erledigt && (
<button
className="btn btn-sm btn-outline btn-success flex items-center gap-2"
onClick={async () => {
const result = await handleMarkAsResolved(report.id);
if (result.success) {
setReports((prevReports) =>
prevReports.map((r) =>
r.id === report.id ? { ...r, erledigt: true } : r,
),
);
} else {
alert("Error: " + result.error);
}
}}
>
<Check className="w-4 h-4" /> Erledigen
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View File

@@ -24,7 +24,6 @@ import { min } from "date-fns";
import { cn } from "../../../../../../helper/cn";
import { ChartBarBigIcon, PlaneIcon } from "lucide-react";
import Link from "next/link";
import UserReports from "./UserReports";
interface ProfileFormProps {
user: User;
@@ -420,7 +419,7 @@ export const AdminForm = ({ user, dispoTime, pilotTime }: AdminFormProps) => {
<h2 className="card-title">
<ExclamationTriangleIcon className="w-5 h-5" /> Reports
</h2>
<UserReports user={user} />
{/* TODO: Report summary Here */}
</div>
);
};

View File

@@ -1,10 +1,9 @@
import { PersonIcon } from "@radix-ui/react-icons";
import { PrismaClient, User } from "@repo/db";
import { prisma, User } from "@repo/db";
import { AdminForm, ConnectionHistory, ProfileForm } from "./_components/forms";
import { Error } from "../../../../_components/Error";
const Page = async ({ params }: { params: { id: string } }) => {
const prisma = new PrismaClient();
const { id } = await params;
const user: User | null = await prisma.user.findUnique({

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,12 @@
# [1.4.0](https://github.com/grafana/profiles-drilldown/compare/v1.3.0...v1.4.0) (2025-05-15)
### Features
* Add extension point to add additional settings ([#478](https://github.com/grafana/profiles-drilldown/issues/478)) ([4ff758f](https://github.com/grafana/profiles-drilldown/commit/4ff758f4b2d0a458da647b663b2488140a0c0b63))
# [1.3.0](https://github.com/grafana/explore-profiles/compare/v1.2.3...v1.3.0) (2025-04-16)

View File

@@ -8,44 +8,48 @@ Hash: SHA512
"signedByOrg": "grafana",
"signedByOrgName": "Grafana Labs",
"plugin": "grafana-pyroscope-app",
"version": "1.3.0",
"time": 1744790362190,
"version": "1.4.0",
"time": 1747304216077,
"keyId": "7e4d0c6a708866e7",
"files": {
"module.js.LICENSE.txt": "84798babe5a84ee41efdf41174af68e377c212b027183ecdd830747793156ded",
"plugin.json": "249922ca0b392205fcbc173e77e01e0d74b7ff5df75dd9060d4b7a2a51fadfdf",
"715.js": "c87a98e6efaacbfec42c46f2fab14e9fda694fda48b209c7d44dafef92c2005c",
"350.js": "753480a1eb3b8f9846b91299e3b5edf9e8b4853b0fa7c83895b8ac52590209f7",
"LICENSE": "8486a10c4393cee1c25392769ddd3b2d6c242d6ec7928e1414efff7dfb2f07ef",
"CHANGELOG.md": "196cf272cc550487bf13bdc66c800220d0477583d5a8ca0e9d683452a7075e9d",
"pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/SceneEmptyState/ui/img/grot-404-dark.svg": "a0c8acbcf5685a8950ce1c67722579dc745585fb0d668ce965327955e5e829ff",
"pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/SceneEmptyState/ui/img/grot-404-light.svg": "89ea40b6dcf2dc8dfe146f8acac42b604e4d3c3dad03e539551d58a21f80654d",
"350.js.LICENSE.txt": "84798babe5a84ee41efdf41174af68e377c212b027183ecdd830747793156ded",
"350.js.map": "c872929c0280ed6b54c0aa60fb01388a226ea037ef7671982ea330d3b1574cd6",
"944c737f589d02ecf603.svg": "a0c8acbcf5685a8950ce1c67722579dc745585fb0d668ce965327955e5e829ff",
"shared/infrastructure/profile-metrics/profile-metrics.json": "0a3a345a365e72f4278d3a76d5739600483bed8f374ddc1c2af85029057b8d07",
"CHANGELOG.md": "002099f76c0cc7707f2f866625bb5d83fd504177c50b482307e6973fd45c63f2",
"e6c722427cfa8715e19d.svg": "559341996765c2d5639a2818d76bcd88ffe252212193a573f2f9f77dae5064dd",
"module.js.map": "8883883ae2f79d66ce4faa5f870219761990244adf4b53f7b6bcb213bc710c9d",
"e79edcfbe2068fae2364.svg": "89ea40b6dcf2dc8dfe146f8acac42b604e4d3c3dad03e539551d58a21f80654d",
"715.js.map": "3c82669890d387d1d8ed9d7ea8bd0ccd2b94f5dedfbb94a6bc8e5475a7093a90",
"module.js": "023078e56a3c48fa8465863544d512e6a36f7c5bba4bb21b7ef2201b5f19ced2",
"img/bafee50693eb02088442.png": "66d5311c4ca898cdae2d0a23a414f04a7c49052f0035c1a2906b9e9bb15d628d",
"img/9c9cdd5175734d579007.png": "ab65c374d22c5faad274f6b8b2ab00bf404bb988803f09d375326cd692fce821",
"img/58f0b0e1cfa063e4b662.png": "87598baf93192a8dc7ee923e0af6a0c5e4b3359b00b7391fc9530108feb7aac0",
"img/diff-view-how-to.gif": "72afdd2fcad33e13db33af765a3fae9528315d78391684190dd23e40bd688852",
"img/decrease-latency.png": "f626933745990d2fef3d90e995f38d5e28e4754c002cf031379db7588c8fd70c",
"img/reduce-costs.png": "ab65c374d22c5faad274f6b8b2ab00bf404bb988803f09d375326cd692fce821",
"img/8cdf4d2e2df8326311ab.gif": "72afdd2fcad33e13db33af765a3fae9528315d78391684190dd23e40bd688852",
"img/61b4cf746a6f58780f27.png": "f626933745990d2fef3d90e995f38d5e28e4754c002cf031379db7588c8fd70c",
"img/reduce-costs.png": "ab65c374d22c5faad274f6b8b2ab00bf404bb988803f09d375326cd692fce821",
"img/decrease-latency.png": "f626933745990d2fef3d90e995f38d5e28e4754c002cf031379db7588c8fd70c",
"img/hero-image.png": "87598baf93192a8dc7ee923e0af6a0c5e4b3359b00b7391fc9530108feb7aac0",
"img/9c9cdd5175734d579007.png": "ab65c374d22c5faad274f6b8b2ab00bf404bb988803f09d375326cd692fce821",
"img/diff-view-how-to.gif": "72afdd2fcad33e13db33af765a3fae9528315d78391684190dd23e40bd688852",
"img/resolve-incidents.png": "66d5311c4ca898cdae2d0a23a414f04a7c49052f0035c1a2906b9e9bb15d628d",
"img/logo.svg": "559341996765c2d5639a2818d76bcd88ffe252212193a573f2f9f77dae5064dd",
"img/hero-image.png": "87598baf93192a8dc7ee923e0af6a0c5e4b3359b00b7391fc9530108feb7aac0",
"img/8cdf4d2e2df8326311ab.gif": "72afdd2fcad33e13db33af765a3fae9528315d78391684190dd23e40bd688852",
"README.md": "da879e54a2da3e7134c14016f0e5b59c9255da5b81d03a02e3e8d47356e15638",
"module.js.map": "148d0a70d1c8bf76daa71107e7eaad43b69db051cacff5a09a649255d0009840",
"e6c722427cfa8715e19d.svg": "559341996765c2d5639a2818d76bcd88ffe252212193a573f2f9f77dae5064dd",
"shared/infrastructure/profile-metrics/profile-metrics.json": "0a3a345a365e72f4278d3a76d5739600483bed8f374ddc1c2af85029057b8d07",
"pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/SceneEmptyState/ui/img/grot-404-light.svg": "89ea40b6dcf2dc8dfe146f8acac42b604e4d3c3dad03e539551d58a21f80654d",
"pages/ProfilesExplorerView/components/SceneByVariableRepeaterGrid/components/SceneEmptyState/ui/img/grot-404-dark.svg": "a0c8acbcf5685a8950ce1c67722579dc745585fb0d668ce965327955e5e829ff",
"e79edcfbe2068fae2364.svg": "89ea40b6dcf2dc8dfe146f8acac42b604e4d3c3dad03e539551d58a21f80654d",
"plugin.json": "5c6a1c691e238e51599e4fcdb92a9be1ccdeabf340aad6faecab7faf15473078",
"944c737f589d02ecf603.svg": "a0c8acbcf5685a8950ce1c67722579dc745585fb0d668ce965327955e5e829ff",
"module.js": "9f2c0361bb11aeb56c2c8abdbfa2f6a54b985c7f22c8f51427af2973df81b4e7"
"img/58f0b0e1cfa063e4b662.png": "87598baf93192a8dc7ee923e0af6a0c5e4b3359b00b7391fc9530108feb7aac0",
"README.md": "9808cbc6131d2512117f366e51f5ede7aeea5b73a852f0763ba2d05ce7334dd0"
}
}
-----BEGIN PGP SIGNATURE-----
Version: OpenPGP.js v4.10.11
Comment: https://openpgpjs.org
wrkEARMKAAYFAmf/Y1oAIQkQfk0ManCIZucWIQTzOyW2kQdOhGNlcPN+TQxq
cIhm5+hKAgkA2MEgGa5HxlYzxQ9tJdQy2vUJjZYH630QBRfx11WSCh8l6Ths
qk6f2HzwsZSWh4kDkzfwFqSvMF3l33FUHGpH5z4CCQGwhcUQScay0D+FIjtN
BaAv7DIPpcG5fNYcaxdEGj8mt4UfNcE5zvs3yJ5bf88arZafNskJq/HpxDbn
N5W9l+XEAg==
=DXtv
wrkEARMKAAYFAmglvxgAIQkQfk0ManCIZucWIQTzOyW2kQdOhGNlcPN+TQxq
cIhm5/lnAgkAv2sUDLW60ervJ8gaooLBBhK4I0BZMGpoUIGSYrBQucff9I6o
ZJ/mEaJRvckOhjCygiC7jOsnFtMYRFn6DOX4g7MCCQEpLjlyxr+mPA3B+BDE
NMaiXpsfmFVX+LDjYbG0aDltVZ8ADyQjCPlmyLRvCht6vNFvUAutMKceqoLG
qKCZ/5U08w==
=fwcs
-----END PGP SIGNATURE-----

View File

@@ -33,26 +33,26 @@ For instructions installing, refer to the [access and installation instructions]
## Resources
- [Documentation](https://grafana.com/docs/grafana-cloud/visualizations/simplified-exploration/profiles/)
- [CHANGELOG](https://github.com/grafana/explore-profiles/releases)
- [GITHUB](https://github.com/grafana/explore-profiles/)
- [CHANGELOG](https://github.com/grafana/profiles-drilldown/releases)
- [GITHUB](https://github.com/grafana/profiles-drilldown/)
## Contributing
We love accepting contributions!
If your change is minor, please feel free submit
a [pull request](https://github.com/grafana/explore-profiles/pull/new)
a [pull request](https://github.com/grafana/profiles-drilldown/pull/new)
If your change is larger, or adds a feature, please file an issue beforehand so
that we can discuss the change. You're welcome to file an implementation pull
request immediately as well, although we generally lean towards discussing the
change and then reviewing the implementation separately.
For more information, refer to [Contributing to Grafana Profiles Drilldown](https://github.com/grafana/explore-profiles/blob/main/docs/CONTRIBUTING.md)
For more information, refer to [Contributing to Grafana Profiles Drilldown](https://github.com/grafana/profiles-drilldown/blob/main/docs/CONTRIBUTING.md)
### Bugs
If your issue is a bug, please open one [here](https://github.com/grafana/explore-profiles/issues/new).
If your issue is a bug, please open one [here](https://github.com/grafana/profiles-drilldown/issues/new).
### Changes
We do not have a formal proposal process for changes or feature requests. If you have a change you would like to see in
Grafana Profiles Drilldown, please [file an issue](https://github.com/grafana/explore-profiles/issues/new) with the necessary details.
Grafana Profiles Drilldown, please [file an issue](https://github.com/grafana/profiles-drilldown/issues/new) with the necessary details.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -34,16 +34,16 @@
"path": "img/hero-image.png"
}
],
"version": "1.3.0",
"updated": "2025-04-16",
"version": "1.4.0",
"updated": "2025-05-15",
"links": [
{
"name": "GitHub",
"url": "https://github.com/grafana/explore-profiles"
"url": "https://github.com/grafana/profiles-drilldown"
},
{
"name": "Report bug",
"url": "https://github.com/grafana/explore-profiles/issues/new"
"url": "https://github.com/grafana/profiles-drilldown/issues/new"
}
]
},
@@ -61,6 +61,9 @@
"extensionPoints": [
{
"id": "grafana-pyroscope-app/investigation/v1"
},
{
"id": "grafana-pyroscope-app/settings/v1"
}
],
"addedLinks": [

View File

@@ -1,14 +1,15 @@
model Report {
id Int @id @default(autoincrement())
text String
senderUserId String
reportedUserId String
timestamp DateTime @default(now())
reviewed Boolean @default(false)
reviewerUserId String?
id Int @id @default(autoincrement())
text String
senderUserId String
reportedUserId String
timestamp DateTime @default(now())
reviewerComment String?
reviewed Boolean @default(false)
reviewerUserId String?
// relations:
sender User @relation("SentReports", fields: [senderUserId], references: [id])
reported User @relation("ReceivedReports", fields: [reportedUserId], references: [id])
reviewer User? @relation("ReviewedReports", fields: [reviewerUserId], references: [id])
Sender User @relation("SentReports", fields: [senderUserId], references: [id])
Reported User @relation("ReceivedReports", fields: [reportedUserId], references: [id])
Reviewer User? @relation("ReviewedReports", fields: [reviewerUserId], references: [id])
}