edc-web-ui 0.2.1

A Rust Web interface with patternfly-yew client for EDC
Documentation
#[cfg(not(target_arch = "wasm32"))]
fn main() {
  println!("edc-web-ui only target web (wasm32)");
}

#[cfg(target_arch = "wasm32")]
fn main() {
  use main_application::MainApplication;

  std::panic::set_hook(Box::new(console_error_panic_hook::hook));

  fern::Dispatch::new()
    .level(log::LevelFilter::Info)
    .chain(fern::Output::call(console_log::log))
    .apply()
    .unwrap();

  yew::Renderer::<MainApplication>::new().render();
}

#[cfg(target_arch = "wasm32")]
mod main_application {
  use edc_web_ui::{contexts::EdcConnectorContextProvider, pages::*};
  use patternfly_yew::prelude::*;
  use serde::Deserialize;
  use wasm_cookies::CookieOptions;
  use yew::platform::spawn_local;
  use yew::prelude::*;
  use yew_nested_router::prelude::Switch as RouterSwitch;
  use yew_nested_router::{Router, Target};

  #[derive(Debug, Clone, Copy, PartialEq, Target, Eq)]
  enum AppRoute {
    #[target(rename = "assets")]
    Assets,
    #[target(rename = "policies")]
    Policies,
    #[target(rename = "contract-definitions")]
    ContractDefinitions,
    #[target(rename = "contract-negotiations")]
    ContractNegotiations,
    #[target(rename = "contract-agreements")]
    ContractAgreements,
    #[target(rename = "transfer-processes")]
    TransferProcesses,
  }

  #[derive(Clone, Debug, Deserialize, PartialEq)]
  struct Configuration {
    management_url: String,
    api_key: Option<String>,
  }

  #[component]
  pub fn MainApplication() -> Html {
    let management_url = use_state(|| {
      if let Some(Ok(management_url)) = wasm_cookies::get("EDC_CONNECTOR_MANAGEMENT_URL") {
        management_url
      } else {
        "".to_string()
      }
    });

    let api_key = use_state(|| {
      if let Some(Ok(api_key)) = wasm_cookies::get("EDC_CONNECTOR_API_KEY") {
        api_key
      } else {
        "".to_string()
      }
    });

    let validated = use_state(|| !(*management_url).is_empty() && !(*api_key).is_empty());

    {
      let management_url = management_url.clone();
      let api_key = api_key.clone();
      let validated = validated.clone();

      use_effect(move || {
        let management_url = management_url.clone();
        let api_key = api_key.clone();
        let validated = validated.clone();

        spawn_local(async move {
          let server_url = web_sys::window().unwrap().location().origin().unwrap();

          if let Ok(response) = reqwest::get(format!("{server_url}/configuration.json")).await {
            if let Ok(configuration) = response.json::<Configuration>().await {
              log::warn!("Configuration: {:?}", configuration);

              management_url.set(format!("{server_url}{}", configuration.management_url));
              if let Some(configuration_api_key) = configuration.api_key {
                api_key.set(configuration_api_key);
              }

              validated.set(true);
            }
          }
        })
      });
    }

    let onchange_management_url =
      use_callback(management_url.clone(), |value: String, management_url| {
        management_url.set(value);
      });

    let onchange_api_key = use_callback(api_key.clone(), |value: String, api_key| {
      api_key.set(value);
    });

    let onsubmit = use_callback(
      (management_url.clone(), api_key.clone(), validated.clone()),
      |event: SubmitEvent, (management_url, api_key, validated)| {
        event.prevent_default();

        wasm_cookies::set(
          "EDC_CONNECTOR_MANAGEMENT_URL",
          &**management_url,
          &CookieOptions::default().with_path("/"),
        );

        wasm_cookies::set(
          "EDC_CONNECTOR_API_KEY",
          &**api_key,
          &CookieOptions::default().with_path("/"),
        );

        validated.set(true);
      },
    );

    let onlogout = use_callback(validated.setter(), |_, validated_setter| {
      validated_setter.set(false);
    });

    if *validated {
      let management_url = (*management_url).clone();
      let api_key = (*api_key).clone();

      let api_key = if api_key.is_empty() {
        None
      } else {
        Some(api_key)
      };

      html!(
        <Router<AppRoute> default={AppRoute::Assets}>
          <EdcConnectorContextProvider {management_url} {api_key}>
            <BackdropViewer>
              <MainView {onlogout} />
            </BackdropViewer>
          </EdcConnectorContextProvider>
        </Router<AppRoute>>
      )
    } else {
      html!(
        <>
          <Background
            style="logo.png"
            additional_style="background-size: 200px 200px; background-position: calc(100vw - 220px) calc(100vh - 220px); background-color: var(--pf-t--global--background--color--secondary--default);"
          />
          <Login>
            <Card>
              <CardHeader>
                <Title level={Level::H3} size={Size::XXLarge}>{ "EDC Connector Management" }</Title>
              </CardHeader>
              <CardBody>
                <Form {onsubmit}>
                  <FormGroup label="Management URL" required=true>
                    <TextInput
                      required=true
                      value={(*management_url).to_string()}
                      onchange={onchange_management_url}
                    />
                  </FormGroup>
                  <FormGroup label="API Key">
                    <TextInput
                      value={(*api_key).to_string()}
                      onchange={onchange_api_key}
                      r#type={TextInputType::Password}
                    />
                  </FormGroup>
                  <ActionGroup>
                    <Button
                      variant={ButtonVariant::Primary}
                      label="Submit"
                      r#type={ButtonType::Submit}
                    />
                    <Button
                      variant={ButtonVariant::Secondary}
                      label="Reset"
                      r#type={ButtonType::Reset}
                    />
                  </ActionGroup>
                </Form>
              </CardBody>
              <CardFooter>
                <Bullseye>
                  <ToggleTheme />
                </Bullseye>
              </CardFooter>
            </Card>
          </Login>
        </>
      )
    }
  }

  #[derive(Properties, PartialEq, Clone)]
  pub struct MainViewProps {
    pub onlogout: Callback<()>,
  }

  #[component]
  pub fn MainView(props: &MainViewProps) -> Html {
    let brand = html!(
      <>
        <img src="./logo.png" style="height: 25px !important; margin-right: 10px" />
        <Title level={Level::H3} size={Size::XXLarge}>{ "EDC Web UI" }</Title>
      </>
    );

    let sidebar = html_nested!(
      <PageSidebar>
        <Nav>
          <NavList>
            <NavRouterItem<AppRoute> to={AppRoute::Assets}>{ "Assets" }</NavRouterItem<AppRoute>>
            <NavRouterItem<AppRoute> to={AppRoute::Policies}>
              { "Policies" }
            </NavRouterItem<AppRoute>>
            <NavRouterItem<AppRoute> to={AppRoute::ContractDefinitions}>
              { "Contract Definitions" }
            </NavRouterItem<AppRoute>>
            <NavRouterItem<AppRoute> to={AppRoute::ContractNegotiations}>
              { "Contract Negotiations" }
            </NavRouterItem<AppRoute>>
            <NavRouterItem<AppRoute> to={AppRoute::ContractAgreements}>
              { "Contract Agreements" }
            </NavRouterItem<AppRoute>>
            <NavRouterItem<AppRoute> to={AppRoute::TransferProcesses}>
              { "Transfer Processes" }
            </NavRouterItem<AppRoute>>
          </NavList>
        </Nav>
      </PageSidebar>
    );

    let onlogout = use_callback(props.onlogout.clone(), |_, onlogout| {
      wasm_cookies::set(
        "EDC_CONNECTOR_API_KEY",
        "",
        &CookieOptions::default()
          .with_path("/")
          .expires_at_timestamp(0),
      );
      onlogout.emit(());
    });

    let tools = html!(
      <Toolbar>
        <ToolbarContent>
          <ToolbarGroup
            modifiers={ToolbarElementModifier::End.all()}
            variant={GroupVariant::IconButton}
          >
            <ToolbarItem>
              <ToggleTheme />
            </ToolbarItem>
            <ToolbarItem>
              <Button variant={ButtonVariant::Primary} onclick={onlogout}>{ "Logout" }</Button>
            </ToolbarItem>
          </ToolbarGroup>
        </ToolbarContent>
      </Toolbar>
    );

    html!(
      <Page {brand} {sidebar} {tools} full_height=true>
        <PageSection>
          <RouterSwitch<AppRoute> render={switch_app_route} />
        </PageSection>
      </Page>
    )
  }

  fn switch_app_route(target: AppRoute) -> Html {
    match target {
      AppRoute::Assets => html! { <AssetPage /> },
      AppRoute::Policies => html! { <PolicyPage /> },
      AppRoute::ContractDefinitions => html! { <ContractDefinitionPage /> },
      AppRoute::ContractNegotiations => html! { <ContractNegotiationPage /> },
      AppRoute::ContractAgreements => html! { <ContractAgreementPage /> },
      AppRoute::TransferProcesses => html! { <TransferProcessPage /> },
    }
  }

  #[cfg(target_arch = "wasm32")]
  static COOKIE_NAME: &str = "X-EDC-Web-UI-Theme";
  #[cfg(target_arch = "wasm32")]
  static DARK_THEME_VALUE: &str = "edc-web-ui-dark";

  #[component]
  pub fn ToggleTheme() -> Html {
    let is_dark_theme = use_state(get_is_dark_theme_from_cookie);
    let on_theme_switch = use_callback(is_dark_theme.setter(), |_, is_dark_theme| {
      toggle_theme();
      is_dark_theme.set(get_is_dark_theme_from_cookie());
    });

    use_effect_with((), |_| {
      init_page_dark_light_theme();
    });

    let label = if *is_dark_theme {
      "Dark Theme"
    } else {
      "Light Theme"
    };

    html!(<Switch onchange={on_theme_switch} checked={get_is_dark_theme_from_cookie()} {label} />)
  }

  pub(crate) fn get_is_dark_theme_from_cookie() -> bool {
    #[cfg(target_arch = "wasm32")]
    if let Some(Ok(value)) = wasm_cookies::get(COOKIE_NAME) {
      value == DARK_THEME_VALUE
    } else {
      false
    }

    #[cfg(not(target_arch = "wasm32"))]
    false
  }

  pub fn toggle_theme() {
    #[cfg(target_arch = "wasm32")]
    wasm_cookies::set(
      COOKIE_NAME,
      if get_is_dark_theme_from_cookie() {
        "li-light"
      } else {
        DARK_THEME_VALUE
      },
      &wasm_cookies::CookieOptions::default(),
    );

    init_page_dark_light_theme();
  }

  pub fn init_page_dark_light_theme() {
    let document_element = gloo_utils::document_element();

    let class = if get_is_dark_theme_from_cookie() {
      "pf-v6-theme-dark"
    } else {
      Default::default()
    };

    document_element.set_class_name(class);
  }
}