Skip to content

feat(tickets): add way to track changes #1102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 50 additions & 6 deletions api/src/api/tickets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ use crate::RegistryUrl;
use super::ApiError;
use super::ApiTicket;
use super::ApiTicketMessage;
use super::ApiTicketMessageOrAuditLog;
use super::ApiTicketOverview;

pub fn tickets_router() -> Router<Body, ApiError> {
Router::builder()
Expand All @@ -35,18 +37,60 @@ pub fn tickets_router() -> Router<Body, ApiError> {
}

#[instrument(name = "GET /api/tickets/:id", skip(req), err, fields(id))]
pub async fn get_handler(req: Request<Body>) -> ApiResult<ApiTicket> {
let id = req.param_uuid("id")?;
pub async fn get_handler(req: Request<Body>) -> ApiResult<ApiTicketOverview> {
let id = match req.param_uuid("id") {
Ok(id) => id,
Err(_) => {
return Err(ApiError::MalformedRequest {
msg: "Invalid ID".into(),
})
}
};
Span::current().record("id", field::display(id));

let db = req.data::<Database>().unwrap();
let ticket = db.get_ticket(id).await?.ok_or(ApiError::TicketNotFound)?;
let db = match req.data::<Database>() {
Some(db) => db,
None => return Err(ApiError::InternalServerError),
};

let iam = req.iam();
let ticket = match db.get_ticket(id).await {
Ok(Some(ticket)) => ticket,
Ok(None) => return Err(ApiError::TicketNotFound),
Err(_) => return Err(ApiError::InternalServerError),
};

let ticket_audit = db.get_ticket_audit_logs(id).await;

let iam = req.iam();
let current_user = iam.check_current_user_access()?;

if current_user == &ticket.1 || iam.check_admin_access().is_ok() {
Ok(ticket.into())
let mut events: Vec<ApiTicketMessageOrAuditLog> = Vec::new();

for message in ticket.2 {
events.push(ApiTicketMessageOrAuditLog::Message {
message: message.0,
user: message.1,
});
}

if let Ok(audit_logs) = ticket_audit {
for audit_log in audit_logs {
events.push(ApiTicketMessageOrAuditLog::AuditLog {
audit_log: audit_log.0,
user: audit_log.1,
});
}
}

events.sort_by_key(|event| match event {
ApiTicketMessageOrAuditLog::Message { message, .. } => message.created_at,
ApiTicketMessageOrAuditLog::AuditLog { audit_log, .. } => {
audit_log.created_at
}
});

Ok((ticket.0, ticket.1, events).into())
} else {
Err(ApiError::TicketNotFound)
}
Expand Down
46 changes: 46 additions & 0 deletions api/src/api/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,52 @@ pub struct ApiPackageDownloadsRecentVersion {
pub downloads: Vec<ApiDownloadDataPoint>,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum ApiTicketMessageOrAuditLog {
Message {
message: TicketMessage,
user: UserPublic,
},
#[serde(rename_all = "camelCase")]
AuditLog {
audit_log: AuditLog,
user: UserPublic,
},
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiTicketOverview {
pub id: Uuid,
pub kind: TicketKind,
pub creator: ApiUser,
pub meta: serde_json::Value,
pub closed: bool,
pub events: Vec<ApiTicketMessageOrAuditLog>,
pub updated_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
}

impl From<(Ticket, User, Vec<ApiTicketMessageOrAuditLog>)>
for ApiTicketOverview
{
fn from(
(value, user, events): (Ticket, User, Vec<ApiTicketMessageOrAuditLog>),
) -> Self {
Self {
id: value.id,
kind: value.kind,
creator: user.into(),
meta: value.meta,
closed: value.closed,
events,
updated_at: value.updated_at,
created_at: value.created_at,
}
}
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiTicket {
Expand Down
59 changes: 59 additions & 0 deletions api/src/db/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4453,6 +4453,65 @@ impl Database {
Ok(Some((ticket, user, messages)))
}

#[instrument(name = "Database::get_ticket_audit_logs", skip(self), err)]
pub async fn get_ticket_audit_logs(
&self,
ticket_id: Uuid,
) -> Result<Vec<(AuditLog, UserPublic)>> {
let mut tx = self.pool.begin().await?;

let audit_logs = sqlx::query!(
r#"
SELECT
audit_logs.actor_id as "audit_actor_id",
audit_logs.is_sudo as "audit_is_sudo",
audit_logs.action as "audit_action",
audit_logs.meta as "audit_meta",
audit_logs.created_at as "audit_created_at",
users.id as "user_id",
users.name as "user_name",
users.avatar_url as "user_avatar_url",
users.github_id as "user_github_id",
users.updated_at as "user_updated_at",
users.created_at as "user_created_at"
FROM
audit_logs
LEFT JOIN
users ON audit_logs.actor_id = users.id
WHERE
audit_logs.meta::text LIKE $1
ORDER BY audit_logs.created_at DESC;
"#,
format!("%\"ticket_id\": \"{}\"%", ticket_id),
)
.map(|r| {
let audit_log = AuditLog {
actor_id: r.audit_actor_id,
is_sudo: r.audit_is_sudo,
action: r.audit_action,
meta: r.audit_meta,
created_at: r.audit_created_at,
};

let user = UserPublic {
id: r.user_id,
name: r.user_name,
avatar_url: r.user_avatar_url,
github_id: r.user_github_id,
updated_at: r.user_updated_at,
created_at: r.user_created_at,
};

(audit_log, user)
})
.fetch_all(&mut *tx)
.await?;

tx.commit().await?;

Ok(audit_logs)
}

#[instrument(name = "Database::ticket_add_message", skip(self), err)]
pub async fn ticket_add_message(
&self,
Expand Down
9 changes: 6 additions & 3 deletions api/src/db/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ impl FromRow<'_, sqlx::postgres::PgRow> for User {
}
}

#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserPublic {
pub id: Uuid,
pub name: String,
Expand Down Expand Up @@ -975,7 +976,8 @@ pub struct NewTicketMessage {
pub message: String,
}

#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TicketMessage {
pub ticket_id: Uuid,
pub author: Uuid,
Expand All @@ -986,7 +988,8 @@ pub struct TicketMessage {

pub type FullTicket = (Ticket, User, Vec<(TicketMessage, UserPublic)>);

#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuditLog {
pub actor_id: Uuid,
pub is_sudo: bool,
Expand Down
15 changes: 9 additions & 6 deletions frontend/islands/TicketMessageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import {
AdminUpdateTicketRequest,
FullUser,
NewTicketMessage,
Ticket,
} from "../utils/api_types.ts";
import { api, path } from "../utils/api.ts";

export function TicketMessageInput(
{ ticket, user }: { ticket: Ticket; user: FullUser },
{ ticketId, closed, user }: {
ticketId: string;
closed: boolean;
user: FullUser;
},
) {
const [message, setMessage] = useState("");

Expand All @@ -21,7 +24,7 @@ export function TicketMessageInput(
e.preventDefault();

api.post(
path`/tickets/${ticket.id}`,
path`/tickets/${ticketId}`,
{
message,
} satisfies NewTicketMessage,
Expand Down Expand Up @@ -52,9 +55,9 @@ export function TicketMessageInput(
e.preventDefault();

api.patch(
path`/admin/tickets/${ticket.id}`,
path`/admin/tickets/${ticketId}`,
{
closed: !ticket.closed,
closed: !closed,
} satisfies AdminUpdateTicketRequest,
).then((resp) => {
if (resp.ok) {
Expand All @@ -66,7 +69,7 @@ export function TicketMessageInput(
});
}}
>
{ticket.closed
{closed
? (
<>
<TbClock class="text-white" /> Re-open
Expand Down
6 changes: 3 additions & 3 deletions frontend/islands/TicketModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright 2024 the JSR authors. All rights reserved. MIT license.
import { useEffect, useId, useRef, useState } from "preact/hooks";
import { NewTicket, Ticket, TicketKind, User } from "../utils/api_types.ts";
import { ApiTicket, NewTicket, TicketKind, User } from "../utils/api_types.ts";
import type { ComponentChildren } from "preact";
import { TbLoader2 } from "tb-icons";
import { api, path } from "../utils/api.ts";
Expand Down Expand Up @@ -31,7 +31,7 @@ export function TicketModal(
const [status, setStatus] = useState<"pending" | "submitting" | "submitted">(
"pending",
);
const [ticket, setTicket] = useState<Ticket | null>(null);
const [ticket, setTicket] = useState<ApiTicket | null>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const ref = useRef<HTMLFormElement>(null);

Expand Down Expand Up @@ -111,7 +111,7 @@ export function TicketModal(

setStatus("submitting");

api.post<Ticket>(path`/tickets`, data).then((res) => {
api.post<ApiTicket>(path`/tickets`, data).then((res) => {
if (res.ok) {
setStatus("submitted");
setTicket(res.data);
Expand Down
4 changes: 2 additions & 2 deletions frontend/routes/account/tickets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { HttpError } from "fresh";
import { AccountLayout } from "./(_components)/AccountLayout.tsx";
import { define } from "../../util.ts";
import { Table, TableData, TableRow } from "../../components/Table.tsx";
import { Ticket } from "../../utils/api_types.ts";
import { ApiTicket } from "../../utils/api_types.ts";
import { path } from "../../utils/api.ts";
import twas from "twas";
import { TbCheck, TbClock } from "tb-icons";
Expand Down Expand Up @@ -77,7 +77,7 @@ export const handler = define.handlers({
async GET(ctx) {
const [currentUser, ticketsRes] = await Promise.all([
ctx.state.userPromise,
ctx.state.api.get<Ticket[]>(path`/user/tickets`),
ctx.state.api.get<ApiTicket[]>(path`/user/tickets`),
]);
if (currentUser instanceof Response) return currentUser;
if (!currentUser) throw new HttpError(404, "No signed in user found.");
Expand Down
Loading
Loading