room detail page fix

This commit is contained in:
Igor Loskutov
2025-09-02 18:52:31 -04:00
parent 05be6e7f19
commit 1d5a22ad1d
5 changed files with 240 additions and 77 deletions

View File

@@ -2,7 +2,7 @@
if [ "${ENTRYPOINT}" = "server" ]; then
uv run alembic upgrade head
uv run -m reflector.app
uv run uvicorn reflector.app:app --host 0.0.0.0 --port 1250
elif [ "${ENTRYPOINT}" = "worker" ]; then
uv run celery -A reflector.worker.app worker --loglevel=info
elif [ "${ENTRYPOINT}" = "beat" ]; then

View File

@@ -15,11 +15,9 @@ import {
createListCollection,
useDisclosure,
} from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { LuEye, LuEyeOff } from "react-icons/lu";
import useApi from "../../lib/useApi";
import useRoomList from "./useRoomList";
import { ApiError, RoomDetails } from "../../api";
import type { components } from "../../reflector-api";
import {
useRoomCreate,
@@ -27,9 +25,12 @@ import {
useRoomDelete,
useZulipStreams,
useZulipTopics,
useRoomGet,
useRoomTestWebhook,
} from "../../lib/apiHooks";
import { RoomList } from "./_components/RoomList";
import { PaginationPage } from "../browse/_components/Pagination";
import { assertExists } from "../../lib/utils";
type Room = components["schemas"]["Room"];
@@ -86,12 +87,10 @@ export default function RoomsList() {
const recordingTypeCollection = createListCollection({
items: recordingTypeOptions,
});
const [room, setRoom] = useState(roomInitialState);
const [room_, setRoom] = useState(roomInitialState);
const [isEditing, setIsEditing] = useState(false);
const [editRoomId, setEditRoomId] = useState("");
// TODO seems to be no setPage calls
const [page, setPage] = useState<number>(1);
const { loading, response, refetch } = useRoomList(PaginationPage(page));
const [editRoomId, setEditRoomId] = useState<string | null>(null);
const { loading, response, refetch } = useRoomList(PaginationPage(1));
const [nameError, setNameError] = useState("");
const [linkCopied, setLinkCopied] = useState("");
const [selectedStreamId, setSelectedStreamId] = useState<number | null>(null);
@@ -100,14 +99,6 @@ export default function RoomsList() {
null,
);
const [showWebhookSecret, setShowWebhookSecret] = useState(false);
interface Stream {
stream_id: number;
name: string;
}
interface Topic {
name: string;
}
const createRoomMutation = useRoomCreate();
const updateRoomMutation = useRoomUpdate();
@@ -115,6 +106,38 @@ export default function RoomsList() {
const { data: streams = [] } = useZulipStreams();
const { data: topics = [] } = useZulipTopics(selectedStreamId);
const {
data: detailedEditedRoom,
isLoading: isDetailedEditedRoomLoading,
error: detailedEditedRoomError,
} = useRoomGet(editRoomId);
// room being edited, as fetched from the server
const editedRoom: typeof roomInitialState | null = useMemo(
() =>
detailedEditedRoom
? {
name: detailedEditedRoom.name,
zulipAutoPost: detailedEditedRoom.zulip_auto_post,
zulipStream: detailedEditedRoom.zulip_stream,
zulipTopic: detailedEditedRoom.zulip_topic,
isLocked: detailedEditedRoom.is_locked,
roomMode: detailedEditedRoom.room_mode,
recordingType: detailedEditedRoom.recording_type,
recordingTrigger: detailedEditedRoom.recording_trigger,
isShared: detailedEditedRoom.is_shared,
webhookUrl: detailedEditedRoom.webhook_url || "",
webhookSecret: detailedEditedRoom.webhook_secret || "",
}
: null,
[detailedEditedRoom],
);
// here for minimal change in unrelated PR to make it work "backward-compatible" way. TODO make sense of it
const room = editedRoom || room_;
const roomTestWebhookMutation = useRoomTestWebhook();
// Update selected stream ID when zulip stream changes
useEffect(() => {
if (room.zulipStream && streams.length > 0) {
@@ -161,31 +184,37 @@ export default function RoomsList() {
};
const handleTestWebhook = async () => {
if (!room.webhookUrl || !editRoomId) {
if (!room.webhookUrl) {
setWebhookTestResult("Please enter a webhook URL first");
return;
}
if (!editRoomId) {
console.error("No room ID to test webhook");
return;
}
setTestingWebhook(true);
setWebhookTestResult(null);
try {
const response = await api?.v1RoomsTestWebhook({
roomId: editRoomId,
const response = await roomTestWebhookMutation.mutateAsync({
params: {
path: {
room_id: editRoomId,
},
},
});
if (response?.success) {
if (response.success) {
setWebhookTestResult(
`✅ Webhook test successful! Status: ${response.status_code}`,
);
} else {
let errorMsg = `❌ Webhook test failed`;
if (response?.status_code) {
errorMsg += ` (Status: ${response.status_code})`;
}
if (response?.error) {
errorMsg += ` (Status: ${response.status_code})`;
if (response.error) {
errorMsg += `: ${response.error}`;
} else if (response?.response_preview) {
} else if (response.response_preview) {
// Try to parse and extract meaningful error from response
// Specific to N8N at the moment, as there is no specification for that
// We could just display as is, but decided here to dig a little bit more.
@@ -241,7 +270,7 @@ export default function RoomsList() {
if (isEditing) {
await updateRoomMutation.mutateAsync({
params: {
path: { room_id: editRoomId },
path: { room_id: assertExists(editRoomId) },
},
body: roomData,
});
@@ -272,46 +301,11 @@ export default function RoomsList() {
}
};
const handleEditRoom = async (roomId, roomData) => {
const handleEditRoom = async (roomId: string, roomData) => {
// Reset states
setShowWebhookSecret(false);
setWebhookTestResult(null);
// Fetch full room details to get webhook fields
try {
const detailedRoom = await api?.v1RoomsGet({ roomId });
if (detailedRoom) {
setRoom({
name: detailedRoom.name,
zulipAutoPost: detailedRoom.zulip_auto_post,
zulipStream: detailedRoom.zulip_stream,
zulipTopic: detailedRoom.zulip_topic,
isLocked: detailedRoom.is_locked,
roomMode: detailedRoom.room_mode,
recordingType: detailedRoom.recording_type,
recordingTrigger: detailedRoom.recording_trigger,
isShared: detailedRoom.is_shared,
webhookUrl: detailedRoom.webhook_url || "",
webhookSecret: detailedRoom.webhook_secret || "",
});
}
} catch (error) {
console.error("Failed to fetch room details, using list data:", error);
// Fallback to using the data from the list
setRoom({
name: roomData.name,
zulipAutoPost: roomData.zulip_auto_post,
zulipStream: roomData.zulip_stream,
zulipTopic: roomData.zulip_topic,
isLocked: roomData.is_locked,
roomMode: roomData.room_mode,
recordingType: roomData.recording_type,
recordingTrigger: roomData.recording_trigger,
isShared: roomData.is_shared,
webhookUrl: roomData.webhook_url || "",
webhookSecret: roomData.webhook_secret || "",
});
}
setEditRoomId(roomId);
setIsEditing(true);
setNameError("");
@@ -346,9 +340,9 @@ export default function RoomsList() {
});
};
const myRooms: RoomDetails[] =
const myRooms: Room[] =
response?.items.filter((roomData) => !roomData.is_shared) || [];
const sharedRooms: RoomDetails[] =
const sharedRooms: Room[] =
response?.items.filter((roomData) => roomData.is_shared) || [];
if (loading && !response)

View File

@@ -1,7 +1,7 @@
import { useRoomsList } from "../../lib/apiHooks";
import type { components } from "../../reflector-api";
type Page_Room_ = components["schemas"]["Page_Room_"];
type Page_Room_ = components["schemas"]["Page_RoomDetails_"];
import { PaginationPage } from "../browse/_components/Pagination";
type RoomList = {

View File

@@ -120,6 +120,34 @@ export function useTranscriptGet(transcriptId: string | null) {
);
}
export function useRoomGet(roomId: string | null) {
const { isAuthenticated } = useAuthReady();
return $api.useQuery(
"get",
"/v1/rooms/{room_id}",
{
params: {
path: { room_id: roomId || "" },
},
},
{
enabled: !!roomId && isAuthenticated,
staleTime: STALE_TIME,
},
);
}
export function useRoomTestWebhook() {
const { setError } = useError();
return $api.useMutation("post", "/v1/rooms/{room_id}/webhook/test", {
onError: (error) => {
setError(error as Error, "There was an error testing the webhook");
},
});
}
export function useRoomCreate() {
const { setError } = useError();
const queryClient = useQueryClient();

View File

@@ -66,7 +66,8 @@ export interface paths {
path?: never;
cookie?: never;
};
get?: never;
/** Rooms Get */
get: operations["v1_rooms_get"];
put?: never;
post?: never;
/** Rooms Delete */
@@ -94,6 +95,26 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/rooms/{room_id}/webhook/test": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Rooms Test Webhook
* @description Test webhook configuration by sending a sample payload.
*/
post: operations["v1_rooms_test_webhook"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/transcripts": {
parameters: {
query?: never;
@@ -511,6 +532,10 @@ export interface components {
recording_trigger: string;
/** Is Shared */
is_shared: boolean;
/** Webhook Url */
webhook_url: string;
/** Webhook Secret */
webhook_secret: string;
};
/** CreateTranscript */
CreateTranscript: {
@@ -749,10 +774,10 @@ export interface components {
/** Pages */
pages?: number | null;
};
/** Page[Room] */
Page_Room_: {
/** Page[RoomDetails] */
Page_RoomDetails_: {
/** Items */
items: components["schemas"]["Room"][];
items: components["schemas"]["RoomDetails"][];
/** Total */
total?: number | null;
/** Page */
@@ -801,6 +826,40 @@ export interface components {
/** Is Shared */
is_shared: boolean;
};
/** RoomDetails */
RoomDetails: {
/** Id */
id: string;
/** Name */
name: string;
/** User Id */
user_id: string;
/**
* Created At
* Format: date-time
*/
created_at: string;
/** Zulip Auto Post */
zulip_auto_post: boolean;
/** Zulip Stream */
zulip_stream: string;
/** Zulip Topic */
zulip_topic: string;
/** Is Locked */
is_locked: boolean;
/** Room Mode */
room_mode: string;
/** Recording Type */
recording_type: string;
/** Recording Trigger */
recording_trigger: string;
/** Is Shared */
is_shared: boolean;
/** Webhook Url */
webhook_url: string | null;
/** Webhook Secret */
webhook_secret: string | null;
};
/** RtcOffer */
RtcOffer: {
/** Sdp */
@@ -817,11 +876,8 @@ export interface components {
* @description Total number of search results
*/
total: number;
/**
* Query
* @description Search query text
*/
query: string;
/** Query */
query?: string | null;
/**
* Limit
* @description Results per page
@@ -955,6 +1011,10 @@ export interface components {
recording_trigger: string;
/** Is Shared */
is_shared: boolean;
/** Webhook Url */
webhook_url: string;
/** Webhook Secret */
webhook_secret: string;
};
/** UpdateTranscript */
UpdateTranscript: {
@@ -995,6 +1055,25 @@ export interface components {
/** Error Type */
type: string;
};
/** WebhookTestResult */
WebhookTestResult: {
/** Success */
success: boolean;
/**
* Message
* @default
*/
message: string;
/**
* Error
* @default
*/
error: string;
/** Status Code */
status_code?: number | null;
/** Response Preview */
response_preview?: string | null;
};
/** WherebyWebhookEvent */
WherebyWebhookEvent: {
/** Apiversion */
@@ -1117,7 +1196,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Page_Room_"];
"application/json": components["schemas"]["Page_RoomDetails_"];
};
};
/** @description Validation Error */
@@ -1164,6 +1243,37 @@ export interface operations {
};
};
};
v1_rooms_get: {
parameters: {
query?: never;
header?: never;
path: {
room_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["RoomDetails"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
v1_rooms_delete: {
parameters: {
query?: never;
@@ -1216,7 +1326,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Room"];
"application/json": components["schemas"]["RoomDetails"];
};
};
/** @description Validation Error */
@@ -1261,6 +1371,37 @@ export interface operations {
};
};
};
v1_rooms_test_webhook: {
parameters: {
query?: never;
header?: never;
path: {
room_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["WebhookTestResult"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
v1_transcripts_list: {
parameters: {
query?: {