Skip to content

Commit

Permalink
UI - continuously fetch sboms so they eventually appear after they ar…
Browse files Browse the repository at this point in the history
…e uploaded

Signed-off-by: carlosthe19916 <[email protected]>
  • Loading branch information
carlosthe19916 committed Jan 22, 2024
1 parent d8a79c3 commit 62a5186
Show file tree
Hide file tree
Showing 22 changed files with 335 additions and 51 deletions.
13 changes: 13 additions & 0 deletions spog/ui/crates/backend/src/sbom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ impl SBOMService {
Ok(Some(response.api_error_for_status().await?.text().await?))
}

pub async fn get_from_index(&self, id: &str) -> Result<SearchResult<Vec<SbomSummary>>, ApiError> {
let q = format!("id:{id}");
let response = self
.client
.get(self.backend.join(Endpoint::Api, "/api/v1/sbom/search")?)
.query(&[("q", q)])
.latest_access_token(&self.access_token)
.send()
.await?;

Ok(response.api_error_for_status().await?.json().await?)
}

pub async fn get_sbom_vulns(&self, id: impl AsRef<str>) -> Result<Option<SbomReport>, ApiError> {
let mut url = self.backend.join(Endpoint::Api, "/api/v1/sbom/vulnerabilities")?;
url.query_pairs_mut().append_pair("id", id.as_ref()).finish();
Expand Down
31 changes: 31 additions & 0 deletions spog/ui/crates/common/src/utils/count.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,34 @@ where

html!(<> {title.as_ref()} { badge } </>)
}

#[derive(Clone, PartialEq, Properties)]
pub struct CountTabTitleProperties {
#[prop_or_default]
pub count: Option<usize>,

#[prop_or_default]
pub processing: bool,

#[prop_or_default]
pub title: AttrValue,
}

#[function_component(CountTabTitle)]
pub fn grid_item(props: &CountTabTitleProperties) -> Html {
let count_state = use_state_eq(|| None);

use_effect_with((count_state.clone(), props.count), |(count_state, count)| {
if let Some(val) = count {
count_state.set(Some(*val));
}
});

let badge = if props.processing && count_state.is_none() {
html!(<> {" "} <Badge read=true> { Icon::InProgress } </Badge></>)
} else {
html!(<> {" "} <Badge> { *count_state } </Badge> </>)
};

html!(<> {&props.title} { badge } </>)
}
1 change: 1 addition & 0 deletions spog/ui/crates/components/src/advisory/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub fn use_advisory_search(
) -> UseStandardSearch {
let config = use_config();
use_generic_search::<Vulnerabilities, _, _, _, _>(
0,
search_params,
pagination,
callback,
Expand Down
1 change: 1 addition & 0 deletions spog/ui/crates/components/src/cve/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub fn use_cve_search(
) -> UseStandardSearch {
let config = use_config();
use_generic_search::<Cves, _, _, _, _>(
0,
search_params,
pagination,
callback,
Expand Down
23 changes: 21 additions & 2 deletions spog/ui/crates/components/src/hooks/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use spog_ui_common::utils::search::*;
use std::future::Future;
use std::rc::Rc;
use yew::prelude::*;
use yew_hooks::prelude::*;
use yew_more_hooks::prelude::{use_async_with_cloned_deps, UseAsyncHandleDeps};
use yew_oauth2::prelude::{use_latest_access_token, LatestAccessToken};

Expand Down Expand Up @@ -100,6 +101,7 @@ pub struct SearchOperationContext {

#[hook]
pub fn use_generic_search<S, R, F, Fut, IF>(
fetch_interval: u32,
search_params: UseReducerHandle<SearchState<DynamicSearchParameters>>,
pagination: UsePagination,
callback: Callback<UseAsyncHandleDeps<R, String>>,
Expand All @@ -113,6 +115,18 @@ where
F: FnOnce(SearchOperationContext) -> Fut + 'static,
Fut: Future<Output = Result<R, String>> + 'static,
{
let interval = use_state(|| fetch_interval);
let fetch_number = use_state_eq(|| 0);
{
let state: UseStateHandle<i32> = fetch_number.clone();
use_interval(
move || {
state.set(*state + 1);
},
*interval,
);
}

let backend = use_backend();
let access_token = use_latest_access_token();

Expand All @@ -123,7 +137,7 @@ where
let search_op = {
let filters = filters.clone();
use_async_with_cloned_deps(
move |(search_params, page, per_page)| async move {
move |(_, search_params, page, per_page)| async move {
f(SearchOperationContext {
backend: backend.clone(),
access_token,
Expand All @@ -134,7 +148,12 @@ where
})
.await
},
((*search_params).clone(), pagination.page, pagination.per_page),
(
*fetch_number,
(*search_params).clone(),
pagination.page,
pagination.per_page,
),
)
};

Expand Down
1 change: 1 addition & 0 deletions spog/ui/crates/components/src/packages/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub fn use_package_search(
) -> UseStandardSearch {
let config = use_config();
use_generic_search::<PackageInfo, _, _, _, _>(
0,
search_params,
pagination,
callback,
Expand Down
10 changes: 8 additions & 2 deletions spog/ui/crates/components/src/pagination.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,25 @@ pub struct PaginationWrappedProperties {

#[function_component(PaginationWrapped)]
pub fn pagination_wrapped(props: &PaginationWrappedProperties) -> Html {
let total = use_state_eq(|| None);

if let Some(val) = props.total {
total.set(Some(val))
}

html!(
<>
<div class="pf-v5-u-p-sm">
<SimplePagination
pagination={props.pagination.clone()}
total={props.total}
total={*total}
/>
</div>
{ for props.children.iter() }
<div class="pf-v5-u-p-sm">
<SimplePagination
pagination={props.pagination.clone()}
total={props.total}
total={*total}
position={PaginationPosition::Bottom}
/>
</div>
Expand Down
59 changes: 39 additions & 20 deletions spog/ui/crates/components/src/sbom/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
mod report;
mod report_button;
mod report_viewer;
mod search;

pub use report::*;
pub use report_button::*;
pub use report_viewer::*;
pub use search::*;

use crate::{download::Download, table_wrapper::TableWrapper};
Expand All @@ -16,6 +22,8 @@ use yew::prelude::*;
use yew_more_hooks::hooks::UseAsyncState;
use yew_nested_router::components::Link;

use self::report_button::ReportButton;

#[derive(PartialEq, Properties)]
pub struct SbomResultProperties {
pub state: UseAsyncState<SearchResult<Rc<Vec<SbomSummary>>>, String>,
Expand All @@ -31,13 +39,15 @@ pub enum Column {
Dependencies,
Advisories,
Version,
Report,
}

#[derive(Clone)]
#[derive(Clone, PartialEq)]
pub struct PackageEntry {
url: Option<Url>,
package: SbomSummary,
link_advisories: bool,
id: String,
}

impl PackageEntry {
Expand Down Expand Up @@ -81,6 +91,10 @@ impl TableEntryRenderer<Column> for PackageEntry {
}
.into(),
Column::Version => html!(&self.package.version).into(),
Column::Report => html!(
<ReportButton id={self.id.clone()}/>
)
.into(),
}
}

Expand All @@ -100,24 +114,24 @@ pub fn sbom_result(props: &SbomResultProperties) -> Html {
let config = use_config();
let link_advisories = config.features.dedicated_search;

let data = match &props.state {
UseAsyncState::Ready(Ok(val)) => {
let data: Vec<PackageEntry> = val
.result
.iter()
.map(|pkg| {
let url = backend.join(Endpoint::Api, &pkg.href).ok();
PackageEntry {
package: pkg.clone(),
url,
link_advisories,
}
})
.collect();
Some(data)
}
_ => None,
};
let data = use_state_eq(|| None);

if let UseAsyncState::Ready(Ok(val)) = &props.state {
let response: Vec<PackageEntry> = val
.result
.iter()
.map(|pkg| {
let url = backend.join(Endpoint::Api, &pkg.href).ok();
PackageEntry {
package: pkg.clone(),
url,
link_advisories,
id: pkg.id.clone(),
}
})
.collect();
data.set(Some(response));
}

let sortby: UseStateHandle<Option<TableHeaderSortBy<Column>>> = use_state_eq(|| None);
let onsort = use_callback(
Expand All @@ -130,7 +144,7 @@ pub fn sbom_result(props: &SbomResultProperties) -> Html {
},
);

let (entries, onexpand) = use_table_data(MemoizedTableModel::new(Rc::new(data.unwrap_or_default())));
let (entries, onexpand) = use_table_data(MemoizedTableModel::new(Rc::new((*data).clone().unwrap_or_default())));

let header = vec![
yew::props!(TableColumnProperties<Column> {
Expand Down Expand Up @@ -170,6 +184,11 @@ pub fn sbom_result(props: &SbomResultProperties) -> Html {
label: "Download",
width: ColumnWidth::FitContent
}),
yew::props!(TableColumnProperties<Column> {
index: Column::Report,
label: "Report",
width: ColumnWidth::FitContent
}),
];

html!(
Expand Down
File renamed without changes.
78 changes: 78 additions & 0 deletions spog/ui/crates/components/src/sbom/report_button.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use std::rc::Rc;

use patternfly_yew::prelude::*;
use spog_ui_backend::use_backend;
use spog_ui_common::error::components::ApiError;
use yew::prelude::*;
use yew_more_hooks::hooks::*;
use yew_oauth2::hook::use_latest_access_token;

use crate::{common::NotFound, sbom::report_viewer::ReportViewwer};

#[derive(PartialEq, Properties)]
pub struct ReportButtonProperties {
pub id: String,
}

#[function_component(ReportButton)]
pub fn report_button(props: &ReportButtonProperties) -> Html {
let backdropper = use_backdrop().expect("Requires BackdropViewer in its hierarchy");

let onclick = use_callback((props.id.clone(), backdropper.clone()), |_, (id, backdropper)| {
backdropper.open(Backdrop::new(html!(
<Bullseye>
<Modal
title = {"Report"}
variant = { ModalVariant::Large }
>
<ReportModal id={id.to_string()}/>
</Modal>
</Bullseye>
)));
});

html!(
<Button
icon={Icon::Eye}
variant={ButtonVariant::Plain}
{onclick}
/>
)
}

#[derive(PartialEq, Properties)]
pub struct ReportModalProperties {
pub id: String,
}

#[function_component(ReportModal)]
pub fn report_modal(props: &ReportModalProperties) -> Html {
let backend = use_backend();
let access_token = use_latest_access_token();

let sbom = use_async_with_cloned_deps(
|(id, backend)| async move {
spog_ui_backend::SBOMService::new(backend.clone(), access_token)
.get(id)
.await
},
(props.id.clone(), backend),
);

let content = match &*sbom {
UseAsyncState::Pending | UseAsyncState::Processing => html!(<Spinner />),
UseAsyncState::Ready(Ok(None)) => html!(<NotFound/>),
UseAsyncState::Ready(Ok(Some(data))) => html!(
<div style="height: 700px">
<ReportViewwer raw={Rc::new((*data).clone())} />
</div>
),
UseAsyncState::Ready(Err(err)) => html!(<ApiError error={err.clone()} />),
};

html!(
<>
{content}
</>
)
}
Loading

0 comments on commit 62a5186

Please sign in to comment.