From 7f1ea63531bca6248202ee0422d75d295770e8d4 Mon Sep 17 00:00:00 2001 From: Oliver Braun Date: Sun, 19 Jul 2026 17:49:52 +0200 Subject: [PATCH] feat: generated-repos overview page (Milestone F5e, Z4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new /courses/[name]/repos page: per assignment, an "x/y generiert" badge (green all / yellow some / red none) from courseRepoOverview, with an expandable "n fehlend" list of who is missing. Assignments that are abstract or never generated show their note instead. Loaded server-side (GitLab, needs a token); a token error shows a link to /token. "📦 Repos" link on the course page. Co-Authored-By: Claude Opus 4.8 (1M context) --- schema.graphql | 35 ++++++++ src/lib/gql/gql.ts | 6 ++ src/lib/gql/graphql.ts | 8 ++ src/routes/courses/[name]/+page.svelte | 7 ++ .../courses/[name]/repos/+page.server.ts | 38 +++++++++ src/routes/courses/[name]/repos/+page.svelte | 84 +++++++++++++++++++ 6 files changed, 178 insertions(+) create mode 100644 src/routes/courses/[name]/repos/+page.server.ts create mode 100644 src/routes/courses/[name]/repos/+page.svelte diff --git a/schema.graphql b/schema.graphql index b7d5efe..2d94a90 100644 --- a/schema.graphql +++ b/schema.graphql @@ -581,6 +581,41 @@ type Subscription { assignmentReportProgress(course: String!, name: String!): ReportProgress! } +# ----- repos.graphqls ----- +"One target repository of an assignment and whether it actually exists in GitLab." +type RepoStatus { + "The student's email (or username/id) or the group's name." + for: String! + "The repository's project name." + repo: String! + "The full web URL of the repository." + url: String! + "Whether the repository has been generated (exists in GitLab)." + exists: Boolean! +} + +""" +How many of an assignment's target repositories have been generated. `note` is set +when the assignment could not be checked (abstract/unresolvable, or its GitLab group +does not exist yet — i.e. nothing generated). +""" +type AssignmentRepos { + name: String! + "Whether repos are per student or per group." + per: String! + "How many repositories the assignment targets." + targets: Int! + "How many of them actually exist in GitLab." + existing: Int! + repos: [RepoStatus!]! + note: String +} + +extend type Query { + "Per assignment of one of the caller's courses, which target repositories actually exist in GitLab. Needs a stored GitLab token." + courseRepoOverview(course: String!): [AssignmentRepos!]! +} + # ----- scheduling.graphqls ----- "The lifecycle state of a scheduled job. It starts PENDING, is claimed to RUNNING, and ends in exactly one terminal state." enum JobStatus { diff --git a/src/lib/gql/gql.ts b/src/lib/gql/gql.ts index 2cfbc24..4235d69 100644 --- a/src/lib/gql/gql.ts +++ b/src/lib/gql/gql.ts @@ -43,6 +43,7 @@ type Documents = { "\n\t\t\t\tquery CourseLint($name: String!) {\n\t\t\t\t\tcourseLint(name: $name) {\n\t\t\t\t\t\tpath\n\t\t\t\t\t\tmessage\n\t\t\t\t\t\tseverity\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t": typeof types.CourseLintDocument, "\n\t\t\t\tquery CourseActivity($name: String!) {\n\t\t\t\t\tcourseActivity(course: $name) {\n\t\t\t\t\t\tassignment\n\t\t\t\t\t\top\n\t\t\t\t\t\tstatus\n\t\t\t\t\t\tdetail\n\t\t\t\t\t\tat\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t": typeof types.CourseActivityDocument, "\n\t\t\t\tquery AssignmentEditor($course: String!, $name: String!) {\n\t\t\t\t\tcourse(name: $course) {\n\t\t\t\t\t\tassignmentNames\n\t\t\t\t\t}\n\t\t\t\t\tassignmentSchema {\n\t\t\t\t\t\tkey\n\t\t\t\t\t\tlabel\n\t\t\t\t\t\tdescription\n\t\t\t\t\t\tgroup\n\t\t\t\t\t\tkind\n\t\t\t\t\t\trequired\n\t\t\t\t\t\tdeprecated\n\t\t\t\t\t\texample\n\t\t\t\t\t\toptions {\n\t\t\t\t\t\t\tvalue\n\t\t\t\t\t\t\tlabel\n\t\t\t\t\t\t\tdescription\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tbranchRuleSchema {\n\t\t\t\t\t\tkey\n\t\t\t\t\t\tlabel\n\t\t\t\t\t\tdescription\n\t\t\t\t\t\tkind\n\t\t\t\t\t\trequired\n\t\t\t\t\t\texample\n\t\t\t\t\t}\n\t\t\t\t\tapprovalSettingsSchema {\n\t\t\t\t\t\tkey\n\t\t\t\t\t\tlabel\n\t\t\t\t\t\tdescription\n\t\t\t\t\t\tkind\n\t\t\t\t\t\trequired\n\t\t\t\t\t\texample\n\t\t\t\t\t\toptions {\n\t\t\t\t\t\t\tvalue\n\t\t\t\t\t\t\tlabel\n\t\t\t\t\t\t\tdescription\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tapprovalRuleSchema {\n\t\t\t\t\t\tkey\n\t\t\t\t\t\tlabel\n\t\t\t\t\t\tdescription\n\t\t\t\t\t\tkind\n\t\t\t\t\t\trequired\n\t\t\t\t\t\texample\n\t\t\t\t\t}\n\t\t\t\t\tassignment(course: $course, name: $name) {\n\t\t\t\t\t\tcourse\n\t\t\t\t\t\tname\n\t\t\t\t\t\textends\n\t\t\t\t\t\textendsOptions\n\t\t\t\t\t\tabstract\n\t\t\t\t\t\town {\n\t\t\t\t\t\t\tkey\n\t\t\t\t\t\t\tvalue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresolved\n\t\t\t\t\t\tresolveError\n\t\t\t\t\t}\n\t\t\t\t\tassignmentUrls(course: $course, name: $name) {\n\t\t\t\t\t\tper\n\t\t\t\t\t\tgroupUrl\n\t\t\t\t\t\trepos {\n\t\t\t\t\t\t\tfor\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tassignmentActivity(course: $course, name: $name) {\n\t\t\t\t\t\top\n\t\t\t\t\t\tstatus\n\t\t\t\t\t\tdetail\n\t\t\t\t\t\tat\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t": typeof types.AssignmentEditorDocument, + "\n\t\t\t\tquery CourseRepoOverview($course: String!) {\n\t\t\t\t\tcourseRepoOverview(course: $course) {\n\t\t\t\t\t\tname\n\t\t\t\t\t\tper\n\t\t\t\t\t\ttargets\n\t\t\t\t\t\texisting\n\t\t\t\t\t\tnote\n\t\t\t\t\t\trepos {\n\t\t\t\t\t\t\tfor\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t\texists\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t": typeof types.CourseRepoOverviewDocument, "\n\t\t\t\tquery CourseStudents($course: String!) {\n\t\t\t\t\tcourseStudents(course: $course) {\n\t\t\t\t\t\temail\n\t\t\t\t\t\tfound\n\t\t\t\t\t\tfirstName\n\t\t\t\t\t\tlastName\n\t\t\t\t\t\tgender\n\t\t\t\t\t\tgroup\n\t\t\t\t\t\tmtknr\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t": typeof types.CourseStudentsDocument, "\n\t\t\t\tquery CourseYAML($name: String!) {\n\t\t\t\t\tcourseYAML(name: $name)\n\t\t\t\t}\n\t\t\t": typeof types.CourseYamlDocument, "\n\t\t\t\tquery ScheduledJobs {\n\t\t\t\t\tscheduledJobs {\n\t\t\t\t\t\tid\n\t\t\t\t\t\top\n\t\t\t\t\t\tcourse\n\t\t\t\t\t\tassignment\n\t\t\t\t\t\trunAt\n\t\t\t\t\t\tgraceMinutes\n\t\t\t\t\t\tstatus\n\t\t\t\t\t\tcreatedAt\n\t\t\t\t\t\tstartedAt\n\t\t\t\t\t\tfinishedAt\n\t\t\t\t\t\terr\n\t\t\t\t\t\tparams {\n\t\t\t\t\t\t\tkey\n\t\t\t\t\t\t\tvalue\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t": typeof types.ScheduledJobsDocument, @@ -78,6 +79,7 @@ const documents: Documents = { "\n\t\t\t\tquery CourseLint($name: String!) {\n\t\t\t\t\tcourseLint(name: $name) {\n\t\t\t\t\t\tpath\n\t\t\t\t\t\tmessage\n\t\t\t\t\t\tseverity\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t": types.CourseLintDocument, "\n\t\t\t\tquery CourseActivity($name: String!) {\n\t\t\t\t\tcourseActivity(course: $name) {\n\t\t\t\t\t\tassignment\n\t\t\t\t\t\top\n\t\t\t\t\t\tstatus\n\t\t\t\t\t\tdetail\n\t\t\t\t\t\tat\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t": types.CourseActivityDocument, "\n\t\t\t\tquery AssignmentEditor($course: String!, $name: String!) {\n\t\t\t\t\tcourse(name: $course) {\n\t\t\t\t\t\tassignmentNames\n\t\t\t\t\t}\n\t\t\t\t\tassignmentSchema {\n\t\t\t\t\t\tkey\n\t\t\t\t\t\tlabel\n\t\t\t\t\t\tdescription\n\t\t\t\t\t\tgroup\n\t\t\t\t\t\tkind\n\t\t\t\t\t\trequired\n\t\t\t\t\t\tdeprecated\n\t\t\t\t\t\texample\n\t\t\t\t\t\toptions {\n\t\t\t\t\t\t\tvalue\n\t\t\t\t\t\t\tlabel\n\t\t\t\t\t\t\tdescription\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tbranchRuleSchema {\n\t\t\t\t\t\tkey\n\t\t\t\t\t\tlabel\n\t\t\t\t\t\tdescription\n\t\t\t\t\t\tkind\n\t\t\t\t\t\trequired\n\t\t\t\t\t\texample\n\t\t\t\t\t}\n\t\t\t\t\tapprovalSettingsSchema {\n\t\t\t\t\t\tkey\n\t\t\t\t\t\tlabel\n\t\t\t\t\t\tdescription\n\t\t\t\t\t\tkind\n\t\t\t\t\t\trequired\n\t\t\t\t\t\texample\n\t\t\t\t\t\toptions {\n\t\t\t\t\t\t\tvalue\n\t\t\t\t\t\t\tlabel\n\t\t\t\t\t\t\tdescription\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tapprovalRuleSchema {\n\t\t\t\t\t\tkey\n\t\t\t\t\t\tlabel\n\t\t\t\t\t\tdescription\n\t\t\t\t\t\tkind\n\t\t\t\t\t\trequired\n\t\t\t\t\t\texample\n\t\t\t\t\t}\n\t\t\t\t\tassignment(course: $course, name: $name) {\n\t\t\t\t\t\tcourse\n\t\t\t\t\t\tname\n\t\t\t\t\t\textends\n\t\t\t\t\t\textendsOptions\n\t\t\t\t\t\tabstract\n\t\t\t\t\t\town {\n\t\t\t\t\t\t\tkey\n\t\t\t\t\t\t\tvalue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresolved\n\t\t\t\t\t\tresolveError\n\t\t\t\t\t}\n\t\t\t\t\tassignmentUrls(course: $course, name: $name) {\n\t\t\t\t\t\tper\n\t\t\t\t\t\tgroupUrl\n\t\t\t\t\t\trepos {\n\t\t\t\t\t\t\tfor\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tassignmentActivity(course: $course, name: $name) {\n\t\t\t\t\t\top\n\t\t\t\t\t\tstatus\n\t\t\t\t\t\tdetail\n\t\t\t\t\t\tat\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t": types.AssignmentEditorDocument, + "\n\t\t\t\tquery CourseRepoOverview($course: String!) {\n\t\t\t\t\tcourseRepoOverview(course: $course) {\n\t\t\t\t\t\tname\n\t\t\t\t\t\tper\n\t\t\t\t\t\ttargets\n\t\t\t\t\t\texisting\n\t\t\t\t\t\tnote\n\t\t\t\t\t\trepos {\n\t\t\t\t\t\t\tfor\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t\texists\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t": types.CourseRepoOverviewDocument, "\n\t\t\t\tquery CourseStudents($course: String!) {\n\t\t\t\t\tcourseStudents(course: $course) {\n\t\t\t\t\t\temail\n\t\t\t\t\t\tfound\n\t\t\t\t\t\tfirstName\n\t\t\t\t\t\tlastName\n\t\t\t\t\t\tgender\n\t\t\t\t\t\tgroup\n\t\t\t\t\t\tmtknr\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t": types.CourseStudentsDocument, "\n\t\t\t\tquery CourseYAML($name: String!) {\n\t\t\t\t\tcourseYAML(name: $name)\n\t\t\t\t}\n\t\t\t": types.CourseYamlDocument, "\n\t\t\t\tquery ScheduledJobs {\n\t\t\t\t\tscheduledJobs {\n\t\t\t\t\t\tid\n\t\t\t\t\t\top\n\t\t\t\t\t\tcourse\n\t\t\t\t\t\tassignment\n\t\t\t\t\t\trunAt\n\t\t\t\t\t\tgraceMinutes\n\t\t\t\t\t\tstatus\n\t\t\t\t\t\tcreatedAt\n\t\t\t\t\t\tstartedAt\n\t\t\t\t\t\tfinishedAt\n\t\t\t\t\t\terr\n\t\t\t\t\t\tparams {\n\t\t\t\t\t\t\tkey\n\t\t\t\t\t\t\tvalue\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t": types.ScheduledJobsDocument, @@ -214,6 +216,10 @@ export function graphql(source: "\n\t\t\t\tquery CourseActivity($name: String!) * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "\n\t\t\t\tquery AssignmentEditor($course: String!, $name: String!) {\n\t\t\t\t\tcourse(name: $course) {\n\t\t\t\t\t\tassignmentNames\n\t\t\t\t\t}\n\t\t\t\t\tassignmentSchema {\n\t\t\t\t\t\tkey\n\t\t\t\t\t\tlabel\n\t\t\t\t\t\tdescription\n\t\t\t\t\t\tgroup\n\t\t\t\t\t\tkind\n\t\t\t\t\t\trequired\n\t\t\t\t\t\tdeprecated\n\t\t\t\t\t\texample\n\t\t\t\t\t\toptions {\n\t\t\t\t\t\t\tvalue\n\t\t\t\t\t\t\tlabel\n\t\t\t\t\t\t\tdescription\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tbranchRuleSchema {\n\t\t\t\t\t\tkey\n\t\t\t\t\t\tlabel\n\t\t\t\t\t\tdescription\n\t\t\t\t\t\tkind\n\t\t\t\t\t\trequired\n\t\t\t\t\t\texample\n\t\t\t\t\t}\n\t\t\t\t\tapprovalSettingsSchema {\n\t\t\t\t\t\tkey\n\t\t\t\t\t\tlabel\n\t\t\t\t\t\tdescription\n\t\t\t\t\t\tkind\n\t\t\t\t\t\trequired\n\t\t\t\t\t\texample\n\t\t\t\t\t\toptions {\n\t\t\t\t\t\t\tvalue\n\t\t\t\t\t\t\tlabel\n\t\t\t\t\t\t\tdescription\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tapprovalRuleSchema {\n\t\t\t\t\t\tkey\n\t\t\t\t\t\tlabel\n\t\t\t\t\t\tdescription\n\t\t\t\t\t\tkind\n\t\t\t\t\t\trequired\n\t\t\t\t\t\texample\n\t\t\t\t\t}\n\t\t\t\t\tassignment(course: $course, name: $name) {\n\t\t\t\t\t\tcourse\n\t\t\t\t\t\tname\n\t\t\t\t\t\textends\n\t\t\t\t\t\textendsOptions\n\t\t\t\t\t\tabstract\n\t\t\t\t\t\town {\n\t\t\t\t\t\t\tkey\n\t\t\t\t\t\t\tvalue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresolved\n\t\t\t\t\t\tresolveError\n\t\t\t\t\t}\n\t\t\t\t\tassignmentUrls(course: $course, name: $name) {\n\t\t\t\t\t\tper\n\t\t\t\t\t\tgroupUrl\n\t\t\t\t\t\trepos {\n\t\t\t\t\t\t\tfor\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tassignmentActivity(course: $course, name: $name) {\n\t\t\t\t\t\top\n\t\t\t\t\t\tstatus\n\t\t\t\t\t\tdetail\n\t\t\t\t\t\tat\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t"): (typeof documents)["\n\t\t\t\tquery AssignmentEditor($course: String!, $name: String!) {\n\t\t\t\t\tcourse(name: $course) {\n\t\t\t\t\t\tassignmentNames\n\t\t\t\t\t}\n\t\t\t\t\tassignmentSchema {\n\t\t\t\t\t\tkey\n\t\t\t\t\t\tlabel\n\t\t\t\t\t\tdescription\n\t\t\t\t\t\tgroup\n\t\t\t\t\t\tkind\n\t\t\t\t\t\trequired\n\t\t\t\t\t\tdeprecated\n\t\t\t\t\t\texample\n\t\t\t\t\t\toptions {\n\t\t\t\t\t\t\tvalue\n\t\t\t\t\t\t\tlabel\n\t\t\t\t\t\t\tdescription\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tbranchRuleSchema {\n\t\t\t\t\t\tkey\n\t\t\t\t\t\tlabel\n\t\t\t\t\t\tdescription\n\t\t\t\t\t\tkind\n\t\t\t\t\t\trequired\n\t\t\t\t\t\texample\n\t\t\t\t\t}\n\t\t\t\t\tapprovalSettingsSchema {\n\t\t\t\t\t\tkey\n\t\t\t\t\t\tlabel\n\t\t\t\t\t\tdescription\n\t\t\t\t\t\tkind\n\t\t\t\t\t\trequired\n\t\t\t\t\t\texample\n\t\t\t\t\t\toptions {\n\t\t\t\t\t\t\tvalue\n\t\t\t\t\t\t\tlabel\n\t\t\t\t\t\t\tdescription\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tapprovalRuleSchema {\n\t\t\t\t\t\tkey\n\t\t\t\t\t\tlabel\n\t\t\t\t\t\tdescription\n\t\t\t\t\t\tkind\n\t\t\t\t\t\trequired\n\t\t\t\t\t\texample\n\t\t\t\t\t}\n\t\t\t\t\tassignment(course: $course, name: $name) {\n\t\t\t\t\t\tcourse\n\t\t\t\t\t\tname\n\t\t\t\t\t\textends\n\t\t\t\t\t\textendsOptions\n\t\t\t\t\t\tabstract\n\t\t\t\t\t\town {\n\t\t\t\t\t\t\tkey\n\t\t\t\t\t\t\tvalue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresolved\n\t\t\t\t\t\tresolveError\n\t\t\t\t\t}\n\t\t\t\t\tassignmentUrls(course: $course, name: $name) {\n\t\t\t\t\t\tper\n\t\t\t\t\t\tgroupUrl\n\t\t\t\t\t\trepos {\n\t\t\t\t\t\t\tfor\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tassignmentActivity(course: $course, name: $name) {\n\t\t\t\t\t\top\n\t\t\t\t\t\tstatus\n\t\t\t\t\t\tdetail\n\t\t\t\t\t\tat\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n\t\t\t\tquery CourseRepoOverview($course: String!) {\n\t\t\t\t\tcourseRepoOverview(course: $course) {\n\t\t\t\t\t\tname\n\t\t\t\t\t\tper\n\t\t\t\t\t\ttargets\n\t\t\t\t\t\texisting\n\t\t\t\t\t\tnote\n\t\t\t\t\t\trepos {\n\t\t\t\t\t\t\tfor\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t\texists\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t"): (typeof documents)["\n\t\t\t\tquery CourseRepoOverview($course: String!) {\n\t\t\t\t\tcourseRepoOverview(course: $course) {\n\t\t\t\t\t\tname\n\t\t\t\t\t\tper\n\t\t\t\t\t\ttargets\n\t\t\t\t\t\texisting\n\t\t\t\t\t\tnote\n\t\t\t\t\t\trepos {\n\t\t\t\t\t\t\tfor\n\t\t\t\t\t\t\turl\n\t\t\t\t\t\t\texists\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/src/lib/gql/graphql.ts b/src/lib/gql/graphql.ts index 6d319bc..53fa801 100644 --- a/src/lib/gql/graphql.ts +++ b/src/lib/gql/graphql.ts @@ -304,6 +304,13 @@ export type AssignmentEditorQueryVariables = Exact<{ export type AssignmentEditorQuery = { course: { assignmentNames: Array } | null, assignmentSchema: Array<{ key: string, label: string, description: string, group: string, kind: FieldKind, required: boolean, deprecated: boolean, example: string | null, options: Array<{ value: string, label: string, description: string }> }>, branchRuleSchema: Array<{ key: string, label: string, description: string, kind: FieldKind, required: boolean, example: string | null }>, approvalSettingsSchema: Array<{ key: string, label: string, description: string, kind: FieldKind, required: boolean, example: string | null, options: Array<{ value: string, label: string, description: string }> }>, approvalRuleSchema: Array<{ key: string, label: string, description: string, kind: FieldKind, required: boolean, example: string | null }>, assignment: { course: string, name: string, extends: string | null, extendsOptions: Array, abstract: boolean, resolved: string, resolveError: string | null, own: Array<{ key: string, value: string }> } | null, assignmentUrls: { per: string, groupUrl: string, repos: Array<{ for: string, url: string }> } | null, assignmentActivity: Array<{ op: string, status: string, detail: string, at: string }> }; +export type CourseRepoOverviewQueryVariables = Exact<{ + course: string; +}>; + + +export type CourseRepoOverviewQuery = { courseRepoOverview: Array<{ name: string, per: string, targets: number, existing: number, note: string | null, repos: Array<{ for: string, url: string, exists: boolean }> }> }; + export type CourseStudentsQueryVariables = Exact<{ course: string; }>; @@ -358,6 +365,7 @@ export const CourseDocument = {"kind":"Document","definitions":[{"kind":"Operati export const CourseLintDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CourseLint"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"courseLint"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"severity"}}]}}]}}]} as unknown as DocumentNode; export const CourseActivityDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CourseActivity"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"courseActivity"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignment"}},{"kind":"Field","name":{"kind":"Name","value":"op"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"detail"}},{"kind":"Field","name":{"kind":"Name","value":"at"}}]}}]}}]} as unknown as DocumentNode; export const AssignmentEditorDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AssignmentEditor"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"course"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"course"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignmentNames"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignmentSchema"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"required"}},{"kind":"Field","name":{"kind":"Name","value":"deprecated"}},{"kind":"Field","name":{"kind":"Name","value":"example"}},{"kind":"Field","name":{"kind":"Name","value":"options"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"branchRuleSchema"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"required"}},{"kind":"Field","name":{"kind":"Name","value":"example"}}]}},{"kind":"Field","name":{"kind":"Name","value":"approvalSettingsSchema"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"required"}},{"kind":"Field","name":{"kind":"Name","value":"example"}},{"kind":"Field","name":{"kind":"Name","value":"options"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"approvalRuleSchema"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"required"}},{"kind":"Field","name":{"kind":"Name","value":"example"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignment"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course"},"value":{"kind":"Variable","name":{"kind":"Name","value":"course"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"course"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"extends"}},{"kind":"Field","name":{"kind":"Name","value":"extendsOptions"}},{"kind":"Field","name":{"kind":"Name","value":"abstract"}},{"kind":"Field","name":{"kind":"Name","value":"own"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}},{"kind":"Field","name":{"kind":"Name","value":"resolved"}},{"kind":"Field","name":{"kind":"Name","value":"resolveError"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignmentUrls"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course"},"value":{"kind":"Variable","name":{"kind":"Name","value":"course"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"per"}},{"kind":"Field","name":{"kind":"Name","value":"groupUrl"}},{"kind":"Field","name":{"kind":"Name","value":"repos"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"for"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignmentActivity"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course"},"value":{"kind":"Variable","name":{"kind":"Name","value":"course"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"op"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"detail"}},{"kind":"Field","name":{"kind":"Name","value":"at"}}]}}]}}]} as unknown as DocumentNode; +export const CourseRepoOverviewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CourseRepoOverview"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"course"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"courseRepoOverview"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course"},"value":{"kind":"Variable","name":{"kind":"Name","value":"course"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"per"}},{"kind":"Field","name":{"kind":"Name","value":"targets"}},{"kind":"Field","name":{"kind":"Name","value":"existing"}},{"kind":"Field","name":{"kind":"Name","value":"note"}},{"kind":"Field","name":{"kind":"Name","value":"repos"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"for"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"exists"}}]}}]}}]}}]} as unknown as DocumentNode; export const CourseStudentsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CourseStudents"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"course"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"courseStudents"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"course"},"value":{"kind":"Variable","name":{"kind":"Name","value":"course"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"found"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"gender"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"mtknr"}}]}}]}}]} as unknown as DocumentNode; export const CourseYamlDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CourseYAML"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"courseYAML"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}}]}]}}]} as unknown as DocumentNode; export const ScheduledJobsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ScheduledJobs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scheduledJobs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"op"}},{"kind":"Field","name":{"kind":"Name","value":"course"}},{"kind":"Field","name":{"kind":"Name","value":"assignment"}},{"kind":"Field","name":{"kind":"Name","value":"runAt"}},{"kind":"Field","name":{"kind":"Name","value":"graceMinutes"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"startedAt"}},{"kind":"Field","name":{"kind":"Name","value":"finishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"err"}},{"kind":"Field","name":{"kind":"Name","value":"params"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]}}]}}]} as unknown as DocumentNode; diff --git a/src/routes/courses/[name]/+page.svelte b/src/routes/courses/[name]/+page.svelte index 46d75ba..dc74af7 100644 --- a/src/routes/courses/[name]/+page.svelte +++ b/src/routes/courses/[name]/+page.svelte @@ -214,6 +214,13 @@ > 👥 Studierende + + 📦 Repos + { + const course = params.name; + try { + const d = await backendRequest( + graphql(` + query CourseRepoOverview($course: String!) { + courseRepoOverview(course: $course) { + name + per + targets + existing + note + repos { + for + url + exists + } + } + } + `), + { course } + ); + return { course, overview: d?.courseRepoOverview ?? [], error: null as string | null }; + } catch (e) { + return { course, overview: null, error: gqlErrorMessage(e) }; + } +}; diff --git a/src/routes/courses/[name]/repos/+page.svelte b/src/routes/courses/[name]/repos/+page.svelte new file mode 100644 index 0000000..1e58dad --- /dev/null +++ b/src/routes/courses/[name]/repos/+page.svelte @@ -0,0 +1,84 @@ + + +Repos · {data.course} · glabs + +
+ ← {data.course} +

Generierte Repos: {data.course}

+

+ Pro Assignment: wie viele der Ziel-Repos tatsächlich in GitLab existieren. Live aus GitLab + (GitLab-Token nötig). +

+ + {#if error} +
+ {error} +
+ {#if needsToken} +

+ GitLab-Token hinterlegen → +

+ {/if} + {:else if overview.length === 0} +

Keine Assignments in diesem Kurs.

+ {:else} +
+ {#each overview as a (a.name)} + {@const miss = missing(a)} +
+
+ + {a.name} + + {#if a.note} + + {a.note} + {:else} + + {a.existing}/{a.targets} generiert + + ({a.per}) + {/if} +
+ + {#if !a.note && miss.length > 0} +
+ + {miss.length} fehlend + +
    + {#each miss as r (r.url)} +
  • {r.for}
  • + {/each} +
+
+ {/if} +
+ {/each} +
+ {/if} +