edc-web-components 0.6.0

A Rust Web Components with patternfly-yew client for EDC
Documentation
use crate::components::{AssetReference, ContractAgreementReference, DidLabel, MultiStateSelector};
use crate::models::{ConsumerProvider, ContractNegotiationItem};
use patternfly_yew::prelude::*;
use std::collections::HashSet;
use std::rc::Rc;
use yew::prelude::*;

#[derive(Clone, Debug, PartialEq, Properties)]
pub struct ListContractNegotiationsProps {
  pub contract_negotiation_items: Vec<ContractNegotiationItem>,
  pub offset: usize,
  pub limit: usize,
  pub statuses: Vec<(String, bool)>,
  pub on_offset: Callback<usize>,
  pub on_limit: Callback<usize>,
  pub on_show_contract_negotiation: Callback<String>,
  pub on_statuses: Callback<Vec<(String, bool)>>,
  #[prop_or(true)]
  pub show_status_selector: bool,
  #[prop_or_default]
  pub consumer_provider: HashSet<ConsumerProvider>,
  pub on_consumer_provider: Callback<HashSet<ConsumerProvider>>,
}

#[component]
pub fn ListContractNegotiations(props: &ListContractNegotiationsProps) -> Html {
  let header = html_nested! {
    <TableHeader<Columns>>
      <TableColumn<Columns> label="State" index={Columns::State} />
      <TableColumn<Columns> label="Contract Agreement" index={Columns::ContractAgreementId} />
      <TableColumn<Columns> label="Counter Party" index={Columns::CounterParty} />
      <TableColumn<Columns> label="Asset" index={Columns::Asset} />
      <TableColumn<Columns> label="Kind" index={Columns::Kind} />
      <TableColumn<Columns> label="" index={Columns::Actions} />
    </TableHeader<Columns>>
  };

  let total_entries: Option<usize> = None;

  let nav_callback = use_callback(
    (
      props.offset,
      props.limit,
      total_entries,
      props.on_offset.clone(),
    ),
    |page: Navigation, (offset, limit, total_entries, on_offset)| {
      let offset = match page {
        Navigation::First => 0,
        Navigation::Last => (total_entries.unwrap_or_default().saturating_sub(1) / limit) * limit,
        Navigation::Previous => *offset - limit,
        Navigation::Next => *offset + limit,
        Navigation::Page(n) => n * limit,
      };
      on_offset.emit(offset);
    },
  );

  let rows = props
    .contract_negotiation_items
    .iter()
    .map(
      |contract_negotiation_item| ContractNegotiationItemRenderer {
        item: contract_negotiation_item.clone(),
        on_show_contract_negotiation: props.on_show_contract_negotiation.clone(),
      },
    )
    .collect();

  let (entries, _) = use_table_data(MemoizedTableModel::new(Rc::new(rows)));

  let statuses_selector = if props.show_status_selector {
    html!(
      <MultiStateSelector
        selectable_items={props.statuses.clone()}
        on_selected={props.on_statuses.clone()}
        none_selected_label="All States"
        all_selected_label="All States"
      />
    )
  } else {
    html!()
  };

  let consumer_provider_select = {
    let onclick_consumer = {
      let consumer_provider = props.consumer_provider.clone();

      props.on_consumer_provider.reform(move |_| {
        let mut consumer_provider = consumer_provider.clone();
        if consumer_provider.contains(&ConsumerProvider::Consumer) {
          consumer_provider.remove(&ConsumerProvider::Consumer);
        } else {
          consumer_provider.insert(ConsumerProvider::Consumer);
        }

        consumer_provider
      })
    };

    let onclick_provider = {
      let consumer_provider = props.consumer_provider.clone();

      props.on_consumer_provider.reform(move |_| {
        let mut consumer_provider = consumer_provider.clone();
        if consumer_provider.contains(&ConsumerProvider::Provider) {
          consumer_provider.remove(&ConsumerProvider::Provider);
        } else {
          consumer_provider.insert(ConsumerProvider::Provider);
        }

        consumer_provider
      })
    };

    let text = {
      let state = if props.consumer_provider.is_empty() {
        HashSet::from([ConsumerProvider::Consumer, ConsumerProvider::Provider])
      } else {
        props.consumer_provider.clone()
      };

      state
        .into_iter()
        .map(|consumer_provider| consumer_provider.to_string())
        .collect::<Vec<String>>()
        .join(", ")
    };

    html!(
      <Dropdown {text}>
        <MenuAction
          onclick={onclick_consumer}
          selected={props.consumer_provider.is_empty() || props.consumer_provider.contains(&ConsumerProvider::Consumer)}
        >
          { "Consumer" }
        </MenuAction>
        <MenuAction
          onclick={onclick_provider}
          selected={props.consumer_provider.is_empty() || props.consumer_provider.contains(&ConsumerProvider::Provider)}
        >
          { "Provider" }
        </MenuAction>
      </Dropdown>
    )
  };

  html!(
    <>
      <Toolbar>
        <ToolbarContent>
          <ToolbarItem r#type={ToolbarItemType::BulkSelect}>
            { consumer_provider_select }
            { statuses_selector }
          </ToolbarItem>
          <ToolbarItem r#type={ToolbarItemType::Pagination}>
            <Pagination
              offset={props.offset}
              entries_per_page_choices={vec![5, 10, 25, 50, 100]}
              selected_choice={props.limit}
              onlimit={&props.on_limit}
              onnavigation={&nav_callback}
            />
          </ToolbarItem>
        </ToolbarContent>
      </Toolbar>
      <Table<Columns, UseTableData<Columns, MemoizedTableModel<ContractNegotiationItemRenderer>>>
        mode={TableMode::Compact}
        {header}
        {entries}
      />
    </>
  )
}

#[derive(Clone, Debug, Eq, PartialEq)]
enum Columns {
  State,
  ContractAgreementId,
  CounterParty,
  Asset,
  Kind,
  Actions,
}

#[derive(Clone, Debug)]
struct ContractNegotiationItemRenderer {
  item: ContractNegotiationItem,
  on_show_contract_negotiation: Callback<String>,
}

impl TableEntryRenderer<Columns> for ContractNegotiationItemRenderer {
  fn render_cell(&self, context: CellContext<'_, Columns>) -> Cell {
    match context.column {
      Columns::State => {
        let color = match self.item.state.as_str() {
          "Finalized" => Color::Green,
          "Terminated" => Color::Red,
          _ => Color::Blue,
        };

        html! { <Label label={self.item.state.to_string()} {color} /> }
      },
      Columns::ContractAgreementId => {
        if self.item.contract_agreement_id.is_empty() {
          html! { "-" }
        }
         else {
           html! {
             <ContractAgreementReference
               contract_agreement_id={self.item.contract_agreement_id.to_string()}
             />
           }
         }
      },
      Columns::CounterParty => html! { <DidLabel did={self.item.counter_party_id.to_string()} /> },
      Columns::Asset => html! { <AssetReference asset_id={self.item.asset_id.to_string()} /> },
      Columns::Kind => html! { self.item.kind.to_string() },
      Columns::Actions => {
        let contract_negotiation_id = self.item.id.clone();
        html! {
          <Button
            variant={ButtonVariant::Primary}
            onclick={self.on_show_contract_negotiation.clone().reform(move |_| contract_negotiation_id.clone())}
            icon={Icon::Eye}
          >
            { "Show" }
          </Button>
        }
      },
    }
    .into()
  }
}