Update
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
6620dea812
commit
8055a5e4db
4
Makefile
4
Makefile
|
@ -19,10 +19,10 @@
|
|||
all: prod
|
||||
|
||||
dev:
|
||||
docker-compose build --build-arg BUILD_ENV=development
|
||||
docker compose build --build-arg BUILD_ENV=development
|
||||
|
||||
prod:
|
||||
docker-compose build
|
||||
docker compose build
|
||||
|
||||
docs:
|
||||
cd docs && mdbook build
|
||||
|
|
|
@ -54,12 +54,13 @@ CREATE TABLE teacher_votes(
|
|||
id serial PRIMARY KEY,
|
||||
vote_id int NOT NULL REFERENCES votes(id),
|
||||
teacher_id int NOT NULL REFERENCES teachers(id),
|
||||
priority int NOT NULL
|
||||
priority int NOT NULL,
|
||||
UNIQUE (vote_id, teacher_id, priority)
|
||||
);
|
||||
|
||||
CREATE TABLE assignments(
|
||||
id serial PRIMARY KEY,
|
||||
student_id int NOT NULL REFERENCES students(id),
|
||||
student_id int NOT NULL REFERENCES students(id) UNIQUE,
|
||||
teacher_id int NOT NULL REFERENCES teachers(id)
|
||||
);
|
||||
|
||||
|
|
|
@ -70,7 +70,12 @@ module Backend
|
|||
return ATH::Exceptions::BadRequest.new("No request body given") unless request.body
|
||||
query = GraphQLQuery.from_json(request.body.not_nil!)
|
||||
|
||||
ATH::StreamedResponse.new(headers: HTTP::Headers{"content-type" => "application/json"}) do |io|
|
||||
ATH::StreamedResponse.new(
|
||||
headers: HTTP::Headers{
|
||||
"content-type" => "application/json",
|
||||
"cache-control" => ["no-cache", "no-store", "max-age=0", "must-revalidate"],
|
||||
}
|
||||
) do |io|
|
||||
Api::Schema::SCHEMA.execute(
|
||||
io,
|
||||
query.query,
|
||||
|
|
|
@ -19,11 +19,13 @@ module Backend
|
|||
module Jobs
|
||||
# Assigns students to teachers when all students voted
|
||||
class AssignmentJob < Mosquito::QueuedJob
|
||||
# run_every 1.minute
|
||||
|
||||
alias TeacherVote = {student: Int32, priority: Int32}
|
||||
alias Assignment = {teacher: Int32, priority: Int32}
|
||||
|
||||
def rescheduleable?
|
||||
false
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def perform : Nil
|
||||
teacher_count = Db::Teacher.query.count
|
||||
|
|
|
@ -30,7 +30,7 @@ services:
|
|||
- frontend
|
||||
|
||||
db:
|
||||
image: postgres:alpine
|
||||
image: docker.io/postgres:alpine
|
||||
restart: always
|
||||
networks:
|
||||
- db
|
||||
|
@ -41,7 +41,7 @@ services:
|
|||
- db:/var/lib/postgresql/data
|
||||
|
||||
adminer:
|
||||
image: adminer:standalone
|
||||
image: docker.io/adminer:standalone
|
||||
restart: always
|
||||
networks:
|
||||
- default
|
||||
|
|
|
@ -10,10 +10,10 @@ opt-level = "z"
|
|||
panic = "abort"
|
||||
|
||||
[dependencies]
|
||||
yew = "0.19.3"
|
||||
yew = { version = "0.20.0", features = ["csr"] }
|
||||
wasm-logger = "0.2.0"
|
||||
log = "0.4.6"
|
||||
yew-router = "0.16.0"
|
||||
yew-router = "0.17.0"
|
||||
wee_alloc = "0.4.5"
|
||||
graphql_client = { git = "https://github.com/graphql-rust/graphql-client.git", branch = "main", features = ["reqwest"] }
|
||||
reqwest = "0.11.12"
|
||||
|
@ -22,7 +22,7 @@ web-sys = { version = "0.3.60", features = ["Window", "Location"] }
|
|||
wasm-cookies = "0.1.0"
|
||||
lazy_static = "1.4.0"
|
||||
const_format = "0.2.30"
|
||||
yew-agent = "0.1.0"
|
||||
yew-side-effect = "0.2.0"
|
||||
uuid = { version = "1.2.2", features = ["serde"] }
|
||||
chrono = { version = "0.4.23", features = ["serde"] }
|
||||
yewdux = "0.9.0"
|
||||
bounce = { version = "0.6.0", features = ["helmet"] }
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
query Students {
|
||||
students {
|
||||
id
|
||||
user {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
username
|
||||
email
|
||||
role
|
||||
admin
|
||||
}
|
||||
vote {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
query Teachers {
|
||||
teachers {
|
||||
id
|
||||
user {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
username
|
||||
email
|
||||
role
|
||||
admin
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,19 @@
|
|||
"The `Boolean` scalar type represents `true` or `false`."
|
||||
scalar Boolean
|
||||
|
||||
type Config {
|
||||
minimumTeacherSelectionCount: Int!
|
||||
}
|
||||
|
||||
"The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point)."
|
||||
scalar Float
|
||||
|
||||
"The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID."
|
||||
scalar ID
|
||||
|
||||
"The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1."
|
||||
scalar Int
|
||||
|
||||
type Query {
|
||||
admins: [User!]
|
||||
allStudentsVoted: Boolean
|
||||
|
@ -23,6 +35,9 @@ type Query {
|
|||
votes: [Vote!]
|
||||
}
|
||||
|
||||
"The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text."
|
||||
scalar String
|
||||
|
||||
type Student {
|
||||
id: Int!
|
||||
user: User!
|
||||
|
@ -79,6 +94,67 @@ type Vote {
|
|||
teacherVotes: [TeacherVote!]!
|
||||
}
|
||||
|
||||
type __Directive {
|
||||
args: [__InputValue!]!
|
||||
description: String
|
||||
locations: [String!]!
|
||||
name: String!
|
||||
}
|
||||
|
||||
type __EnumValue {
|
||||
deprecationReason: String
|
||||
description: String
|
||||
isDeprecated: Boolean!
|
||||
name: String!
|
||||
}
|
||||
|
||||
type __Field {
|
||||
args: [__InputValue!]!
|
||||
deprecationReason: String
|
||||
description: String
|
||||
isDeprecated: Boolean!
|
||||
name: String!
|
||||
type: __Type!
|
||||
}
|
||||
|
||||
type __InputValue {
|
||||
defaultValue: String
|
||||
description: String
|
||||
name: String!
|
||||
type: __Type!
|
||||
}
|
||||
|
||||
type __Schema {
|
||||
directives: [__Directive!]!
|
||||
mutationType: __Type
|
||||
queryType: __Type!
|
||||
subscriptionType: __Type
|
||||
types: [__Type!]!
|
||||
}
|
||||
|
||||
type __Type {
|
||||
description: String
|
||||
enumValues(includeDeprecated: Boolean! = false): [__EnumValue!]
|
||||
fields(includeDeprecated: Boolean! = false): [__Field!]
|
||||
inputFields: [__InputValue!]
|
||||
interfaces: [__Type!]
|
||||
kind: __TypeKind!
|
||||
name: String
|
||||
ofType: __Type
|
||||
possibleTypes: [__Type!]
|
||||
}
|
||||
|
||||
enum __TypeKind {
|
||||
ENUM
|
||||
INPUT_OBJECT
|
||||
INTERFACE
|
||||
LIST
|
||||
NON_NULL
|
||||
OBJECT
|
||||
SCALAR
|
||||
UNION
|
||||
}
|
||||
|
||||
type LoginPayload {
|
||||
bearer: String!
|
||||
token: String!
|
||||
|
@ -108,4 +184,4 @@ input UserCreateInput {
|
|||
|
||||
input VoteCreateInput {
|
||||
teacherIds: [Int!]!
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use yew_agent::{Agent, AgentLink, Context, HandlerId};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum Request {
|
||||
LoggedIn(bool),
|
||||
}
|
||||
|
||||
pub struct EventBus {
|
||||
link: AgentLink<Self>,
|
||||
subscribers: HashSet<HandlerId>,
|
||||
}
|
||||
|
||||
impl Agent for EventBus {
|
||||
type Reach = Context<Self>;
|
||||
type Message = ();
|
||||
type Input = Request;
|
||||
type Output = bool;
|
||||
|
||||
fn create(link: AgentLink<Self>) -> Self {
|
||||
Self {
|
||||
link,
|
||||
subscribers: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _msg: Self::Message) {}
|
||||
|
||||
fn handle_input(&mut self, msg: Self::Input, _id: HandlerId) {
|
||||
match msg {
|
||||
Request::LoggedIn(x) => {
|
||||
for sub in self.subscribers.iter() {
|
||||
self.link.respond(*sub, x);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn connected(&mut self, id: HandlerId) {
|
||||
self.subscribers.insert(id);
|
||||
}
|
||||
|
||||
fn disconnected(&mut self, id: HandlerId) {
|
||||
self.subscribers.remove(&id);
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
pub mod logged_in;
|
|
@ -15,7 +15,7 @@ impl Component for Footer {
|
|||
<footer class={classes!("footer")}>
|
||||
<div class={classes!("columns")}>
|
||||
<div class={classes!("column", "is-three-quarters")}>
|
||||
<h3 class={classes!("title", "is-3")}>{ "dergrimm.net" }</h3>
|
||||
<h3 class={classes!("title", "is-3")}>{ "mentorenwahl.dergrimm.net" }</h3>
|
||||
<p>
|
||||
<a href="https://git.dergrimm.net/mentorenwahl/mentorenwahl"><strong>{ "Mentorenwahl" }</strong></a> { " von " }
|
||||
<a href="https://dergrimm.net">{ "Dominic Grimm" }</a> { ". " }
|
||||
|
@ -24,9 +24,11 @@ impl Component for Footer {
|
|||
</div>
|
||||
<div class={classes!("column")}>
|
||||
<p>
|
||||
<figure class={classes!("image")}>
|
||||
<img src="https://forthebadge.com/images/badges/made-with-rust.svg" />
|
||||
</figure>
|
||||
<a href="https://www.rust-lang.org/">
|
||||
<figure class={classes!("image")}>
|
||||
<img src="https://forthebadge.com/images/badges/made-with-rust.svg" />
|
||||
</figure>
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<figure class={classes!("image")}>
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
use yew::prelude::*;
|
||||
use yew_agent::{Bridge, Bridged};
|
||||
use yew_router::prelude::*;
|
||||
|
||||
use crate::agents;
|
||||
use crate::cookies;
|
||||
use crate::routes;
|
||||
|
||||
pub enum Msg {
|
||||
LoggedIn(bool),
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct LoggedInHandlerProps {
|
||||
pub logged_in: bool,
|
||||
}
|
||||
|
||||
pub struct LoggedInHandler {
|
||||
logged_in: bool,
|
||||
_logged_in_producer: Box<dyn Bridge<agents::logged_in::EventBus>>,
|
||||
}
|
||||
|
||||
impl Component for LoggedInHandler {
|
||||
type Message = Msg;
|
||||
type Properties = LoggedInHandlerProps;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
Self {
|
||||
logged_in: ctx.props().logged_in,
|
||||
_logged_in_producer: agents::logged_in::EventBus::bridge(
|
||||
ctx.link().callback(Msg::LoggedIn),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::LoggedIn(x) => {
|
||||
if self.logged_in && !x {
|
||||
log::info!("Global logout!");
|
||||
cookies::logout_clear();
|
||||
ctx.link().history().unwrap().push(routes::Route::Login);
|
||||
}
|
||||
self.logged_in = x;
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||
html! {}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
pub mod fetching;
|
||||
pub mod footer;
|
||||
pub mod graphql_errors;
|
||||
pub mod logged_in_handler;
|
||||
pub mod navbar;
|
||||
pub mod title;
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
use graphql_client::reqwest::post_graphql;
|
||||
use std::rc::Rc;
|
||||
use yew::prelude::*;
|
||||
use yew_agent::{Dispatched, Dispatcher};
|
||||
use yew_router::prelude::*;
|
||||
use yewdux::prelude::*;
|
||||
|
||||
use crate::agents;
|
||||
use crate::cookies;
|
||||
use crate::graphql;
|
||||
use crate::routes;
|
||||
use crate::stores;
|
||||
|
||||
pub enum Msg {
|
||||
UpdateLoggedIn(Rc<stores::LoggedIn>),
|
||||
LogoutClicked,
|
||||
Logout,
|
||||
BurgerClicked,
|
||||
|
@ -16,12 +19,11 @@ pub enum Msg {
|
|||
#[derive(Properties, PartialEq)]
|
||||
pub struct NavbarProps {
|
||||
pub token: Option<String>,
|
||||
pub logged_in: bool,
|
||||
}
|
||||
|
||||
pub struct Navbar {
|
||||
logged_in: bool,
|
||||
logged_in_event_bus: Dispatcher<agents::logged_in::EventBus>,
|
||||
logged_in: Rc<stores::LoggedIn>,
|
||||
logged_in_dispatch: Dispatch<stores::LoggedIn>,
|
||||
burger_active: bool,
|
||||
}
|
||||
|
||||
|
@ -30,15 +32,23 @@ impl Component for Navbar {
|
|||
type Properties = NavbarProps;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let dispatch = Dispatch::subscribe(ctx.link().callback(Msg::UpdateLoggedIn));
|
||||
|
||||
Self {
|
||||
logged_in: ctx.props().logged_in,
|
||||
logged_in_event_bus: agents::logged_in::EventBus::dispatcher(),
|
||||
logged_in: dispatch.get(),
|
||||
logged_in_dispatch: dispatch,
|
||||
burger_active: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::UpdateLoggedIn(x) => {
|
||||
let tmp = self.logged_in.0;
|
||||
self.logged_in = x;
|
||||
|
||||
tmp != self.logged_in.0
|
||||
}
|
||||
Msg::LogoutClicked => {
|
||||
let client = graphql::client(ctx.props().token.as_ref()).unwrap();
|
||||
ctx.link().send_future(async move {
|
||||
|
@ -56,8 +66,9 @@ impl Component for Navbar {
|
|||
false
|
||||
}
|
||||
Msg::Logout => {
|
||||
self.logged_in_event_bus
|
||||
.send(agents::logged_in::Request::LoggedIn(false));
|
||||
cookies::logout_clear();
|
||||
self.logged_in_dispatch.reduce_mut(|x| x.0 = false);
|
||||
ctx.link().navigator().unwrap().push(&routes::Route::Login);
|
||||
false
|
||||
}
|
||||
Msg::BurgerClicked => {
|
||||
|
@ -114,7 +125,7 @@ impl Component for Navbar {
|
|||
|
||||
<div class={classes!("navbar-item")}>
|
||||
<div class={classes!("buttons")}>
|
||||
if self.logged_in {
|
||||
if self.logged_in.0 {
|
||||
<a onclick={ctx.link().callback(|_| Msg::LogoutClicked)} class={classes!("button")}>
|
||||
{ "Logout" }
|
||||
</a>
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
use bounce::helmet::Helmet;
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct TitleProps {
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
pub struct Title;
|
||||
|
||||
impl Component for Title {
|
||||
type Message = ();
|
||||
type Properties = TitleProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<Helmet>
|
||||
<title>{ format!("{} | Mentorenwahl", ctx.props().title) }</title>
|
||||
</Helmet>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,21 @@
|
|||
use const_format::concatcp;
|
||||
pub const DEFAULT_COOKIE_OPTIONS: wasm_cookies::CookieOptions = wasm_cookies::CookieOptions {
|
||||
domain: None,
|
||||
expires: None,
|
||||
path: None,
|
||||
same_site: wasm_cookies::SameSite::Lax,
|
||||
secure: false,
|
||||
};
|
||||
|
||||
const BASE: &str = "mentorenwahl_";
|
||||
pub mod names {
|
||||
use const_format::concatcp;
|
||||
|
||||
pub const TOKEN: &str = concatcp!(BASE, "token");
|
||||
pub const ADMIN: &str = concatcp!(BASE, "admin");
|
||||
const BASE: &str = "mentorenwahl_";
|
||||
|
||||
pub const DELETE_ON_LOGOUT: [&str; 2] = [TOKEN, ADMIN];
|
||||
pub const TOKEN: &str = concatcp!(BASE, "token");
|
||||
pub const ADMIN: &str = concatcp!(BASE, "admin");
|
||||
}
|
||||
|
||||
const DELETE_ON_LOGOUT: [&str; 2] = [names::TOKEN, names::ADMIN];
|
||||
|
||||
pub fn logout_clear() {
|
||||
for x in DELETE_ON_LOGOUT {
|
||||
|
|
|
@ -4,3 +4,4 @@ pub mod ok;
|
|||
pub mod students_can_vote;
|
||||
pub mod teachers;
|
||||
pub mod tokens;
|
||||
pub mod users_by_role;
|
||||
|
|
|
@ -2,7 +2,6 @@ use graphql_client::GraphQLQuery;
|
|||
|
||||
use crate::graphql::scalars;
|
||||
|
||||
// type UUID = String;
|
||||
type UUID = scalars::UUID;
|
||||
type Time = String;
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
use graphql_client::GraphQLQuery;
|
||||
|
||||
#[derive(GraphQLQuery)]
|
||||
#[graphql(
|
||||
schema_path = "graphql/schema.graphql",
|
||||
query_path = "graphql/queries/users_by_role_students.graphql",
|
||||
response_derives = "Debug",
|
||||
skip_serializing_none
|
||||
)]
|
||||
pub struct Students;
|
||||
|
||||
#[derive(GraphQLQuery)]
|
||||
#[graphql(
|
||||
schema_path = "graphql/schema.graphql",
|
||||
query_path = "graphql/queries/users_by_role_teachers.graphql",
|
||||
response_derives = "Debug",
|
||||
skip_serializing_none
|
||||
)]
|
||||
pub struct Teachers;
|
|
@ -5,7 +5,6 @@ use crate::components;
|
|||
#[derive(Properties, PartialEq)]
|
||||
pub struct BaseProps {
|
||||
pub token: Option<String>,
|
||||
pub logged_in: bool,
|
||||
#[prop_or_default]
|
||||
pub children: Children,
|
||||
}
|
||||
|
@ -23,10 +22,8 @@ impl Component for Base {
|
|||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<components::logged_in_handler::LoggedInHandler logged_in={ctx.props().logged_in} />
|
||||
|
||||
<div id="wrapper">
|
||||
<components::navbar::Navbar token={ctx.props().token.to_owned()} logged_in={ctx.props().logged_in} />
|
||||
<components::navbar::Navbar token={ctx.props().token.to_owned()} />
|
||||
{ for ctx.props().children.iter() }
|
||||
</div>
|
||||
<components::footer::Footer />
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
use std::rc::Rc;
|
||||
use yew::prelude::*;
|
||||
use yew_agent::{Bridge, Bridged};
|
||||
use yew_router::prelude::*;
|
||||
use yewdux::prelude::*;
|
||||
|
||||
use crate::agents;
|
||||
use crate::routes;
|
||||
use crate::stores;
|
||||
|
||||
pub enum Msg {
|
||||
LoggedIn(bool),
|
||||
UpdateLoggedIn(Rc<stores::LoggedIn>),
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct LoggedInProps {
|
||||
pub logged_in: bool,
|
||||
#[prop_or_default]
|
||||
pub children: Children,
|
||||
}
|
||||
|
||||
pub struct LoggedIn {
|
||||
logged_in: bool,
|
||||
_logged_in_producer: Box<dyn Bridge<agents::logged_in::EventBus>>,
|
||||
logged_in: Rc<stores::LoggedIn>,
|
||||
_logged_in_dispatch: Dispatch<stores::LoggedIn>, // _logged_in_producer: Box<dyn Bridge<agents::logged_in::EventBus>>,
|
||||
}
|
||||
|
||||
impl Component for LoggedIn {
|
||||
|
@ -26,33 +26,37 @@ impl Component for LoggedIn {
|
|||
type Properties = LoggedInProps;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let dispatch = Dispatch::subscribe(ctx.link().callback(Msg::UpdateLoggedIn));
|
||||
let logged_in = dispatch.get();
|
||||
|
||||
if !logged_in.0 {
|
||||
ctx.link().navigator().unwrap().push(&routes::Route::Login)
|
||||
}
|
||||
|
||||
Self {
|
||||
logged_in: ctx.props().logged_in,
|
||||
_logged_in_producer: agents::logged_in::EventBus::bridge(
|
||||
ctx.link().callback(Msg::LoggedIn),
|
||||
),
|
||||
logged_in,
|
||||
_logged_in_dispatch: dispatch,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::LoggedIn(x) => {
|
||||
let prev = self.logged_in;
|
||||
Msg::UpdateLoggedIn(x) => {
|
||||
self.logged_in = x;
|
||||
prev != self.logged_in
|
||||
if !self.logged_in.0 {
|
||||
ctx.link().navigator().unwrap().push(&routes::Route::Login);
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
if !self.logged_in {
|
||||
ctx.link().history().unwrap().push(routes::Route::Login);
|
||||
}
|
||||
|
||||
html! {
|
||||
if self.logged_in {
|
||||
{ for ctx.props().children.iter() }
|
||||
}
|
||||
if self.logged_in.0 {
|
||||
{ for ctx.props().children.iter() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ use crate::layouts;
|
|||
#[derive(Properties, PartialEq)]
|
||||
pub struct MainProps {
|
||||
pub token: Option<String>,
|
||||
pub logged_in: bool,
|
||||
#[prop_or_default]
|
||||
pub children: Children,
|
||||
}
|
||||
|
@ -23,9 +22,9 @@ impl Component for Main {
|
|||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<layouts::base::Base token={ctx.props().token.to_owned()} logged_in={ctx.props().logged_in}>
|
||||
<div class={classes!("columns")}>
|
||||
<main class={classes!("column", "is-four-fifths", "mx-auto")}>
|
||||
<layouts::base::Base token={ctx.props().token.to_owned()}>
|
||||
<div class={classes!("columns", "is-centered")}>
|
||||
<main class={classes!("column", "is-four-fifths")}>
|
||||
{ for ctx.props().children.iter() }
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
pub mod agents;
|
||||
pub mod components;
|
||||
pub mod cookies;
|
||||
pub mod graphql;
|
||||
pub mod layouts;
|
||||
pub mod routes;
|
||||
pub mod stores;
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
use std::rc::Rc;
|
||||
use bounce::helmet::HelmetBridge;
|
||||
use bounce::BounceRoot;
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
use yew_side_effect::title::TitleProvider;
|
||||
|
||||
use frontend::routes;
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
@ -19,15 +17,13 @@ impl Component for App {
|
|||
}
|
||||
|
||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||
let format_title =
|
||||
Rc::new(|m: &str| format!("{} | mentorenwahl.de", m)) as Rc<dyn Fn(&str) -> String>;
|
||||
|
||||
html! {
|
||||
<BrowserRouter>
|
||||
<TitleProvider default_title="mentorenwahl.de" {format_title}>
|
||||
<Switch<routes::Route> render={Switch::render(routes::switch)} />
|
||||
</TitleProvider>
|
||||
</BrowserRouter>
|
||||
<BounceRoot>
|
||||
<HelmetBridge default_title="Mentorenwahl" />
|
||||
<BrowserRouter>
|
||||
<Switch<frontend::routes::Route> render={frontend::routes::switch} />
|
||||
</BrowserRouter>
|
||||
</BounceRoot>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,5 +32,5 @@ fn main() {
|
|||
#[cfg(target_arch = "wasm32")]
|
||||
wasm_logger::init(wasm_logger::Config::default());
|
||||
|
||||
yew::start_app::<App>();
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use graphql_client::reqwest::post_graphql;
|
||||
use yew::prelude::*;
|
||||
use yew_side_effect::title::Title;
|
||||
|
||||
use crate::components;
|
||||
use crate::graphql;
|
||||
|
@ -70,7 +69,7 @@ impl Component for Home {
|
|||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<Title value="Home" />
|
||||
<components::title::Title title="Home" />
|
||||
if self.fetching {
|
||||
<components::fetching::Fetching />
|
||||
} else {
|
||||
|
|
|
@ -218,7 +218,7 @@ impl Component for StudentVote {
|
|||
<p>{ "Noch nicht erlaubt zu wählen." }</p>
|
||||
}
|
||||
} else {
|
||||
let onsubmit = ctx.link().callback(|e: FocusEvent| {
|
||||
let onsubmit = ctx.link().callback(|e: SubmitEvent| {
|
||||
e.prevent_default();
|
||||
|
||||
Msg::Submit
|
||||
|
@ -241,32 +241,34 @@ impl Component for StudentVote {
|
|||
<fieldset>
|
||||
<legend>{ format!("{}. Wahl", i + 1) }</legend>
|
||||
|
||||
{ for self.teachers.iter().enumerate().filter_map(|(j, t)| {
|
||||
let checked = curr_t == Some(&Some(t.id));
|
||||
{
|
||||
for self.teachers.iter().enumerate().filter_map(|(j, t)| {
|
||||
let checked = curr_t == Some(&Some(t.id));
|
||||
|
||||
if teachers.contains(&t.id) && !checked {
|
||||
None
|
||||
} else {
|
||||
let id = format!("{}_{}", name, j);
|
||||
let teacher_id = t.id;
|
||||
let onchange = ctx.link().callback(move |_| Msg::RadioSelect { priority: i, teacher: teacher_id });
|
||||
if teachers.contains(&t.id) && !checked {
|
||||
None
|
||||
} else {
|
||||
let id = format!("{}_{}", name, j);
|
||||
let teacher_id = t.id;
|
||||
let onchange = ctx.link().callback(move |_| Msg::RadioSelect { priority: i, teacher: teacher_id });
|
||||
|
||||
Some(html! {
|
||||
<div>
|
||||
<input
|
||||
type="radio"
|
||||
id={id.to_owned()}
|
||||
name={name.to_owned()}
|
||||
value={t.user.name.to_owned()}
|
||||
required=true
|
||||
{onchange}
|
||||
{checked}
|
||||
/>
|
||||
<label for={id}>{ &t.user.name }</label>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
}) }
|
||||
Some(html! {
|
||||
<div>
|
||||
<input
|
||||
type="radio"
|
||||
id={id.to_owned()}
|
||||
name={name.to_owned()}
|
||||
value={t.user.name.to_owned()}
|
||||
required=true
|
||||
{onchange}
|
||||
{checked}
|
||||
/>
|
||||
<label for={id}>{ &t.user.name }</label>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</fieldset>
|
||||
if i < self.slots - 1 {
|
||||
<br />
|
||||
|
|
|
@ -74,7 +74,7 @@ impl Component for TeacherRegistration {
|
|||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let onsubmit = ctx.link().callback(|e: FocusEvent| {
|
||||
let onsubmit = ctx.link().callback(|e: SubmitEvent| {
|
||||
e.prevent_default();
|
||||
|
||||
Msg::Submit
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
use yew::prelude::*;
|
||||
use yew_side_effect::title::Title;
|
||||
|
||||
use crate::components;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct IndexProps {
|
||||
pub token: Option<String>,
|
||||
pub logged_in: bool,
|
||||
}
|
||||
|
||||
pub struct Index;
|
||||
|
@ -22,13 +20,10 @@ impl Component for Index {
|
|||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<components::logged_in_handler::LoggedInHandler logged_in={ctx.props().logged_in} />
|
||||
|
||||
<Title value="Mentorenwahl" />
|
||||
<div id="wrapper">
|
||||
<section class={classes!("hero", "is-success", "is-fullheight")}>
|
||||
<div class={classes!("hero-head")}>
|
||||
<components::navbar::Navbar token={ctx.props().token.to_owned()} logged_in={ctx.props().logged_in} />
|
||||
<components::navbar::Navbar token={ctx.props().token.to_owned()} />
|
||||
</div>
|
||||
|
||||
<div class={classes!("hero-body")}>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
use yew::prelude::*;
|
||||
|
||||
use crate::components;
|
||||
|
||||
pub struct Info;
|
||||
|
||||
impl Component for Info {
|
||||
type Message = ();
|
||||
type Properties = ();
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<components::title::Title title="Informationen" />
|
||||
<h1>{ "Informationen" }</h1>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +1,30 @@
|
|||
use graphql_client::reqwest::post_graphql;
|
||||
use std::rc::Rc;
|
||||
use web_sys::HtmlInputElement;
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
use yew_side_effect::title::Title;
|
||||
use yewdux::prelude::*;
|
||||
|
||||
use crate::components;
|
||||
use crate::cookies;
|
||||
use crate::graphql;
|
||||
use crate::layouts;
|
||||
use crate::routes;
|
||||
use crate::stores;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct LoginProps {
|
||||
pub token: Option<String>,
|
||||
pub logged_in: bool,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
UpdateLoggedIn(Rc<stores::LoggedIn>),
|
||||
Submit,
|
||||
Login(Option<Vec<String>>),
|
||||
}
|
||||
|
||||
pub struct Login {
|
||||
logged_in_dispatch: Dispatch<stores::LoggedIn>,
|
||||
username: NodeRef,
|
||||
password: NodeRef,
|
||||
fetching: bool,
|
||||
|
@ -32,8 +35,9 @@ impl Component for Login {
|
|||
type Message = Msg;
|
||||
type Properties = LoginProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
Self {
|
||||
logged_in_dispatch: Dispatch::subscribe(ctx.link().callback(Msg::UpdateLoggedIn)),
|
||||
username: NodeRef::default(),
|
||||
password: NodeRef::default(),
|
||||
fetching: false,
|
||||
|
@ -43,6 +47,7 @@ impl Component for Login {
|
|||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::UpdateLoggedIn(_) => false,
|
||||
Msg::Submit => {
|
||||
if let (Some(username), Some(password)) = (
|
||||
self.username.cast::<HtmlInputElement>().map(|x| x.value()),
|
||||
|
@ -66,18 +71,18 @@ impl Component for Login {
|
|||
let data = response.data.unwrap().login.unwrap();
|
||||
|
||||
wasm_cookies::set(
|
||||
cookies::TOKEN,
|
||||
cookies::names::TOKEN,
|
||||
&data.token,
|
||||
&wasm_cookies::CookieOptions::default(),
|
||||
&cookies::DEFAULT_COOKIE_OPTIONS,
|
||||
);
|
||||
if data.user.admin {
|
||||
wasm_cookies::set(
|
||||
cookies::ADMIN,
|
||||
cookies::names::ADMIN,
|
||||
"1",
|
||||
&wasm_cookies::CookieOptions::default(),
|
||||
&cookies::DEFAULT_COOKIE_OPTIONS,
|
||||
);
|
||||
} else {
|
||||
wasm_cookies::delete(cookies::ADMIN);
|
||||
wasm_cookies::delete(cookies::names::ADMIN);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,7 +101,8 @@ impl Component for Login {
|
|||
self.fetching = false;
|
||||
self.errors = errors;
|
||||
if self.errors.is_none() {
|
||||
ctx.link().history().unwrap().push(routes::Route::Home);
|
||||
self.logged_in_dispatch.reduce_mut(|x| x.0 = true);
|
||||
ctx.link().navigator().unwrap().push(&routes::Route::Home);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
|
@ -106,17 +112,13 @@ impl Component for Login {
|
|||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
if ctx.props().logged_in {
|
||||
ctx.link().history().unwrap().push(routes::Route::Home);
|
||||
}
|
||||
|
||||
html! {
|
||||
<>
|
||||
<Title value="Login" />
|
||||
<layouts::main::Main token={ctx.props().token.to_owned()} logged_in={false}>
|
||||
<div class={classes!("columns")}>
|
||||
<div class={classes!("column", "is-one-third", "mx-auto")}>
|
||||
<form onsubmit={ctx.link().callback(|e: FocusEvent| { e.prevent_default(); Msg::Submit })} class={classes!("box")}>
|
||||
<components::title::Title title="Login" />
|
||||
<layouts::main::Main token={ctx.props().token.to_owned()}>
|
||||
<div class={classes!("columns", "is-centered")}>
|
||||
<div class={classes!("column", "is-half")}>
|
||||
<form onsubmit={ctx.link().callback(|e: SubmitEvent| { e.prevent_default(); Msg::Submit })} class={classes!("box")}>
|
||||
<div class={classes!("field")}>
|
||||
<p class={classes!("control", "has-icons-left")}>
|
||||
<input ref={self.username.clone()}
|
||||
|
|
|
@ -6,6 +6,7 @@ use crate::layouts;
|
|||
|
||||
pub mod home;
|
||||
pub mod index;
|
||||
pub mod info;
|
||||
pub mod login;
|
||||
pub mod not_found;
|
||||
pub mod settings;
|
||||
|
@ -27,28 +28,27 @@ pub enum Route {
|
|||
NotFound,
|
||||
}
|
||||
|
||||
pub fn switch(routes: &Route) -> Html {
|
||||
pub fn switch(routes: Route) -> Html {
|
||||
let token = {
|
||||
let tmp = wasm_cookies::get(cookies::TOKEN);
|
||||
let tmp = wasm_cookies::get(cookies::names::TOKEN);
|
||||
if let Some(x) = tmp {
|
||||
if let Ok(y) = x {
|
||||
Some(y)
|
||||
} else {
|
||||
wasm_cookies::delete(cookies::TOKEN);
|
||||
wasm_cookies::delete(cookies::names::TOKEN);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
let logged_in = token.is_some();
|
||||
let admin = {
|
||||
let tmp = wasm_cookies::get(cookies::ADMIN);
|
||||
let tmp = wasm_cookies::get(cookies::names::ADMIN);
|
||||
if let Some(x) = tmp {
|
||||
if let Ok(_) = x {
|
||||
true
|
||||
} else {
|
||||
wasm_cookies::delete(cookies::ADMIN);
|
||||
wasm_cookies::delete(cookies::names::ADMIN);
|
||||
false
|
||||
}
|
||||
} else {
|
||||
|
@ -59,13 +59,13 @@ pub fn switch(routes: &Route) -> Html {
|
|||
match routes {
|
||||
Route::Index => {
|
||||
html! {
|
||||
<index::Index token={token.to_owned()} {logged_in} />
|
||||
<index::Index token={token.to_owned()}/>
|
||||
}
|
||||
}
|
||||
Route::Home => {
|
||||
html! {
|
||||
<layouts::logged_in::LoggedIn {logged_in}>
|
||||
<layouts::main::Main token={token.to_owned()} {logged_in}>
|
||||
<layouts::logged_in::LoggedIn>
|
||||
<layouts::main::Main token={token.to_owned()}>
|
||||
<home::Home {token} />
|
||||
</layouts::main::Main>
|
||||
</layouts::logged_in::LoggedIn>
|
||||
|
@ -73,8 +73,8 @@ pub fn switch(routes: &Route) -> Html {
|
|||
}
|
||||
Route::Settings => {
|
||||
html! {
|
||||
<layouts::logged_in::LoggedIn {logged_in}>
|
||||
<layouts::main::Main token={token.to_owned()} {logged_in}>
|
||||
<layouts::logged_in::LoggedIn>
|
||||
<layouts::main::Main token={token.to_owned()}>
|
||||
<settings::Settings {token} {admin} />
|
||||
</layouts::main::Main>
|
||||
</layouts::logged_in::LoggedIn>
|
||||
|
@ -82,16 +82,16 @@ pub fn switch(routes: &Route) -> Html {
|
|||
}
|
||||
Route::Info => {
|
||||
html! {
|
||||
<layouts::main::Main token={token.to_owned()} {logged_in}>
|
||||
<h1>{ "Informationen" }</h1>
|
||||
<layouts::main::Main token={token.to_owned()}>
|
||||
<info::Info />
|
||||
</layouts::main::Main>
|
||||
}
|
||||
}
|
||||
Route::Login => html! {
|
||||
<login::Login {token} {logged_in} />
|
||||
<login::Login {token} />
|
||||
},
|
||||
Route::NotFound => html! {
|
||||
<not_found::NotFound {token} {logged_in} />
|
||||
<not_found::NotFound {token} />
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
use yew::prelude::*;
|
||||
use yew_side_effect::title::Title;
|
||||
|
||||
use crate::components;
|
||||
use crate::layouts;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct NotFoundProps {
|
||||
pub token: Option<String>,
|
||||
pub logged_in: bool,
|
||||
}
|
||||
|
||||
pub struct NotFound;
|
||||
|
@ -22,8 +21,8 @@ impl Component for NotFound {
|
|||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<Title value="404 Not found" />
|
||||
<layouts::main::Main token={ctx.props().token.to_owned()} logged_in={ctx.props().logged_in}>
|
||||
<components::title::Title title="404 Not found" />
|
||||
<layouts::main::Main token={ctx.props().token.to_owned()}>
|
||||
<h1>{ "404" }</h1>
|
||||
</layouts::main::Main>
|
||||
</>
|
||||
|
|
|
@ -54,25 +54,29 @@ impl Component for Assignments {
|
|||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<fieldset>
|
||||
<fieldset class={classes!("fieldset")}>
|
||||
<legend>{ "Zuweisungen" }</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{ "Aktion" }</th>
|
||||
<th>{ "Optionen" }</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{ "Zuweisung starten" }</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>
|
||||
<button onclick={ctx.link().callback(|_| Msg::StartAssignment)}>
|
||||
{ "Ausführen" }
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<table class={classes!("table")}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{ "Aktion" }</th>
|
||||
<th>{ "Optionen" }</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{ "Zuweisung starten" }</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>
|
||||
<button class={classes!("button")} onclick={ctx.link().callback(|_| Msg::StartAssignment)}>
|
||||
{ "Ausführen" }
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<components::graphql_errors::GraphQLErrors errors={self.errors.to_owned()} />
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
use yew::prelude::*;
|
||||
use yew_side_effect::title::Title;
|
||||
|
||||
use crate::components;
|
||||
|
||||
pub mod assignments;
|
||||
pub mod new_user_modal;
|
||||
pub mod tokens;
|
||||
pub mod users;
|
||||
|
||||
#[derive(Properties, PartialEq, Eq)]
|
||||
pub struct SettingsProps {
|
||||
|
@ -23,15 +26,18 @@ impl Component for Settings {
|
|||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<Title value="Settings" />
|
||||
<section>
|
||||
<tokens::Tokens token={ctx.props().token.as_ref().unwrap().to_owned()} />
|
||||
<components::title::Title title="Settings" />
|
||||
<section class={classes!("block")}>
|
||||
<tokens::Tokens token={ctx.props().token.as_ref().unwrap().to_owned()} />
|
||||
</section>
|
||||
if ctx.props().admin {
|
||||
<section class={classes!("block")}>
|
||||
<assignments::Assignments token={ctx.props().token.as_ref().unwrap().to_owned()} />
|
||||
</section>
|
||||
if ctx.props().admin {
|
||||
<section>
|
||||
<assignments::Assignments token={ctx.props().token.as_ref().unwrap().to_owned()} />
|
||||
</section>
|
||||
}
|
||||
<section class={classes!("block")}>
|
||||
<users::Users token={ctx.props().token.as_ref().unwrap().to_owned()} />
|
||||
</section>
|
||||
}
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
use yew::prelude::*;
|
||||
|
||||
pub enum Msg {
|
||||
CloseModal,
|
||||
Save,
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct NewUserModalProps {
|
||||
pub close: Callback<()>,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
pub struct NewUserModal {
|
||||
username: NodeRef,
|
||||
admin: NodeRef,
|
||||
}
|
||||
|
||||
impl Component for NewUserModal {
|
||||
type Message = Msg;
|
||||
type Properties = NewUserModalProps;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
Self {
|
||||
username: NodeRef::default(),
|
||||
admin: NodeRef::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::CloseModal => {
|
||||
ctx.props().close.emit(());
|
||||
false
|
||||
}
|
||||
Msg::Save => {
|
||||
log::debug!("self.username = {:?}", self.username);
|
||||
log::debug!("self.admin = {:?}", self.admin);
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let close = ctx.link().callback(|_| Msg::CloseModal);
|
||||
|
||||
html! {
|
||||
<div class={classes!("modal", if ctx.props().active { Some("is-active") } else { None })}>
|
||||
<div onclick={close.clone()}
|
||||
class={classes!("modal-background")}
|
||||
/>
|
||||
<div class={classes!("modal-card")}>
|
||||
<header class={classes!("modal-card-head")}>
|
||||
<p class={classes!("modal-card-title")}>{ "Neuer Benutzer" }</p>
|
||||
<button onclick={close.clone()} class={classes!("delete")} aria-label="close" />
|
||||
</header>
|
||||
|
||||
<section class={classes!("modal-card-body")}>
|
||||
<form onsubmit={ctx.link().callback(|e: SubmitEvent| { e.prevent_default(); Msg::Save })}>
|
||||
<div class={classes!("field")}>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<footer class={classes!("modal-card-foot")}>
|
||||
<button onclick={close} class={classes!("button")}>{ "Abbrechen" }</button>
|
||||
<button class={classes!("button", "is-success")}>{ "Speichern" }</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,11 +6,11 @@ use crate::graphql;
|
|||
|
||||
pub enum Msg {
|
||||
DoneFetching {
|
||||
errors: Option<Vec<String>>,
|
||||
errors: graphql::Errors,
|
||||
tokens: Vec<graphql::queries::tokens::tokens::TokensTokens>,
|
||||
},
|
||||
Revoke(usize),
|
||||
RevokeDone(Option<Vec<String>>),
|
||||
RevokeDone(graphql::Errors),
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq, Eq)]
|
||||
|
@ -20,7 +20,7 @@ pub struct TokensProps {
|
|||
|
||||
pub struct Tokens {
|
||||
fetching: bool,
|
||||
errors: Option<Vec<String>>,
|
||||
errors: graphql::Errors,
|
||||
tokens: Option<Vec<graphql::queries::tokens::tokens::TokensTokens>>,
|
||||
}
|
||||
|
||||
|
@ -40,7 +40,7 @@ impl Component for Tokens {
|
|||
.unwrap();
|
||||
|
||||
Msg::DoneFetching {
|
||||
errors: None,
|
||||
errors: graphql::convert(response.errors),
|
||||
tokens: response.data.unwrap().tokens.unwrap(),
|
||||
}
|
||||
});
|
||||
|
@ -92,7 +92,7 @@ impl Component for Tokens {
|
|||
<fieldset class={classes!("fieldset")}>
|
||||
<legend>{ "Tokens" }</legend>
|
||||
if self.fetching {
|
||||
<p>{ "Fetching..." }</p>
|
||||
<components::fetching::Fetching />
|
||||
} else {
|
||||
<table class={classes!("table")}>
|
||||
<thead>
|
||||
|
@ -104,20 +104,20 @@ impl Component for Tokens {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ for self.tokens.as_ref().unwrap().iter().enumerate().map(|(i, t)| {
|
||||
html! {
|
||||
{
|
||||
for self.tokens.as_ref().unwrap().iter().enumerate().map(|(i, t)| html! {
|
||||
<tr>
|
||||
<th><code>{ &t.id }</code></th>
|
||||
<td><code>{ &t.iat }</code></td>
|
||||
<td><code>{ &t.exp }</code></td>
|
||||
<td>
|
||||
<button onclick={ctx.link().callback(move |_| Msg::Revoke(i))}>
|
||||
<button class={classes!("button")} onclick={ctx.link().callback(move |_| Msg::Revoke(i))}>
|
||||
{ "Revoke" }
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}) }
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
|
|
@ -0,0 +1,329 @@
|
|||
use graphql_client::reqwest::post_graphql;
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components;
|
||||
use crate::graphql;
|
||||
use crate::routes::settings::new_user_modal;
|
||||
|
||||
pub enum Msg {
|
||||
DoneFetching {
|
||||
errors: graphql::Errors,
|
||||
students: Option<Vec<graphql::queries::users_by_role::students::StudentsStudents>>,
|
||||
teachers: Option<Vec<graphql::queries::users_by_role::teachers::TeachersTeachers>>,
|
||||
},
|
||||
SwitchTab(UsersTab),
|
||||
OpenModal,
|
||||
CloseModal,
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq, Eq)]
|
||||
pub struct UsersProps {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
pub enum UsersTab {
|
||||
All,
|
||||
Students,
|
||||
Teachers,
|
||||
}
|
||||
|
||||
pub struct Users {
|
||||
tab: UsersTab,
|
||||
fetching: bool,
|
||||
errors: graphql::Errors,
|
||||
students: Option<Vec<graphql::queries::users_by_role::students::StudentsStudents>>,
|
||||
teachers: Option<Vec<graphql::queries::users_by_role::teachers::TeachersTeachers>>,
|
||||
modal_active: bool,
|
||||
}
|
||||
|
||||
impl Component for Users {
|
||||
type Message = Msg;
|
||||
type Properties = UsersProps;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let client = graphql::client(Some(&ctx.props().token)).unwrap();
|
||||
ctx.link().send_future(async move {
|
||||
let students_response = post_graphql::<graphql::queries::users_by_role::Students, _>(
|
||||
&client,
|
||||
graphql::URL.as_str(),
|
||||
graphql::queries::users_by_role::students::Variables,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let teachers_response = post_graphql::<graphql::queries::users_by_role::Teachers, _>(
|
||||
&client,
|
||||
graphql::URL.as_str(),
|
||||
graphql::queries::users_by_role::teachers::Variables,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let x = [
|
||||
students_response.errors.map_or_else(|| vec![], |e| e),
|
||||
teachers_response.errors.map_or_else(|| vec![], |e| e),
|
||||
]
|
||||
.concat();
|
||||
|
||||
if x.is_empty() {
|
||||
Msg::DoneFetching {
|
||||
errors: None,
|
||||
students: Some(students_response.data.unwrap().students.unwrap()),
|
||||
teachers: Some(teachers_response.data.unwrap().teachers),
|
||||
}
|
||||
} else {
|
||||
Msg::DoneFetching {
|
||||
errors: graphql::convert(Some(x)),
|
||||
students: None,
|
||||
teachers: None,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
tab: UsersTab::All,
|
||||
fetching: true,
|
||||
errors: None,
|
||||
students: None,
|
||||
teachers: None,
|
||||
modal_active: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::DoneFetching {
|
||||
errors,
|
||||
students,
|
||||
teachers,
|
||||
} => {
|
||||
self.fetching = false;
|
||||
self.errors = errors;
|
||||
self.students = students;
|
||||
self.teachers = teachers;
|
||||
|
||||
true
|
||||
}
|
||||
Msg::SwitchTab(tab) => {
|
||||
if self.tab == tab {
|
||||
false
|
||||
} else {
|
||||
self.tab = tab;
|
||||
true
|
||||
}
|
||||
}
|
||||
Msg::OpenModal => {
|
||||
self.modal_active = true;
|
||||
true
|
||||
}
|
||||
Msg::CloseModal => {
|
||||
self.modal_active = false;
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<fieldset class={classes!("fieldset")}>
|
||||
<legend>{ "Benutzer" }</legend>
|
||||
<div class={classes!("tabs")}>
|
||||
<ul>
|
||||
<li class={classes!(if self.tab == UsersTab::All { Some("is-active") } else { None })}>
|
||||
<a onclick={ctx.link().callback(|_| Msg::SwitchTab(UsersTab::All))}>{ "Alle" }</a>
|
||||
</li>
|
||||
<li class={classes!(if self.tab == UsersTab::Students { Some("is-active") } else { None })}>
|
||||
<a onclick={ctx.link().callback(|_| Msg::SwitchTab(UsersTab::Students))}>{ "Schüler" }</a>
|
||||
</li>
|
||||
<li class={classes!(if self.tab == UsersTab::Teachers { Some("is-active") } else { None })}>
|
||||
<a onclick={ctx.link().callback(|_| Msg::SwitchTab(UsersTab::Teachers))}>{ "Lehrer" }</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
if self.fetching {
|
||||
<components::fetching::Fetching />
|
||||
} else {
|
||||
<div>
|
||||
{
|
||||
match self.tab {
|
||||
UsersTab::All => html! {
|
||||
<table class={classes!("table")}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><abbr title="ID des Benutzers in der Datenbank / API">{ "ID" }</abbr></th>
|
||||
<th>{ "Nachname" }</th>
|
||||
<th>{ "Vorname" }</th>
|
||||
<th><abbr title="LDAP Benutzername">{ "Benutzername" }</abbr></th>
|
||||
<th>{ "Email" }</th>
|
||||
<th>{ "Rolle" }</th>
|
||||
<th><abbr title="ID des externen Benutzerobjekts">{ "Externe Rollen-ID" }</abbr></th>
|
||||
<th>{ "Admin" }</th>
|
||||
<th><abbr title="Wenn Schüler, dann ob Lehrer gewählt wurde">{ "Gewählt" }</abbr></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
for self.students.as_ref().unwrap().iter().map(|s| html! {
|
||||
<tr>
|
||||
<td><code>{ &s.user.id }</code></td>
|
||||
<td>{ &s.user.last_name }</td>
|
||||
<td>{ &s.user.first_name }</td>
|
||||
<td><code>{ &s.user.username }</code></td>
|
||||
<td>
|
||||
<a href={format!("mailto:{}", s.user.email)}>
|
||||
<code>{ &s.user.email }</code>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<code>
|
||||
{
|
||||
match &s.user.role {
|
||||
graphql::queries::users_by_role::students::UserRole::Student => "S",
|
||||
graphql::queries::users_by_role::students::UserRole::Teacher => "T",
|
||||
graphql::queries::users_by_role::students::UserRole::Other(_) => "N/A",
|
||||
}
|
||||
}
|
||||
</code>
|
||||
</td>
|
||||
<td><code>{ &s.id }</code></td>
|
||||
<td><code>{ if s.user.admin { 1 } else { 0 } }</code></td>
|
||||
<td><code>{ if s.vote.is_some() { 1 } else { 0 } }</code></td>
|
||||
</tr>
|
||||
})
|
||||
}
|
||||
{
|
||||
for self.teachers.as_ref().unwrap().iter().map(|t| html! {
|
||||
<tr>
|
||||
<td><code>{ &t.user.id }</code></td>
|
||||
<td>{ &t.user.last_name }</td>
|
||||
<td>{ &t.user.first_name }</td>
|
||||
<td><code>{ &t.user.username }</code></td>
|
||||
<td>
|
||||
<a href={format!("mailto:{}", t.user.email)}>
|
||||
<code>{ &t.user.email }</code>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<code>
|
||||
{
|
||||
match &t.user.role {
|
||||
graphql::queries::users_by_role::teachers::UserRole::Student => "S",
|
||||
graphql::queries::users_by_role::teachers::UserRole::Teacher => "T",
|
||||
graphql::queries::users_by_role::teachers::UserRole::Other(_) => "N/A",
|
||||
}
|
||||
}
|
||||
</code>
|
||||
</td>
|
||||
<td><code>{ &t.id }</code></td>
|
||||
<td><code>{ if t.user.admin { 1 } else { 0 } }</code></td>
|
||||
<td><code>{ "N/A" }</code></td>
|
||||
</tr>
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
},
|
||||
UsersTab::Students => html! {
|
||||
<table class={classes!("table")}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><abbr title="ID des Schülers in der Datenbank / API">{ "ID" }</abbr></th>
|
||||
<th>{ "Nachname" }</th>
|
||||
<th>{ "Vorname" }</th>
|
||||
<th><abbr title="LDAP Benutzername">{ "Benutzername" }</abbr></th>
|
||||
<th>{ "Email" }</th>
|
||||
<th>{ "Benutzer-ID" }</th>
|
||||
<th>{ "Admin" }</th>
|
||||
<th>{ "Gewählt" }</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
for self.students.as_ref().unwrap().iter().map(|s| html! {
|
||||
<tr>
|
||||
<td><code>{ &s.id }</code></td>
|
||||
<td>{ &s.user.last_name }</td>
|
||||
<td>{ &s.user.first_name }</td>
|
||||
<td><code>{ &s.user.username }</code></td>
|
||||
<td>
|
||||
<a href={format!("mailto:{}", s.user.email)}>
|
||||
<code>{ &s.user.email }</code>
|
||||
</a>
|
||||
</td>
|
||||
<td><code>{ &s.user.id }</code></td>
|
||||
<td><code>{ if s.user.admin { 1 } else { 0 } }</code></td>
|
||||
<td><code>{ if s.vote.is_some() { 1 } else { 0 } }</code></td>
|
||||
</tr>
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
},
|
||||
UsersTab::Teachers => html! {
|
||||
<table class={classes!("table")}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><abbr title="ID des Lehrers in der Datenbank / API">{ "ID" }</abbr></th>
|
||||
<th>{ "Nachname" }</th>
|
||||
<th>{ "Vorname" }</th>
|
||||
<th><abbr title="LDAP Benutzername">{ "Benutzername" }</abbr></th>
|
||||
<th>{ "Email" }</th>
|
||||
<th>{ "Benutzer-ID" }</th>
|
||||
<th>{ "Admin" }</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
for self.teachers.as_ref().unwrap().iter().map(|t| html! {
|
||||
<tr>
|
||||
<td><code>{ &t.id }</code></td>
|
||||
<td>{ &t.user.last_name }</td>
|
||||
<td>{ &t.user.first_name }</td>
|
||||
<td><code>{ &t.user.username }</code></td>
|
||||
<td>
|
||||
<a href={format!("mailto:{}", t.user.email)}>
|
||||
<code>{ &t.user.email }</code>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<code>
|
||||
{
|
||||
match &t.user.role {
|
||||
graphql::queries::users_by_role::teachers::UserRole::Student => "S",
|
||||
graphql::queries::users_by_role::teachers::UserRole::Teacher => "T",
|
||||
graphql::queries::users_by_role::teachers::UserRole::Other(_) => "N/A",
|
||||
}
|
||||
}
|
||||
</code>
|
||||
</td>
|
||||
<td><code>{ &t.user.id }</code></td>
|
||||
<td><code>{ if t.user.admin { 1 } else { 0 } }</code></td>
|
||||
</tr>
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
<button onclick={ctx.link().callback(|_| Msg::OpenModal)}
|
||||
class={classes!("button", "is-success", "is-fullwidth")}
|
||||
>
|
||||
<span class={classes!("icon")}>
|
||||
<i class={classes!("fas", "fa-user-plus")} />
|
||||
</span>
|
||||
<span>{ "Neuer Benutzer" }</span>
|
||||
</button>
|
||||
|
||||
<new_user_modal::NewUserModal close={ctx.link().callback(|_| Msg::CloseModal)}
|
||||
active={self.modal_active}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<components::graphql_errors::GraphQLErrors errors={self.errors.to_owned()} />
|
||||
}
|
||||
</fieldset>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
use yewdux::prelude::*;
|
||||
|
||||
use crate::cookies;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Store)]
|
||||
pub struct LoggedIn(pub bool);
|
||||
|
||||
impl Default for LoggedIn {
|
||||
fn default() -> Self {
|
||||
let token = {
|
||||
let tmp = wasm_cookies::get(cookies::names::TOKEN);
|
||||
if let Some(x) = tmp {
|
||||
if let Ok(y) = x {
|
||||
Some(y)
|
||||
} else {
|
||||
wasm_cookies::delete(cookies::names::TOKEN);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
Self(token.is_some())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Store)]
|
||||
pub struct Token(pub String);
|
|
@ -16,4 +16,4 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
docker-compose exec backend backend "$@"
|
||||
docker compose exec backend backend "$@"
|
||||
|
|
|
@ -16,4 +16,4 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
docker-compose exec backend micrate "$@"
|
||||
docker compose exec backend micrate "$@"
|
||||
|
|
Loading…
Reference in New Issue