diff --git a/app/components/crate-header.gjs b/app/components/crate-header.gjs index 878940d4199..3417bff4109 100644 --- a/app/components/crate-header.gjs +++ b/app/components/crate-header.gjs @@ -106,6 +106,10 @@ export default class CrateHeader extends Component { Dependents + + Security + + {{#if this.isOwner}} Settings diff --git a/app/controllers/crate/security.js b/app/controllers/crate/security.js new file mode 100644 index 00000000000..4cdbb0d68a0 --- /dev/null +++ b/app/controllers/crate/security.js @@ -0,0 +1,46 @@ +import Controller from '@ember/controller'; +import { service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; + +import { didCancel, dropTask } from 'ember-concurrency'; + +import { AjaxError } from '../../utils/ajax'; + +export default class SearchController extends Controller { + @service releaseTracks; + @service sentry; + + @tracked crate; + @tracked data; + + constructor() { + super(...arguments); + this.reset(); + } + + loadMoreTask = dropTask(async () => { + let { crate } = this; + let url = `https://rustsec.org/packages/${crate.id}.json`; + + try { + let response = await fetch(url); + if (response.status === 404) { + this.data = []; + } else if (response.ok) { + this.data = await response.json(); + } else { + throw new Error(`HTTP error! status: ${response}`); + } + } catch (error) { + // report unexpected errors to Sentry and ignore `ajax()` errors + if (!didCancel(error) && !(error instanceof AjaxError)) { + this.sentry.captureException(error); + } + } + }); + + reset() { + this.crate = undefined; + this.data = undefined; + } +} diff --git a/app/router.js b/app/router.js index 54aca0ef7ca..ca18f122c15 100644 --- a/app/router.js +++ b/app/router.js @@ -18,6 +18,7 @@ Router.map(function () { this.route('range', { path: '/range/:range' }); this.route('reverse-dependencies', { path: 'reverse_dependencies' }); + this.route('security'); this.route('owners'); this.route('settings', function () { diff --git a/app/routes/crate/security.js b/app/routes/crate/security.js new file mode 100644 index 00000000000..c1934044cbe --- /dev/null +++ b/app/routes/crate/security.js @@ -0,0 +1,27 @@ +import Route from '@ember/routing/route'; +import { waitForPromise } from '@ember/test-waiters'; + +export default class SecurityRoute extends Route { + queryParams = { + sort: { refreshModel: true }, + }; + + model(params) { + // we need a model() implementation that changes, otherwise the setupController() hook + // is not called and we won't reload the results if a new query string is used + return params; + } + + setupController(controller) { + super.setupController(...arguments); + let crate = this.modelFor('crate'); + // reset when crate changes + if (crate && crate !== controller.crate) { + controller.reset(); + } + controller.set('crate', crate); + if (controller.data === undefined) { + waitForPromise(controller.loadMoreTask.perform()); + } + } +} diff --git a/app/templates/crate/security.css b/app/templates/crate/security.css new file mode 100644 index 00000000000..761735264d4 --- /dev/null +++ b/app/templates/crate/security.css @@ -0,0 +1,18 @@ +.heading { + font-size: 1.17em; + margin-block-start: 1em; + margin-block-end: 1em; +} + +.advisories { + list-style: none; + margin: 0; + padding: 0; +} + +.row { + margin-top: var(--space-2xs); + background-color: light-dark(white, #141413); + padding: var(--space-m) var(--space-l); + list-style: none; +} diff --git a/app/templates/crate/security.gjs b/app/templates/crate/security.gjs new file mode 100644 index 00000000000..de4e1d73b68 --- /dev/null +++ b/app/templates/crate/security.gjs @@ -0,0 +1,23 @@ +import CrateHeader from 'crates-io/components/crate-header'; + + + + {{#if @controller.model}} + Advisories + + {{#each @controller.data as |advisory|}} + + + {{advisory.id}}: + {{advisory.summary}} + + {{advisory.details}} + + {{/each}} + + {{else}} + + No advisories found for this crate. + + {{/if}} +
{{advisory.details}}