Skip to content

Commit ba383ad

Browse files
Merge pull request #55 from CS3219-AY2425S1/solomon/code-execute
solomon/history-service
2 parents bb2b215 + 3502e42 commit ba383ad

File tree

26 files changed

+1206
-27
lines changed

26 files changed

+1206
-27
lines changed

.github/workflows/test.yml

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@ jobs:
2929
QUESTION_SERVICE_URL: ${{ vars.QUESTION_SERVICE_URL }}
3030
USER_SERVICE_URL: ${{ vars.USER_SERVICE_URL }}
3131
MATCHING_SERVICE_URL: ${{ vars.MATCHING_SERVICE_URL }}
32+
HISTORY_SERVICE_URL: ${{ vars.HISTORY_SERVICE_URL }}
3233
JWT_SECRET: ${{ secrets.JWT_SECRET }}
33-
FIREBASE_CREDENTIAL_PATH: ${{ vars.QUESTION_SERVICE_FIREBASE_CREDENTIAL_PATH }}
34+
QUESTION_FIREBASE_CREDENTIAL_PATH: ${{ vars.QUESTION_SERVICE_FIREBASE_CREDENTIAL_PATH }}
35+
HISTORY_FIREBASE_CREDENTIAL_PATH: ${{ vars.HISTORY_SERVICE_FIREBASE_CREDENTIAL_PATH }}
3436
DB_CLOUD_URI: ${{ secrets.USER_SERVICE_DB_CLOUD_URI }}
3537
USER_SERVICE_PORT: ${{ vars.USER_SERVICE_PORT }}
3638
MATCHING_SERVICE_PORT: ${{ vars.MATCHING_SERVICE_PORT }}
39+
HISTORY_SERVICE_PORT: ${{ vars.HISTORY_SERVICE_PORT }}
3740
MATCHING_SERVICE_TIMEOUT: ${{ vars.MATCHING_SERVICE_TIMEOUT }}
3841
REDIS_URL: ${{ vars.REDIS_URL }}
3942
QUESTION_SERVICE_GRPC_URL: ${{ vars.QUESTION_SERVICE_GPRC_URL }}
@@ -42,9 +45,10 @@ jobs:
4245
echo "NEXT_PUBLIC_QUESTION_SERVICE_URL=$QUESTION_SERVICE_URL" >> .env
4346
echo "NEXT_PUBLIC_USER_SERVICE_URL=$USER_SERVICE_URL" >> .env
4447
echo "NEXT_PUBLIC_MATCHING_SERVICE_URL=$MATCHING_SERVICE_URL" >> .env
48+
echo "NEXT_PUBLIC_HISTORY_SERVICE_URL=$HISTORY_SERVICE_URL" >> .env
4549
4650
cd ../question-service
47-
echo "FIREBASE_CREDENTIAL_PATH=$FIREBASE_CREDENTIAL_PATH" >> .env
51+
echo "FIREBASE_CREDENTIAL_PATH=$QUESTION_FIREBASE_CREDENTIAL_PATH" >> .env
4852
echo "JWT_SECRET=$JWT_SECRET" >> .env
4953
5054
cd ../user-service
@@ -59,13 +63,22 @@ jobs:
5963
echo "REDIS_URL=$REDIS_URL" >> .env
6064
echo "QUESTION_SERVICE_GRPC_URL=$QUESTION_SERVICE_GRPC_URL" >> .env
6165
66+
cd ../history-service
67+
echo "FIREBASE_CREDENTIAL_PATH=$HISTORY_FIREBASE_CREDENTIAL_PATH" >> .env
68+
echo "PORT=$HISTORY_SERVICE_PORT" >> .env
69+
6270
- name: Create Database Credential Files
6371
env:
64-
FIREBASE_JSON: ${{ secrets.QUESTION_SERVICE_FIREBASE_CREDENTIAL }}
65-
FIREBASE_CREDENTIAL_PATH: ${{ vars.QUESTION_SERVICE_FIREBASE_CREDENTIAL_PATH }}
72+
QUESTION_FIREBASE_JSON: ${{ secrets.QUESTION_SERVICE_FIREBASE_CREDENTIAL }}
73+
QUESTION_FIREBASE_CREDENTIAL_PATH: ${{ vars.QUESTION_SERVICE_FIREBASE_CREDENTIAL_PATH }}
74+
HISTORY_FIREBASE_JSON: ${{ secrets.HISTORY_SERVICE_FIREBASE_CREDENTIAL }}
75+
HISTORY_FIREBASE_CREDENTIAL_PATH: ${{ vars.HISTORY_SERVICE_FIREBASE_CREDENTIAL_PATH }}
6676
run: |
6777
cd ./apps/question-service
68-
echo "$FIREBASE_JSON" > "./$FIREBASE_CREDENTIAL_PATH"
78+
echo "$QUESTION_FIREBASE_JSON" > "./$QUESTION_FIREBASE_CREDENTIAL_PATH"
79+
80+
cd ../history-service
81+
echo "$HISTORY_FIREBASE_JSON" > "./$HISTORY_FIREBASE_CREDENTIAL_PATH"
6982
7083
- name: Build and Run Services
7184
run: |
@@ -87,13 +100,16 @@ jobs:
87100
USER_SERVICE_URL: ${{ vars.USER_SERVICE_URL }}
88101
QUESTION_SERVICE_URL: ${{ vars.QUESTION_SERVICE_URL }}
89102
MATCHING_SERVICE_URL: ${{ vars.MATCHING_SERVICE_URL }}
103+
HISTORY_SERVICE_URL: ${{ vars.HISTORY_SERVICE_URL }}
90104
run: |
91105
echo "Testing Question Service..."
92106
curl -sSL -o /dev/null $QUESTION_SERVICE_URL && echo "Question Service is up"
93107
echo "Testing User Service..."
94108
curl -fsSL -o /dev/null $USER_SERVICE_URL && echo "User Service is up"
95109
echo "Testing Frontend..."
96110
curl -fsSL -o /dev/null $FRONTEND_URL && echo "Frontend is up"
111+
echo "Testing History Service..."
112+
curl -fsSL -o /dev/null $HISTORY_SERVICE_URL && echo "History Service is up"
97113
echo "Testing Matching Service..."
98114
if ! (echo "Hello" | websocat $MATCHING_SERVICE_URL); then
99115
echo "WebSocket for Matching Service is not live"

apps/docker-compose.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ services:
5454
depends_on:
5555
- redis
5656

57+
history-service:
58+
build:
59+
context: ./history-service
60+
dockerfile: Dockerfile
61+
ports:
62+
- 8082:8082
63+
env_file:
64+
- ./history-service/.env
65+
networks:
66+
- apps_network
67+
volumes:
68+
- ./history-service:/history-service
69+
5770
redis:
5871
image: redis:latest
5972
networks:

apps/frontend/.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
NEXT_PUBLIC_QUESTION_SERVICE_URL="http://localhost:8080/"
33
NEXT_PUBLIC_USER_SERVICE_URL="http://localhost:3001/"
44
NEXT_PUBLIC_MATCHING_SERVICE_URL="ws://localhost:8081/match"
5-
NEXT_PUBLIC_SIGNALLING_SERVICE_URL="ws://localhost:4444/"
5+
NEXT_PUBLIC_SIGNALLING_SERVICE_URL="ws://localhost:4444/"
6+
NEXT_PUBLIC_HISTORY_SERVICE_URL="http://localhost:8082/"

apps/frontend/src/app/collaboration/[id]/page.tsx

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
Col,
66
Input,
77
Layout,
8+
message,
89
Row,
910
Select,
1011
Tabs,
@@ -15,7 +16,7 @@ import {
1516
import { Content } from "antd/es/layout/layout";
1617
import "./styles.scss";
1718
import { useRouter, useSearchParams } from "next/navigation";
18-
import { useEffect, useState } from "react";
19+
import { useEffect, useRef, useState } from "react";
1920
import { GetSingleQuestion, Question } from "@/app/services/question";
2021
import {
2122
ClockCircleOutlined,
@@ -27,29 +28,39 @@ import {
2728
} from "@ant-design/icons";
2829
import { ProgrammingLanguageOptions } from "@/utils/SelectOptions";
2930
import CollaborativeEditor from "@/components/CollaborativeEditor/CollaborativeEditor";
31+
import { CreateOrUpdateHistory } from "@/app/services/history";
32+
import { Language } from "@codemirror/language";
33+
import { WebrtcProvider } from "y-webrtc";
3034

3135
interface CollaborationProps {}
3236

3337
export default function CollaborationPage(props: CollaborationProps) {
3438
const router = useRouter();
39+
const providerRef = useRef<WebrtcProvider | null>(null);
3540

3641
const [isLoading, setIsLoading] = useState<boolean>(false);
3742

3843
// Code Editor States
44+
const [historyDocRefId, setHistoryDocRefId] = useState<string | undefined>(
45+
undefined
46+
);
47+
const [code, setCode] = useState<string>("");
3948
const [questionTitle, setQuestionTitle] = useState<string | undefined>(
4049
undefined
4150
);
51+
const [questionDocRefId, setQuestionDocRefId] = useState<string | undefined>(undefined);
4252
const [complexity, setComplexity] = useState<string | undefined>(undefined);
4353
const [categories, setCategories] = useState<string[]>([]); // Store the selected filter categories
4454
const [description, setDescription] = useState<string | undefined>(undefined);
45-
const [selectedLanguage, setSelectedLanguage] = useState("javascript"); // State to hold the selected language item
55+
const [selectedLanguage, setSelectedLanguage] = useState("Javascript"); // State to hold the selected language item
4656

4757
// Session states
4858
const [collaborationId, setCollaborationId] = useState<string | undefined>(
4959
undefined
5060
);
5161
const [currentUser, setCurrentUser] = useState<string | undefined>(undefined);
5262
const [matchedUser, setMatchedUser] = useState<string | undefined>(undefined);
63+
const [matchedTopics, setMatchedTopics] = useState<string[] | undefined>(undefined);
5364

5465
// Chat states
5566
const [messageToSend, setMessageToSend] = useState<string | undefined>(
@@ -61,6 +72,47 @@ export default function CollaborationPage(props: CollaborationProps) {
6172
undefined
6273
);
6374

75+
// Message
76+
const [messageApi, contextHolder] = message.useMessage();
77+
78+
const successMessage = (message: string) => {
79+
messageApi.open({
80+
type: "success",
81+
content: message,
82+
});
83+
};
84+
85+
const sendCodeSavedStatusToMatchedUser = () => {
86+
if (!providerRef.current) {
87+
throw new Error("Provider not initialized");
88+
}
89+
providerRef.current.awareness.setLocalStateField("codeSavedStatus", true);
90+
}
91+
92+
const handleSubmitCode = async () => {
93+
if (!collaborationId) {
94+
throw new Error("Collaboration ID not found");
95+
}
96+
const data = await CreateOrUpdateHistory({
97+
title: questionTitle ?? "",
98+
code: code,
99+
language: selectedLanguage,
100+
user: currentUser ?? "",
101+
matchedUser: matchedUser ?? "",
102+
matchId: collaborationId ?? "",
103+
matchedTopics: matchedTopics ?? [],
104+
questionDocRefId: questionDocRefId ?? "",
105+
questionDifficulty: complexity ?? "",
106+
questionTopics: categories,
107+
}, collaborationId);
108+
successMessage("Code saved successfully!");
109+
sendCodeSavedStatusToMatchedUser();
110+
}
111+
112+
const handleCodeChange = (code: string) => {
113+
setCode(code);
114+
}
115+
64116
// Retrieve the docRefId from query params during page navigation
65117
// const searchParams = useSearchParams();
66118

@@ -71,17 +123,20 @@ export default function CollaborationPage(props: CollaborationProps) {
71123
}
72124

73125
// Retrieve details from localstorage
74-
const docRefId: string = localStorage.getItem("docRefId") ?? "";
126+
const questionDocRefId: string = localStorage.getItem("questionDocRefId") ?? "";
75127
const collabId: string = localStorage.getItem("collabId") ?? "";
76128
const matchedUser: string = localStorage.getItem("matchedUser") ?? "";
77129
const currentUser: string = localStorage.getItem("user") ?? "";
130+
const matchedTopics: string[] = localStorage.getItem("matchedTopics")?.split(",") ?? [];
78131

79132
// Set states from localstorage
80133
setCollaborationId(collabId);
81134
setMatchedUser(matchedUser);
82135
setCurrentUser(currentUser);
136+
setMatchedTopics(matchedTopics);
137+
setQuestionDocRefId(questionDocRefId);
83138

84-
GetSingleQuestion(docRefId).then((data: Question) => {
139+
GetSingleQuestion(questionDocRefId).then((data: Question) => {
85140
setQuestionTitle(`${data.id}. ${data.title}`);
86141
setComplexity(data.complexity);
87142
setCategories(data.categories);
@@ -121,15 +176,17 @@ export default function CollaborationPage(props: CollaborationProps) {
121176
// Remove localstorage variables for collaboration
122177
localStorage.removeItem("user");
123178
localStorage.removeItem("matchedUser");
124-
localStorage.removeItem("collaId");
125-
localStorage.removeItem("docRefId");
179+
localStorage.removeItem("collabId");
180+
localStorage.removeItem("questionDocRefId");
181+
localStorage.removeItem("matchedTopics");
126182

127183
// Redirect back to matching page
128184
router.push("/matching");
129185
};
130186

131187
return (
132188
<Layout className="collaboration-layout">
189+
{contextHolder}
133190
<Header selectedKey={undefined} />
134191
<Content className="collaboration-content">
135192
<Row gutter={0} className="collab-row">
@@ -189,7 +246,11 @@ export default function CollaborationPage(props: CollaborationProps) {
189246
Code
190247
</div>
191248
{/* TODO: Link to execution service for code submission */}
192-
<Button icon={<SendOutlined />} iconPosition="end">
249+
<Button
250+
icon={<SendOutlined />}
251+
iconPosition="end"
252+
onClick={() => handleSubmitCode()}
253+
>
193254
Submit
194255
</Button>
195256
</div>
@@ -207,6 +268,9 @@ export default function CollaborationPage(props: CollaborationProps) {
207268
user={currentUser}
208269
collaborationId={collaborationId}
209270
language={selectedLanguage}
271+
providerRef={providerRef}
272+
matchedUser={matchedUser}
273+
onCodeChange={handleCodeChange}
210274
/>
211275
)}
212276
</div>

apps/frontend/src/app/matching/MatchingModal.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ const MatchingModal: React.FC<MatchingModalProps> = ({
5959
cancel={() => {
6060
setClosedType("cancelled");
6161
}}
62-
name1={matchingState.info?.myName || ""}
63-
name2={matchingState.info?.partnerName || ""}
62+
name1={matchingState.info?.user || ""}
63+
name2={matchingState.info?.matchedUser || ""}
6464
/>
6565
);
6666
}
@@ -97,19 +97,20 @@ const MatchingModal: React.FC<MatchingModalProps> = ({
9797
join={() => {
9898
matchingState.ok();
9999
setClosedType("joined");
100-
localStorage.setItem("user", matchingState.info.myName);
100+
localStorage.setItem("user", matchingState.info.user);
101101
localStorage.setItem(
102102
"matchedUser",
103-
matchingState.info.partnerName
103+
matchingState.info.matchedUser
104104
);
105105
localStorage.setItem("collabId", matchingState.info.matchId);
106-
localStorage.setItem("docRefId", matchingState.info.docRefId);
106+
localStorage.setItem("questionDocRefId", matchingState.info.questionDocRefId);
107+
localStorage.setItem("matchedTopics", matchingState.info.matchedTopics.join(","));
107108

108109
// Redirect to collaboration page
109110
router.push(`/collaboration/${matchingState.info.matchId}`);
110111
}}
111-
name1={matchingState.info.myName}
112-
name2={matchingState.info.partnerName}
112+
name1={matchingState.info.user}
113+
name2={matchingState.info.matchedUser}
113114
/>
114115
);
115116
case "timeout":
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
const HISTORY_SERVICE_URL = process.env.NEXT_PUBLIC_HISTORY_SERVICE_URL;
2+
3+
export interface History {
4+
title: string;
5+
code: string;
6+
language: string;
7+
user: string;
8+
matchedUser: string;
9+
matchId: string;
10+
matchedTopics: string[];
11+
questionDocRefId: string;
12+
questionDifficulty: string;
13+
questionTopics: string[];
14+
createdAt?: string;
15+
updatedAt?: string;
16+
}
17+
18+
export const CreateOrUpdateHistory = async (
19+
history: History,
20+
matchId: string,
21+
): Promise<History> => {
22+
const response = await fetch(
23+
`${HISTORY_SERVICE_URL}histories/${matchId}`,
24+
{
25+
method: "PUT",
26+
headers: {
27+
"Content-Type": "application/json",
28+
},
29+
body: JSON.stringify(history),
30+
}
31+
);
32+
33+
if (response.status === 200) {
34+
return response.json();
35+
} else {
36+
throw new Error(
37+
`Error saving history: ${response.status} ${response.statusText}`
38+
);
39+
}
40+
}
41+
42+
export const GetHistory = async (
43+
matchId: string,
44+
): Promise<History> => {
45+
const response = await fetch(
46+
`${HISTORY_SERVICE_URL}histories/${matchId}`,
47+
{
48+
method: "GET",
49+
headers: {
50+
"Content-Type": "application/json",
51+
},
52+
}
53+
);
54+
55+
if (response.status === 200) {
56+
return response.json();
57+
} else {
58+
throw new Error(
59+
`Error reading history: ${response.status} ${response.statusText}`
60+
);
61+
}
62+
}
63+
64+
export const GetUserHistories = async (
65+
username: string,
66+
): Promise<History[]> => {
67+
const response = await fetch(
68+
`${HISTORY_SERVICE_URL}histories/${username}`,
69+
{
70+
method: "GET",
71+
headers: {
72+
"Content-Type": "application/json",
73+
},
74+
}
75+
);
76+
77+
if (response.status === 200) {
78+
return response.json();
79+
} else {
80+
throw new Error(
81+
`Error reading user histories: ${response.status} ${response.statusText}`
82+
);
83+
}
84+
}

0 commit comments

Comments
 (0)