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

View File

@@ -3,11 +3,12 @@ import { ExclamationTriangleIcon, PaperPlaneIcon } from "@radix-ui/react-icons";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { cn } from "helpers/cn"; import { cn } from "helpers/cn";
import { serverApi } from "helpers/axios"; import { toast } from "react-hot-toast";
import { useLeftMenuStore } from "_store/leftMenuStore"; import { useLeftMenuStore } from "_store/leftMenuStore";
import { asPublicUser } from "@repo/db"; import { asPublicUser } from "@repo/db";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { getConnectedUserAPI } from "querys/connected-user"; import { getConnectedUserAPI } from "querys/connected-user";
import { sendReportAPI } from "querys/report";
export const Report = () => { export const Report = () => {
const { setChatOpen, setReportTabOpen, reportTabOpen, setOwnId } = const { setChatOpen, setReportTabOpen, reportTabOpen, setOwnId } =
@@ -106,18 +107,18 @@ export const Report = () => {
e.preventDefault(); e.preventDefault();
if (message.length < 1 || !selectedPlayer) return; if (message.length < 1 || !selectedPlayer) return;
setSending(true); setSending(true);
serverApi("/report", { sendReportAPI({
method: "POST", text: message,
data: { senderUserId: session.data!.user.id,
message, reportedUserId: selectedPlayer,
to: selectedPlayer,
},
}) })
.then(() => { .then(() => {
toast.success("Report gesendet");
setMessage(""); setMessage("");
setSending(false); setSending(false);
}) })
.catch(() => { .catch((err) => {
toast.error(`Fehler beim Senden des Reports: ${err}`);
setSending(false); 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 { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import { useEffect, useState } from "react"; import { prisma } from "@repo/db";
import { Eye } from "lucide-react"; import { Error } from "_components/Error";
import { fetchReportDetails, handleMarkAsResolved } from "../actions"; import {
ReportAdmin,
ReportSenderInfo,
} from "(app)/admin/report/_components/form";
export default function ReportDetailsPage({ export default async function ReportDetailsPage({
params: paramsPromise, params,
}: { }: {
params: Promise<{ id: number }>; params: { id: string };
}) { }) {
const [params, setParams] = useState<{ id: number } | null>(null); const { id } = await params;
const [report, setReport] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => { const report = await prisma.report.findUnique({
async function unwrapParams() { where: {
const resolvedParams = await paramsPromise; id: Number(id),
setParams(resolvedParams); },
} include: {
unwrapParams(); Reported: true,
}, [paramsPromise]); Reviewer: true,
Sender: true,
},
});
useEffect(() => { if (!report) return <Error statusCode={404} title="User not found" />;
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>;
return ( return (
<div className="p-4"> <div className="grid grid-cols-6 gap-4">
<h1 className="text-2xl font-bold mb-4 flex items-center gap-2"> <div className="col-span-full">
<Eye className="w-6 h-6" /> Report Details <p className="text-2xl font-semibold text-left flex items-center gap-2">
</h1> <ExclamationTriangleIcon className="w-5 h-5" />
<div className="grid grid-cols-2 gap-4"> Report #{report.id}
<div> </p>
<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> </div>
<div className="mt-4 flex gap-2"> <div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<button <ReportSenderInfo report={report} />
className="btn btn-success btn-outline" </div>
onClick={async () => { <div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
await handleMarkAsResolved(report.id); <ReportAdmin report={report} />
}}
>
Erledigen
</button>
<button
className="btn btn-primary btn-outline"
onClick={() => window.history.back()}
>
Zurück
</button>
</div> </div>
</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"; "use server";
import { prisma, User } from "@repo/db"; import { Prisma, prisma } from "@repo/db";
export const markAsResolved = async (id: number) => { export const editReport = async (
await prisma.report.update({ id: number,
where: { id: id }, data: Prisma.ReportUncheckedUpdateInput,
data: { erledigt: true }, ) => {
}); return await prisma.report.update({
};
// 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({
where: { where: {
reportedUserId: user.id, id: id,
},
include: {
sender: true,
reported: true,
}, },
}); data,
};
export const fetchReportDetails = async (id: number) => {
return prisma.report.findUnique({
where: { id },
include: { sender: true, reported: true },
}); });
}; };

View File

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

View File

@@ -1,47 +1,49 @@
import { DatabaseBackupIcon } from 'lucide-react'; import { DatabaseBackupIcon } from "lucide-react";
import { PaginatedTable } from '../../../_components/PaginatedTable'; import { PaginatedTable } from "../../../_components/PaginatedTable";
import Link from 'next/link'; import Link from "next/link";
export default () => { const page = () => {
return ( return (
<> <>
<PaginatedTable <PaginatedTable
showEditButton showEditButton
prismaModel="station" prismaModel="station"
searchFields={['bosCallsign', 'bosUse', 'country', 'operator']} searchFields={["bosCallsign", "bosUse", "country", "operator"]}
columns={[ columns={[
{ {
header: 'BOS Name', header: "BOS Name",
accessorKey: 'bosCallsign', accessorKey: "bosCallsign",
}, },
{ {
header: 'Bos Use', header: "Bos Use",
accessorKey: 'bosUse', accessorKey: "bosUse",
}, },
{ {
header: 'Country', header: "Country",
accessorKey: 'country', accessorKey: "country",
}, },
{ {
header: 'operator', header: "operator",
accessorKey: 'operator', accessorKey: "operator",
}, },
]} ]}
leftOfSearch={ leftOfSearch={
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<DatabaseBackupIcon className="w-5 h-5" /> Stationen <DatabaseBackupIcon className="w-5 h-5" /> Stationen
</span> </span>
} }
rightOfSearch={ rightOfSearch={
<p className="text-2xl font-semibold text-left flex items-center gap-2 justify-between"> <p className="text-2xl font-semibold text-left flex items-center gap-2 justify-between">
<Link href={'/admin/station/new'}> <Link href={"/admin/station/new"}>
<button className="btn btn-sm btn-outline btn-primary"> <button className="btn btn-sm btn-outline btn-primary">
Erstellen Erstellen
</button> </button>
</Link> </Link>
</p> </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 { cn } from "../../../../../../helper/cn";
import { ChartBarBigIcon, PlaneIcon } from "lucide-react"; import { ChartBarBigIcon, PlaneIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import UserReports from "./UserReports";
interface ProfileFormProps { interface ProfileFormProps {
user: User; user: User;
@@ -420,7 +419,7 @@ export const AdminForm = ({ user, dispoTime, pilotTime }: AdminFormProps) => {
<h2 className="card-title"> <h2 className="card-title">
<ExclamationTriangleIcon className="w-5 h-5" /> Reports <ExclamationTriangleIcon className="w-5 h-5" /> Reports
</h2> </h2>
<UserReports user={user} /> {/* TODO: Report summary Here */}
</div> </div>
); );
}; };

View File

@@ -1,10 +1,9 @@
import { PersonIcon } from "@radix-ui/react-icons"; 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 { AdminForm, ConnectionHistory, ProfileForm } from "./_components/forms";
import { Error } from "../../../../_components/Error"; import { Error } from "../../../../_components/Error";
const Page = async ({ params }: { params: { id: string } }) => { const Page = async ({ params }: { params: { id: string } }) => {
const prisma = new PrismaClient();
const { id } = await params; const { id } = await params;
const user: User | null = await prisma.user.findUnique({ 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) # [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", "signedByOrg": "grafana",
"signedByOrgName": "Grafana Labs", "signedByOrgName": "Grafana Labs",
"plugin": "grafana-pyroscope-app", "plugin": "grafana-pyroscope-app",
"version": "1.3.0", "version": "1.4.0",
"time": 1744790362190, "time": 1747304216077,
"keyId": "7e4d0c6a708866e7", "keyId": "7e4d0c6a708866e7",
"files": { "files": {
"module.js.LICENSE.txt": "84798babe5a84ee41efdf41174af68e377c212b027183ecdd830747793156ded", "plugin.json": "249922ca0b392205fcbc173e77e01e0d74b7ff5df75dd9060d4b7a2a51fadfdf",
"715.js": "c87a98e6efaacbfec42c46f2fab14e9fda694fda48b209c7d44dafef92c2005c",
"350.js": "753480a1eb3b8f9846b91299e3b5edf9e8b4853b0fa7c83895b8ac52590209f7",
"LICENSE": "8486a10c4393cee1c25392769ddd3b2d6c242d6ec7928e1414efff7dfb2f07ef", "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/bafee50693eb02088442.png": "66d5311c4ca898cdae2d0a23a414f04a7c49052f0035c1a2906b9e9bb15d628d",
"img/9c9cdd5175734d579007.png": "ab65c374d22c5faad274f6b8b2ab00bf404bb988803f09d375326cd692fce821", "img/8cdf4d2e2df8326311ab.gif": "72afdd2fcad33e13db33af765a3fae9528315d78391684190dd23e40bd688852",
"img/58f0b0e1cfa063e4b662.png": "87598baf93192a8dc7ee923e0af6a0c5e4b3359b00b7391fc9530108feb7aac0",
"img/diff-view-how-to.gif": "72afdd2fcad33e13db33af765a3fae9528315d78391684190dd23e40bd688852",
"img/decrease-latency.png": "f626933745990d2fef3d90e995f38d5e28e4754c002cf031379db7588c8fd70c",
"img/reduce-costs.png": "ab65c374d22c5faad274f6b8b2ab00bf404bb988803f09d375326cd692fce821",
"img/61b4cf746a6f58780f27.png": "f626933745990d2fef3d90e995f38d5e28e4754c002cf031379db7588c8fd70c", "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/resolve-incidents.png": "66d5311c4ca898cdae2d0a23a414f04a7c49052f0035c1a2906b9e9bb15d628d",
"img/logo.svg": "559341996765c2d5639a2818d76bcd88ffe252212193a573f2f9f77dae5064dd", "img/logo.svg": "559341996765c2d5639a2818d76bcd88ffe252212193a573f2f9f77dae5064dd",
"img/hero-image.png": "87598baf93192a8dc7ee923e0af6a0c5e4b3359b00b7391fc9530108feb7aac0", "img/58f0b0e1cfa063e4b662.png": "87598baf93192a8dc7ee923e0af6a0c5e4b3359b00b7391fc9530108feb7aac0",
"img/8cdf4d2e2df8326311ab.gif": "72afdd2fcad33e13db33af765a3fae9528315d78391684190dd23e40bd688852", "README.md": "9808cbc6131d2512117f366e51f5ede7aeea5b73a852f0763ba2d05ce7334dd0"
"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"
} }
} }
-----BEGIN PGP SIGNATURE----- -----BEGIN PGP SIGNATURE-----
Version: OpenPGP.js v4.10.11 Version: OpenPGP.js v4.10.11
Comment: https://openpgpjs.org Comment: https://openpgpjs.org
wrkEARMKAAYFAmf/Y1oAIQkQfk0ManCIZucWIQTzOyW2kQdOhGNlcPN+TQxq wrkEARMKAAYFAmglvxgAIQkQfk0ManCIZucWIQTzOyW2kQdOhGNlcPN+TQxq
cIhm5+hKAgkA2MEgGa5HxlYzxQ9tJdQy2vUJjZYH630QBRfx11WSCh8l6Ths cIhm5/lnAgkAv2sUDLW60ervJ8gaooLBBhK4I0BZMGpoUIGSYrBQucff9I6o
qk6f2HzwsZSWh4kDkzfwFqSvMF3l33FUHGpH5z4CCQGwhcUQScay0D+FIjtN ZJ/mEaJRvckOhjCygiC7jOsnFtMYRFn6DOX4g7MCCQEpLjlyxr+mPA3B+BDE
BaAv7DIPpcG5fNYcaxdEGj8mt4UfNcE5zvs3yJ5bf88arZafNskJq/HpxDbn NMaiXpsfmFVX+LDjYbG0aDltVZ8ADyQjCPlmyLRvCht6vNFvUAutMKceqoLG
N5W9l+XEAg== qKCZ/5U08w==
=DXtv =fwcs
-----END PGP SIGNATURE----- -----END PGP SIGNATURE-----

View File

@@ -33,26 +33,26 @@ For instructions installing, refer to the [access and installation instructions]
## Resources ## Resources
- [Documentation](https://grafana.com/docs/grafana-cloud/visualizations/simplified-exploration/profiles/) - [Documentation](https://grafana.com/docs/grafana-cloud/visualizations/simplified-exploration/profiles/)
- [CHANGELOG](https://github.com/grafana/explore-profiles/releases) - [CHANGELOG](https://github.com/grafana/profiles-drilldown/releases)
- [GITHUB](https://github.com/grafana/explore-profiles/) - [GITHUB](https://github.com/grafana/profiles-drilldown/)
## Contributing ## Contributing
We love accepting contributions! We love accepting contributions!
If your change is minor, please feel free submit 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 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 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 request immediately as well, although we generally lean towards discussing the
change and then reviewing the implementation separately. 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 ### 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 ### 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 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" "path": "img/hero-image.png"
} }
], ],
"version": "1.3.0", "version": "1.4.0",
"updated": "2025-04-16", "updated": "2025-05-15",
"links": [ "links": [
{ {
"name": "GitHub", "name": "GitHub",
"url": "https://github.com/grafana/explore-profiles" "url": "https://github.com/grafana/profiles-drilldown"
}, },
{ {
"name": "Report bug", "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": [ "extensionPoints": [
{ {
"id": "grafana-pyroscope-app/investigation/v1" "id": "grafana-pyroscope-app/investigation/v1"
},
{
"id": "grafana-pyroscope-app/settings/v1"
} }
], ],
"addedLinks": [ "addedLinks": [

View File

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