From ae63f56645070d517c5764eaba00a4da391dd011 Mon Sep 17 00:00:00 2001 From: swve Date: Wed, 12 Mar 2025 13:14:24 +0100 Subject: [PATCH 1/5] fix: remove sentry from backend --- apps/api/config/config.py | 40 +----------------------------- apps/api/pyproject.toml | 2 +- apps/api/src/core/events/events.py | 4 --- apps/api/src/core/events/sentry.py | 16 ------------ 4 files changed, 2 insertions(+), 60 deletions(-) delete mode 100644 apps/api/src/core/events/sentry.py diff --git a/apps/api/config/config.py b/apps/api/config/config.py index c9a7bd23..e3f7ff8c 100644 --- a/apps/api/config/config.py +++ b/apps/api/config/config.py @@ -5,12 +5,6 @@ from pydantic import BaseModel from dotenv import load_dotenv -class SentryConfig(BaseModel): - dsn: str - environment: str - release: str - - class CookieConfig(BaseModel): domain: str @@ -53,7 +47,6 @@ class HostingConfig(BaseModel): allowed_origins: list allowed_regexp: str self_hosted: bool - sentry_config: Optional[SentryConfig] cookie_config: CookieConfig content_delivery: ContentDeliveryConfig @@ -150,10 +143,7 @@ def get_learnhouse_config() -> LearnHouseConfig: env_self_hosted = os.environ.get("LEARNHOUSE_SELF_HOSTED") env_sql_connection_string = os.environ.get("LEARNHOUSE_SQL_CONNECTION_STRING") - # Sentry Config - env_sentry_dsn = os.environ.get("LEARNHOUSE_SENTRY_DSN") - env_sentry_environment = os.environ.get("LEARNHOUSE_SENTRY_ENVIRONMENT") - env_sentry_release = os.environ.get("LEARNHOUSE_SENTRY_RELEASE") + # Fill in values with YAML file if they are not provided site_name = env_site_name or yaml_config.get("site_name") @@ -247,33 +237,6 @@ def get_learnhouse_config() -> LearnHouseConfig: "mailing_config", {} ).get("system_email_adress") - # Sentry config - # check if the sentry config is provided in the YAML file - sentry_config_verif = ( - yaml_config.get("hosting_config", {}).get("sentry_config") - or env_sentry_dsn - or env_sentry_environment - or env_sentry_release - or None - ) - - sentry_dsn = env_sentry_dsn or yaml_config.get("hosting_config", {}).get( - "sentry_config", {} - ).get("dsn") - sentry_environment = env_sentry_environment or yaml_config.get( - "hosting_config", {} - ).get("sentry_config", {}).get("environment") - sentry_release = env_sentry_release or yaml_config.get("hosting_config", {}).get( - "sentry_config", {} - ).get("release") - - if sentry_config_verif: - sentry_config = SentryConfig( - dsn=sentry_dsn, environment=sentry_environment, release=sentry_release - ) - else: - sentry_config = None - # Payments config env_stripe_secret_key = os.environ.get("LEARNHOUSE_STRIPE_SECRET_KEY") env_stripe_publishable_key = os.environ.get("LEARNHOUSE_STRIPE_PUBLISHABLE_KEY") @@ -310,7 +273,6 @@ def get_learnhouse_config() -> LearnHouseConfig: allowed_origins=list(allowed_origins), allowed_regexp=allowed_regexp, self_hosted=bool(self_hosted), - sentry_config=sentry_config, cookie_config=cookie_config, content_delivery=content_delivery, ) diff --git a/apps/api/pyproject.toml b/apps/api/pyproject.toml index 8e011737..3f5b3417 100644 --- a/apps/api/pyproject.toml +++ b/apps/api/pyproject.toml @@ -27,7 +27,6 @@ dependencies = [ "redis>=5.0.7", "requests>=2.32.3", "resend>=2.4.0", - "sentry-sdk[fastapi]>=2.13.0", "sqlmodel>=0.0.19", "tiktoken>=0.7.0", "uvicorn==0.30.1", @@ -38,6 +37,7 @@ dependencies = [ "sqlalchemy-utils>=0.41.2", "stripe>=11.1.1", "python-jose>=3.3.0", + "logfire[sqlalchemy]>=3.8.0", ] [tool.ruff] diff --git a/apps/api/src/core/events/events.py b/apps/api/src/core/events/events.py index 6ef8b5f9..87d8e058 100644 --- a/apps/api/src/core/events/events.py +++ b/apps/api/src/core/events/events.py @@ -5,7 +5,6 @@ from src.core.events.autoinstall import auto_install from src.core.events.content import check_content_directory from src.core.events.database import close_database, connect_to_db from src.core.events.logs import create_logs_dir -from src.core.events.sentry import init_sentry def startup_app(app: FastAPI) -> Callable: @@ -14,9 +13,6 @@ def startup_app(app: FastAPI) -> Callable: learnhouse_config: LearnHouseConfig = get_learnhouse_config() app.learnhouse_config = learnhouse_config # type: ignore - # Init Sentry - await init_sentry(app) - # Connect to database await connect_to_db(app) diff --git a/apps/api/src/core/events/sentry.py b/apps/api/src/core/events/sentry.py deleted file mode 100644 index 7c460fdb..00000000 --- a/apps/api/src/core/events/sentry.py +++ /dev/null @@ -1,16 +0,0 @@ -from fastapi import FastAPI - -import sentry_sdk - -from config.config import LearnHouseConfig - -async def init_sentry(app: FastAPI) -> None: - - learnhouse_config : LearnHouseConfig = app.learnhouse_config # type: ignore - if learnhouse_config.hosting_config.sentry_config is not None: - sentry_sdk.init( - dsn=app.learnhouse_config.hosting_config.sentry_config.dsn, # type: ignore - environment=app.learnhouse_config.hosting_config.sentry_config.environment, # type: ignore - release=app.learnhouse_config.hosting_config.sentry_config.release, # type: ignore - traces_sample_rate=1.0, - ) From 67ac0b9d6720cc7eef3eefd7c451e2041cb9dd68 Mon Sep 17 00:00:00 2001 From: swve Date: Wed, 12 Mar 2025 13:23:37 +0100 Subject: [PATCH 2/5] feat: implement backend observability with logfire --- apps/api/app.py | 4 ++ apps/api/src/core/events/database.py | 2 + apps/api/uv.lock | 88 +++++++++++++++++++++------- 3 files changed, 74 insertions(+), 20 deletions(-) diff --git a/apps/api/app.py b/apps/api/app.py index f23973c9..aa3b4b2e 100644 --- a/apps/api/app.py +++ b/apps/api/app.py @@ -1,4 +1,5 @@ import uvicorn +import logfire from fastapi import FastAPI, Request from config.config import LearnHouseConfig, get_learnhouse_config from src.core.events.events import shutdown_app, startup_app @@ -38,6 +39,9 @@ app.add_middleware( allow_headers=["*"], ) +logfire.configure(console=False, service_name=learnhouse_config.site_name,) +logfire.instrument_fastapi(app) + # Gzip Middleware (will add brotli later) app.add_middleware(GZipMiddleware, minimum_size=1000) diff --git a/apps/api/src/core/events/database.py b/apps/api/src/core/events/database.py index e910f628..67a199ef 100644 --- a/apps/api/src/core/events/database.py +++ b/apps/api/src/core/events/database.py @@ -1,4 +1,5 @@ import logging +import logfire import os import importlib from config.config import get_learnhouse_config @@ -39,6 +40,7 @@ engine = create_engine( # Create all tables after importing all models SQLModel.metadata.create_all(engine) +logfire.instrument_sqlalchemy(engine=engine) async def connect_to_db(app: FastAPI): app.db_engine = engine # type: ignore diff --git a/apps/api/uv.lock b/apps/api/uv.lock index b8745cc7..f82a1c4c 100644 --- a/apps/api/uv.lock +++ b/apps/api/uv.lock @@ -403,6 +403,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521 }, ] +[[package]] +name = "executing" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 }, +] + [[package]] name = "faker" version = "36.1.1" @@ -1003,6 +1012,7 @@ dependencies = [ { name = "langchain-community", version = "0.2.19", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12.4'" }, { name = "langchain-openai", version = "0.1.14", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12.4'" }, { name = "langchain-openai", version = "0.1.25", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12.4'" }, + { name = "logfire", extra = ["sqlalchemy"] }, { name = "openai" }, { name = "passlib" }, { name = "psycopg2-binary" }, @@ -1015,7 +1025,6 @@ dependencies = [ { name = "redis" }, { name = "requests" }, { name = "resend" }, - { name = "sentry-sdk", extra = ["fastapi"] }, { name = "sqlalchemy-utils" }, { name = "sqlmodel" }, { name = "stripe" }, @@ -1038,6 +1047,7 @@ requires-dist = [ { name = "langchain", specifier = ">=0.1.7" }, { name = "langchain-community", specifier = ">=0.0.20" }, { name = "langchain-openai", specifier = ">=0.0.6" }, + { name = "logfire", extras = ["sqlalchemy"], specifier = ">=3.8.0" }, { name = "openai", specifier = ">=1.50.2" }, { name = "passlib", specifier = ">=1.7.4" }, { name = "psycopg2-binary", specifier = ">=2.9.9" }, @@ -1050,7 +1060,6 @@ requires-dist = [ { name = "redis", specifier = ">=5.0.7" }, { name = "requests", specifier = ">=2.32.3" }, { name = "resend", specifier = ">=2.4.0" }, - { name = "sentry-sdk", extras = ["fastapi"], specifier = ">=2.13.0" }, { name = "sqlalchemy-utils", specifier = ">=0.41.2" }, { name = "sqlmodel", specifier = ">=0.0.19" }, { name = "stripe", specifier = ">=11.1.1" }, @@ -1059,6 +1068,29 @@ requires-dist = [ { name = "uvicorn", specifier = "==0.30.1" }, ] +[[package]] +name = "logfire" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "executing" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-sdk" }, + { name = "protobuf" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/7c/ccd2aa47da9154788f0846864481b194695ca38db486500ae207b2c9f995/logfire-3.8.0.tar.gz", hash = "sha256:dc64745641d077e9411836c0d17af08f4fcef2737545dc43b67e51b20679e88b", size = 292729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d2/5644fab9b42c9f6de2dfe202666e125349d08d963d2f788aa44780d4da71/logfire-3.8.0-py3-none-any.whl", hash = "sha256:a2cda4d7bfb3a3a21bf99aa5c94bde6edff405ffa312b4dcbf8b99a8d52bf32e", size = 186976 }, +] + +[package.optional-dependencies] +sqlalchemy = [ + { name = "opentelemetry-instrumentation-sqlalchemy" }, +] + [[package]] name = "mako" version = "1.3.9" @@ -1304,6 +1336,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/35/d9f63fd84c2ed8dbd407bcbb933db4ed6e1b08e7fbdaca080b9ac309b927/opentelemetry_exporter_otlp_proto_grpc-1.30.0-py3-none-any.whl", hash = "sha256:2906bcae3d80acc54fd1ffcb9e44d324e8631058b502ebe4643ca71d1ff30830", size = 18550 }, ] +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/f9/abb9191d536e6a2e2b7903f8053bf859a76bf784e3ca19a5749550ef19e4/opentelemetry_exporter_otlp_proto_http-1.30.0.tar.gz", hash = "sha256:c3ae75d4181b1e34a60662a6814d0b94dd33b628bee5588a878bed92cee6abdc", size = 15073 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/3c/cdf34bc459613f2275aff9b258f35acdc4c4938dad161d17437de5d4c034/opentelemetry_exporter_otlp_proto_http-1.30.0-py3-none-any.whl", hash = "sha256:9578e790e579931c5ffd50f1e6975cbdefb6a0a0a5dea127a6ae87df10e0a589", size = 17245 }, +] + [[package]] name = "opentelemetry-instrumentation" version = "0.51b0" @@ -1351,6 +1401,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/1c/ec2d816b78edf2404d7b3df6d09eefb690b70bfd191b7da06f76634f1bdc/opentelemetry_instrumentation_fastapi-0.51b0-py3-none-any.whl", hash = "sha256:10513bbc11a1188adb9c1d2c520695f7a8f2b5f4de14e8162098035901cd6493", size = 12117 }, ] +[[package]] +name = "opentelemetry-instrumentation-sqlalchemy" +version = "0.51b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/b2/970b1b46576b663bba64503486afe266c064c2bfd1862876420714ce29d9/opentelemetry_instrumentation_sqlalchemy-0.51b0.tar.gz", hash = "sha256:dbfe95b69006017f903dda194606be458d54789e6b3419d37161fb8861bb98a5", size = 14582 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/d4/b68c3b3388dd5107f3ed532747e112249c152ba44af71a1f96673d66e3ee/opentelemetry_instrumentation_sqlalchemy-0.51b0-py3-none-any.whl", hash = "sha256:5ff4816228b496aef1511149e2b17a25e0faacec4d5eb65bf18a9964af40f1af", size = 14132 }, +] + [[package]] name = "opentelemetry-proto" version = "1.30.0" @@ -1816,24 +1882,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/ac/e7dc469e49048dc57f62e0c555d2ee3117fa30813d2a1a2962cce3a2a82a/s3transfer-0.11.2-py3-none-any.whl", hash = "sha256:be6ecb39fadd986ef1701097771f87e4d2f821f27f6071c872143884d2950fbc", size = 84151 }, ] -[[package]] -name = "sentry-sdk" -version = "2.22.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/81/b6/662988ecd2345bf6c3a5c306a9a3590852742eff91d0a78a143398b816f3/sentry_sdk-2.22.0.tar.gz", hash = "sha256:b4bf43bb38f547c84b2eadcefbe389b36ef75f3f38253d7a74d6b928c07ae944", size = 303539 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/7f/0e4459173e9671ba5f75a48dda2442bcc48a12c79e54e5789381c8c6a9bc/sentry_sdk-2.22.0-py2.py3-none-any.whl", hash = "sha256:3d791d631a6c97aad4da7074081a57073126c69487560c6f8bffcf586461de66", size = 325815 }, -] - -[package.optional-dependencies] -fastapi = [ - { name = "fastapi" }, -] - [[package]] name = "shellingham" version = "1.5.4" From 0229380cba2c14f61f8f7532422dbc13076d2c39 Mon Sep 17 00:00:00 2001 From: swve Date: Wed, 12 Mar 2025 14:10:42 +0100 Subject: [PATCH 3/5] feat: enhance assignment page with edit functionality and description display --- .../assignments/[assignmentuuid]/page.tsx | 113 ++++++---- .../Assignment/AssignmentStudentActivity.tsx | 17 +- .../Assignments/EditAssignmentModal.tsx | 194 ++++++++++++++++++ 3 files changed, 285 insertions(+), 39 deletions(-) create mode 100644 apps/web/components/Objects/Modals/Activities/Assignments/EditAssignmentModal.tsx diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx index 861af9fc..5b630c5e 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx @@ -1,6 +1,6 @@ 'use client'; import BreadCrumbs from '@components/Dashboard/Misc/BreadCrumbs' -import { BookOpen, BookX, EllipsisVertical, Eye, Layers2, Monitor, UserRoundPen } from 'lucide-react' +import { ArrowRight, BookOpen, BookX, EllipsisVertical, Eye, Layers2, Monitor, Pencil, UserRoundPen } from 'lucide-react' import React, { useEffect } from 'react' import { AssignmentProvider, useAssignments } from '@components/Contexts/Assignments/AssignmentContext'; import ToolTip from '@components/Objects/StyledElements/Tooltip/Tooltip'; @@ -16,6 +16,7 @@ import { updateActivity } from '@services/courses/activities'; import dynamic from 'next/dynamic'; import AssignmentEditorSubPage from './subpages/AssignmentEditorSubPage'; import { useMediaQuery } from 'usehooks-ts'; +import EditAssignmentModal from '@components/Objects/Modals/Activities/Assignments/EditAssignmentModal'; const AssignmentSubmissionsSubPage = dynamic(() => import('./subpages/AssignmentSubmissionsSubPage')) function AssignmentEdit() { @@ -46,7 +47,9 @@ function AssignmentEdit() {
-
Assignment Tools
+
+ +
@@ -106,6 +109,7 @@ function PublishingState() { const assignment = useAssignments() as any; const session = useLHSession() as any; const access_token = session?.data?.tokens?.access_token; + const [isEditModalOpen, setIsEditModalOpen] = React.useState(false); async function updateAssignmentPublishState(assignmentUUID: string) { const res = await updateAssignment({ published: !assignment?.assignment_object?.published }, assignmentUUID, access_token) @@ -125,50 +129,83 @@ function PublishingState() { }, [assignment]) return ( -
-
- {assignment?.assignment_object?.published ? 'Published' : 'Unpublished'} -
-
- - - - -

Preview

- -
- {assignment?.assignment_object?.published && -
updateAssignmentPublishState(assignment?.assignment_object?.assignment_uuid)} - className='flex px-3 py-2 cursor-pointer rounded-md space-x-2 items-center bg-linear-to-bl text-gray-800 font-medium from-gray-400/50 to-gray-200/80 border border-gray-600/10 shadow-gray-900/10 shadow-lg'> - -

Unpublish

+ <> +
+
+ {assignment?.assignment_object?.published ? 'Published' : 'Unpublished'}
- } - {!assignment?.assignment_object?.published && +
+ + content="Edit Assignment Details"> +
setIsEditModalOpen(true)} + className='flex px-3 py-2 cursor-pointer rounded-md space-x-2 items-center bg-linear-to-bl text-blue-800 font-medium from-blue-400/50 to-blue-200/80 border border-blue-600/10 shadow-blue-900/10 shadow-lg'> + +

Edit

+
+
+ + + + +

Preview

+ +
+ {assignment?.assignment_object?.published &&
updateAssignmentPublishState(assignment?.assignment_object?.assignment_uuid)} - className='flex px-3 py-2 cursor-pointer rounded-md space-x-2 items-center bg-linear-to-bl text-green-800 font-medium from-green-400/50 to-lime-200/80 border border-green-600/10 shadow-green-900/10 shadow-lg'> - -

Publish

+ className='flex px-3 py-2 cursor-pointer rounded-md space-x-2 items-center bg-linear-to-bl text-gray-800 font-medium from-gray-400/50 to-gray-200/80 border border-gray-600/10 shadow-gray-900/10 shadow-lg'> + +

Unpublish

} -
+ {!assignment?.assignment_object?.published && + +
updateAssignmentPublishState(assignment?.assignment_object?.assignment_uuid)} + className='flex px-3 py-2 cursor-pointer rounded-md space-x-2 items-center bg-linear-to-bl text-green-800 font-medium from-green-400/50 to-lime-200/80 border border-green-600/10 shadow-green-900/10 shadow-lg'> + +

Publish

+
+
} +
+ {isEditModalOpen && ( + setIsEditModalOpen(false)} + assignment={assignment?.assignment_object} + accessToken={access_token} + /> + )} + ) } + +function AssignmentTitle() { + const assignment = useAssignments() as any; + + return ( +
+ Assignment Tools +
+ ); +} diff --git a/apps/web/components/Objects/Activities/Assignment/AssignmentStudentActivity.tsx b/apps/web/components/Objects/Activities/Assignment/AssignmentStudentActivity.tsx index 30f5bfd8..a5420330 100644 --- a/apps/web/components/Objects/Activities/Assignment/AssignmentStudentActivity.tsx +++ b/apps/web/components/Objects/Activities/Assignment/AssignmentStudentActivity.tsx @@ -40,7 +40,22 @@ function AssignmentStudentActivity() {
-
+ + + {assignments?.assignment_object?.description && ( +
+
+
+ +

Assignment Description

+
+
+

{assignments.assignment_object.description}

+
+
+
+ )} + {assignments && assignments?.assignment_tasks?.sort((a: any, b: any) => a.id - b.id).map((task: any, index: number) => { return ( diff --git a/apps/web/components/Objects/Modals/Activities/Assignments/EditAssignmentModal.tsx b/apps/web/components/Objects/Modals/Activities/Assignments/EditAssignmentModal.tsx new file mode 100644 index 00000000..9f4e9453 --- /dev/null +++ b/apps/web/components/Objects/Modals/Activities/Assignments/EditAssignmentModal.tsx @@ -0,0 +1,194 @@ +import React from 'react'; +import { updateAssignment } from '@services/courses/assignments'; +import { mutate } from 'swr'; +import { getAPIUrl } from '@services/config/config'; +import toast from 'react-hot-toast'; +import FormLayout, { + FormField, + FormLabelAndMessage, + Input, + Textarea, + Flex, + FormLabel, + FormMessage +} from '@components/Objects/StyledElements/Form/Form'; +import * as Form from '@radix-ui/react-form'; +import { useFormik } from 'formik'; +import Modal from '@components/Objects/StyledElements/Modal/Modal'; + +interface Assignment { + assignment_uuid: string; + title: string; + description: string; + due_date?: string; + grading_type?: 'ALPHABET' | 'NUMERIC' | 'PERCENTAGE'; +} + +interface EditAssignmentFormProps { + onClose: () => void; + assignment: Assignment; + accessToken: string; +} + +interface EditAssignmentModalProps { + isOpen: boolean; + onClose: () => void; + assignment: Assignment; + accessToken: string; +} + +const EditAssignmentForm: React.FC = ({ + onClose, + assignment, + accessToken +}) => { + const formik = useFormik({ + initialValues: { + title: assignment.title || '', + description: assignment.description || '', + due_date: assignment.due_date || '', + grading_type: assignment.grading_type || 'ALPHABET' + }, + enableReinitialize: true, + onSubmit: async (values, { setSubmitting }) => { + const toast_loading = toast.loading('Updating assignment...'); + try { + const res = await updateAssignment(values, assignment.assignment_uuid, accessToken); + if (res.success) { + mutate(`${getAPIUrl()}assignments/${assignment.assignment_uuid}`); + toast.success('Assignment updated successfully'); + onClose(); + } else { + toast.error('Failed to update assignment'); + } + } catch (error) { + toast.error('An error occurred while updating the assignment'); + } finally { + toast.dismiss(toast_loading); + setSubmitting(false); + } + } + }); + + return ( + + + + Assignment Title + + Please provide a name for your assignment + + + + + + + + + + Assignment Description + + Please provide a description for your assignment + + + +