From 33cd87dbb1b3a372a42e2df28518c4d6abf0334a Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Sat, 27 Feb 2021 11:19:28 +0100 Subject: [PATCH 1/6] Add basic "crate navigation tabs" test --- app/components/nav-tabs/tab.hbs | 1 + tests/acceptance/crate-navtabs-test.js | 51 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 tests/acceptance/crate-navtabs-test.js diff --git a/app/components/nav-tabs/tab.hbs b/app/components/nav-tabs/tab.hbs index 225f6c0ea81..e55b7da02ae 100644 --- a/app/components/nav-tabs/tab.hbs +++ b/app/components/nav-tabs/tab.hbs @@ -2,6 +2,7 @@ {{yield}} diff --git a/tests/acceptance/crate-navtabs-test.js b/tests/acceptance/crate-navtabs-test.js new file mode 100644 index 00000000000..b6effb8e3b7 --- /dev/null +++ b/tests/acceptance/crate-navtabs-test.js @@ -0,0 +1,51 @@ +import { click, currentURL, visit } from '@ember/test-helpers'; +import { module, test } from 'qunit'; + +import { setupApplicationTest } from 'cargo/tests/helpers'; + +const TAB_README = '[data-test-readme-tab] a'; +const TAB_VERSIONS = '[data-test-versions-tab] a'; +const TAB_REV_DEPS = '[data-test-rev-deps-tab] a'; +const TAB_SETTINGS = '[data-test-settings-tab] a'; + +module('Acceptance | crate navigation tabs', function (hooks) { + setupApplicationTest(hooks); + + test('basic navigation between tabs works as expected', async function (assert) { + this.server.create('crate', { name: 'nanomsg' }); + this.server.create('version', { crateId: 'nanomsg', num: '0.6.1' }); + + await visit('/crates/nanomsg'); + assert.equal(currentURL(), '/crates/nanomsg'); + + assert.dom(TAB_README).hasAttribute('href', '/crates/nanomsg').hasAttribute('data-test-active'); + assert.dom(TAB_VERSIONS).hasAttribute('href', '/crates/nanomsg/versions').hasNoAttribute('data-test-active'); + assert + .dom(TAB_REV_DEPS) + .hasAttribute('href', '/crates/nanomsg/reverse_dependencies') + .hasNoAttribute('data-test-active'); + assert.dom(TAB_SETTINGS).doesNotExist(); + + await click(TAB_VERSIONS); + assert.equal(currentURL(), '/crates/nanomsg/versions'); + + assert.dom(TAB_README).hasAttribute('href', '/crates/nanomsg').hasNoAttribute('data-test-active'); + assert.dom(TAB_VERSIONS).hasAttribute('href', '/crates/nanomsg/versions').hasAttribute('data-test-active'); + assert + .dom(TAB_REV_DEPS) + .hasAttribute('href', '/crates/nanomsg/reverse_dependencies') + .hasNoAttribute('data-test-active'); + assert.dom(TAB_SETTINGS).doesNotExist(); + + await click(TAB_REV_DEPS); + assert.equal(currentURL(), '/crates/nanomsg/reverse_dependencies'); + + assert.dom(TAB_README).hasAttribute('href', '/crates/nanomsg').hasNoAttribute('data-test-active'); + assert.dom(TAB_VERSIONS).hasAttribute('href', '/crates/nanomsg/versions').hasNoAttribute('data-test-active'); + assert + .dom(TAB_REV_DEPS) + .hasAttribute('href', '/crates/nanomsg/reverse_dependencies') + .hasAttribute('data-test-active'); + assert.dom(TAB_SETTINGS).doesNotExist(); + }); +}); From 0dbb2949f1146939c6ed7979bebd1db1b8196757 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Sat, 27 Feb 2021 10:07:45 +0100 Subject: [PATCH 2/6] CrateHeader: Add `@versionNum` argument --- app/components/crate-header.hbs | 4 ++-- app/components/crate-header.js | 1 - app/templates/crate/version.hbs | 6 +++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/components/crate-header.hbs b/app/components/crate-header.hbs index 49cd6cb7336..3f1cbbf4f87 100644 --- a/app/components/crate-header.hbs +++ b/app/components/crate-header.hbs @@ -22,8 +22,8 @@ +
From 0f16504f4c8e93faaa19c82ff5f8a107f929d629 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Sat, 27 Feb 2021 01:48:51 +0100 Subject: [PATCH 3/6] Implement "Dependencies" page --- app/components/crate-header.hbs | 11 ++ app/components/dependency-list/row.hbs | 31 ++++++ app/components/dependency-list/row.js | 11 ++ app/components/dependency-list/row.module.css | 100 ++++++++++++++++++ app/router.js | 2 + app/routes/crate/dependencies.js | 13 +++ app/routes/crate/version-dependencies.js | 34 ++++++ .../crate/version-dependencies.module.css | 11 ++ app/templates/crate/version-dependencies.hbs | 38 +++++++ tests/acceptance/crate-dependencies-test.js | 54 ++++++++++ tests/acceptance/crate-navtabs-test.js | 16 +++ 11 files changed, 321 insertions(+) create mode 100644 app/components/dependency-list/row.hbs create mode 100644 app/components/dependency-list/row.js create mode 100644 app/components/dependency-list/row.module.css create mode 100644 app/routes/crate/dependencies.js create mode 100644 app/routes/crate/version-dependencies.js create mode 100644 app/styles/crate/version-dependencies.module.css create mode 100644 app/templates/crate/version-dependencies.hbs create mode 100644 tests/acceptance/crate-dependencies-test.js diff --git a/app/components/crate-header.hbs b/app/components/crate-header.hbs index 3f1cbbf4f87..426773c52b1 100644 --- a/app/components/crate-header.hbs +++ b/app/components/crate-header.hbs @@ -35,6 +35,17 @@ {{@crate.versions.length}} Versions + + Dependencies + + Dependents diff --git a/app/components/dependency-list/row.hbs b/app/components/dependency-list/row.hbs new file mode 100644 index 00000000000..ad561c7f26f --- /dev/null +++ b/app/components/dependency-list/row.hbs @@ -0,0 +1,31 @@ +
+ + {{format-req @dependency.req}} + + + + + {{@dependency.crate_id}} + + + + {{#if @dependency.optional}} + optional + {{/if}} + + +
\ No newline at end of file diff --git a/app/components/dependency-list/row.js b/app/components/dependency-list/row.js new file mode 100644 index 00000000000..48f84c5d1f1 --- /dev/null +++ b/app/components/dependency-list/row.js @@ -0,0 +1,11 @@ +import { action } from '@ember/object'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; + +export default class VersionRow extends Component { + @tracked focused = false; + + @action setFocused(value) { + this.focused = value; + } +} diff --git a/app/components/dependency-list/row.module.css b/app/components/dependency-list/row.module.css new file mode 100644 index 00000000000..4566052bc29 --- /dev/null +++ b/app/components/dependency-list/row.module.css @@ -0,0 +1,100 @@ +.row { + --bg-color: var(--grey200); + --hover-bg-color: hsl(217, 37%, 98%); + --range-color: var(--grey900); + --crate-color: var(--grey700); + --shadow: 0 1px 3px hsla(51, 90%, 42%, .35); + + display: flex; + align-items: center; + position: relative; + font-size: 18px; + padding: 15px 25px; + background-color: white; + border-radius: 7px; + box-shadow: var(--shadow); + transition: all 300ms; + + &:hover, &.focused { + background-color: var(--hover-bg-color); + transition: all 0ms; + } + + &.focused { + box-shadow: 0 0 0 3px var(--yellow500), var(--shadow); + } + + &.optional { + --range-color: var(--grey600); + --crate-color: var(--grey600); + } + + [title], :global(.ember-tooltip-target) { + position: relative; + z-index: 1; + cursor: help; + } + + :global(.ember-tooltip) { + word-break: break-all; + } + + @media only screen and (max-width: 550px) { + display: block + } +} + +.range { + margin-right: 15px; + min-width: 100px; + color: var(--range-color); + font-variant: tabular-nums; +} + +.link { + color: var(--crate-color); + font-weight: 500; + margin-right: 15px; + outline: none; + + &:hover { + color: var(--crate-color); + } + + &::after { + content: ''; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + } +} + +.metadata { + color: var(--grey600); + text-transform: uppercase; + letter-spacing: .7px; + font-size: 13px; + + a { + position: relative; + color: var(--grey600); + + &:hover { + color: var(--grey900); + } + } + + svg { + height: 1em; + width: auto; + margin-right: 2px; + margin-bottom: -.1em; + } + + :global(.ember-tooltip) { + text-transform: none; + letter-spacing: normal; + } +} diff --git a/app/router.js b/app/router.js index be2b1327ac6..1358ca01bee 100644 --- a/app/router.js +++ b/app/router.js @@ -12,7 +12,9 @@ Router.map(function () { this.route('crates'); this.route('crate', { path: '/crates/:crate_id' }, function () { this.route('versions'); + this.route('dependencies'); this.route('version', { path: '/:version_num' }); + this.route('version-dependencies', { path: '/:version_num/dependencies' }); this.route('reverse-dependencies', { path: 'reverse_dependencies' }); diff --git a/app/routes/crate/dependencies.js b/app/routes/crate/dependencies.js new file mode 100644 index 00000000000..2196da52109 --- /dev/null +++ b/app/routes/crate/dependencies.js @@ -0,0 +1,13 @@ +import Route from '@ember/routing/route'; + +export default class VersionRoute extends Route { + async model() { + let crate = this.modelFor('crate'); + let versions = await crate.get('versions'); + + let { defaultVersion } = crate; + let version = versions.find(version => version.num === defaultVersion) ?? versions.lastObject; + + this.replaceWith('crate.version-dependencies', crate, version.num); + } +} diff --git a/app/routes/crate/version-dependencies.js b/app/routes/crate/version-dependencies.js new file mode 100644 index 00000000000..64b1bd80790 --- /dev/null +++ b/app/routes/crate/version-dependencies.js @@ -0,0 +1,34 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +export default class VersionRoute extends Route { + @service notifications; + + async model(params) { + let crate = this.modelFor('crate'); + let versions = await crate.get('versions'); + + let requestedVersion = params.version_num; + let version = versions.find(version => version.num === requestedVersion); + if (!version) { + this.notifications.error(`Version '${requestedVersion}' of crate '${crate.name}' does not exist`); + this.replaceWith('crate.index'); + } + + try { + await version.loadDepsTask.perform(); + } catch { + this.notifications.error( + `Failed to load the list of dependencies for the '${crate.name}' crate. Please try again later!`, + ); + this.replaceWith('crate.index'); + } + + return version; + } + + setupController(controller, model) { + controller.set('version', model); + controller.set('crate', this.modelFor('crate')); + } +} diff --git a/app/styles/crate/version-dependencies.module.css b/app/styles/crate/version-dependencies.module.css new file mode 100644 index 00000000000..7aefbd96b75 --- /dev/null +++ b/app/styles/crate/version-dependencies.module.css @@ -0,0 +1,11 @@ +.list { + list-style: none; + margin: 0; + padding: 0; + + li { + &:not(:first-child) { + margin-top: 10px; + } + } +} diff --git a/app/templates/crate/version-dependencies.hbs b/app/templates/crate/version-dependencies.hbs new file mode 100644 index 00000000000..10b1af2a5cc --- /dev/null +++ b/app/templates/crate/version-dependencies.hbs @@ -0,0 +1,38 @@ +{{page-title this.crate.name}} + + + +

Dependencies

+{{#if this.version.normalDependencies}} +
    + {{#each this.version.normalDependencies as |dependency|}} +
  • + {{/each}} +
+{{else}} +
+ This version of the "{{this.crate.name}}" crate has no dependencies +
+{{/if}} + +{{#if this.version.buildDependencies}} +

Build-Dependencies

+
    + {{#each this.version.buildDependencies as |dependency|}} +
  • + {{/each}} +
+{{/if}} + +{{#if this.version.devDependencies}} +

Dev-Dependencies

+
    + {{#each this.version.devDependencies as |dependency|}} +
  • + {{/each}} +
+{{/if}} diff --git a/tests/acceptance/crate-dependencies-test.js b/tests/acceptance/crate-dependencies-test.js new file mode 100644 index 00000000000..267c04c4515 --- /dev/null +++ b/tests/acceptance/crate-dependencies-test.js @@ -0,0 +1,54 @@ +import { currentURL, visit } from '@ember/test-helpers'; +import { module, test } from 'qunit'; + +import percySnapshot from '@percy/ember'; +import a11yAudit from 'ember-a11y-testing/test-support/audit'; +import { getPageTitle } from 'ember-page-title/test-support'; + +import { setupApplicationTest } from 'cargo/tests/helpers'; + +import axeConfig from '../axe-config'; + +module('Acceptance | crate dependencies page', function (hooks) { + setupApplicationTest(hooks); + + test('shows the lists of dependencies', async function (assert) { + this.server.loadFixtures(); + + await visit('/crates/nanomsg/dependencies'); + assert.equal(currentURL(), '/crates/nanomsg/0.6.1/dependencies'); + assert.equal(getPageTitle(), 'nanomsg - crates.io: Rust Package Registry'); + + assert.dom('[data-test-dependencies] li').exists({ count: 2 }); + assert.dom('[data-test-build-dependencies] li').exists({ count: 1 }); + assert.dom('[data-test-dev-dependencies] li').exists({ count: 1 }); + + await percySnapshot(assert); + await a11yAudit(axeConfig); + }); + + test('empty list case', async function (assert) { + this.server.create('crate', { name: 'nanomsg' }); + this.server.create('version', { crateId: 'nanomsg', num: '0.6.1' }); + + await visit('/crates/nanomsg/dependencies'); + + assert.dom('[data-test-no-dependencies]').exists(); + assert.dom('[data-test-dependencies] li').doesNotExist(); + assert.dom('[data-test-build-dependencies] li').doesNotExist(); + assert.dom('[data-test-dev-dependencies] li').doesNotExist(); + }); + + test('shows error message if loading of dependencies fails', async function (assert) { + this.server.loadFixtures(); + + this.server.get('/api/v1/crates/:crate_name/:version_num/dependencies', {}, 500); + + await visit('/crates/nanomsg/dependencies'); + assert.equal(currentURL(), '/crates/nanomsg'); + + assert + .dom('[data-test-notification-message="error"]') + .hasText("Failed to load the list of dependencies for the 'nanomsg' crate. Please try again later!"); + }); +}); diff --git a/tests/acceptance/crate-navtabs-test.js b/tests/acceptance/crate-navtabs-test.js index b6effb8e3b7..0543f508fde 100644 --- a/tests/acceptance/crate-navtabs-test.js +++ b/tests/acceptance/crate-navtabs-test.js @@ -5,6 +5,7 @@ import { setupApplicationTest } from 'cargo/tests/helpers'; const TAB_README = '[data-test-readme-tab] a'; const TAB_VERSIONS = '[data-test-versions-tab] a'; +const TAB_DEPS = '[data-test-deps-tab] a'; const TAB_REV_DEPS = '[data-test-rev-deps-tab] a'; const TAB_SETTINGS = '[data-test-settings-tab] a'; @@ -20,6 +21,7 @@ module('Acceptance | crate navigation tabs', function (hooks) { assert.dom(TAB_README).hasAttribute('href', '/crates/nanomsg').hasAttribute('data-test-active'); assert.dom(TAB_VERSIONS).hasAttribute('href', '/crates/nanomsg/versions').hasNoAttribute('data-test-active'); + assert.dom(TAB_DEPS).hasAttribute('href', '/crates/nanomsg/dependencies').hasNoAttribute('data-test-active'); assert .dom(TAB_REV_DEPS) .hasAttribute('href', '/crates/nanomsg/reverse_dependencies') @@ -31,6 +33,19 @@ module('Acceptance | crate navigation tabs', function (hooks) { assert.dom(TAB_README).hasAttribute('href', '/crates/nanomsg').hasNoAttribute('data-test-active'); assert.dom(TAB_VERSIONS).hasAttribute('href', '/crates/nanomsg/versions').hasAttribute('data-test-active'); + assert.dom(TAB_DEPS).hasAttribute('href', '/crates/nanomsg/dependencies').hasNoAttribute('data-test-active'); + assert + .dom(TAB_REV_DEPS) + .hasAttribute('href', '/crates/nanomsg/reverse_dependencies') + .hasNoAttribute('data-test-active'); + assert.dom(TAB_SETTINGS).doesNotExist(); + + await click(TAB_DEPS); + assert.equal(currentURL(), '/crates/nanomsg/0.6.1/dependencies'); + + assert.dom(TAB_README).hasAttribute('href', '/crates/nanomsg/0.6.1').hasNoAttribute('data-test-active'); + assert.dom(TAB_VERSIONS).hasAttribute('href', '/crates/nanomsg/versions').hasNoAttribute('data-test-active'); + assert.dom(TAB_DEPS).hasAttribute('href', '/crates/nanomsg/0.6.1/dependencies').hasAttribute('data-test-active'); assert .dom(TAB_REV_DEPS) .hasAttribute('href', '/crates/nanomsg/reverse_dependencies') @@ -42,6 +57,7 @@ module('Acceptance | crate navigation tabs', function (hooks) { assert.dom(TAB_README).hasAttribute('href', '/crates/nanomsg').hasNoAttribute('data-test-active'); assert.dom(TAB_VERSIONS).hasAttribute('href', '/crates/nanomsg/versions').hasNoAttribute('data-test-active'); + assert.dom(TAB_DEPS).hasAttribute('href', '/crates/nanomsg/dependencies').hasNoAttribute('data-test-active'); assert .dom(TAB_REV_DEPS) .hasAttribute('href', '/crates/nanomsg/reverse_dependencies') From 4dfd18ea46e078dd735d335bb0ebfa1de80933ab Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Sat, 27 Feb 2021 11:27:35 +0100 Subject: [PATCH 4/6] CrateSidebar: Remove "Dependencies" sections These are replaced by the top-level tab now --- app/components/crate-sidebar.hbs | 37 -------------------------------- tests/acceptance/crate-test.js | 24 --------------------- 2 files changed, 61 deletions(-) diff --git a/app/components/crate-sidebar.hbs b/app/components/crate-sidebar.hbs index eabecc1457c..439d810de37 100644 --- a/app/components/crate-sidebar.hbs +++ b/app/components/crate-sidebar.hbs @@ -129,42 +129,5 @@
{{/if}} {{/unless}} - -
-

Dependencies

-
    - {{#each @version.normalDependencies as |dep|}} -
  • - {{else}} - {{#if @version.loadDepsTask.isRunning}} -
  • Loading…
  • - {{else}} -
  • None
  • - {{/if}} - {{/each}} -
-
- - {{#if @version.buildDependencies}} -
-

Build-Dependencies

-
    - {{#each @version.buildDependencies as |dep|}} -
  • - {{/each}} -
-
- {{/if}} - - {{#if @version.devDependencies}} -
-

Dev-Dependencies

-
    - {{#each @version.devDependencies as |dep|}} -
  • - {{/each}} -
-
- {{/if}}
\ No newline at end of file diff --git a/tests/acceptance/crate-test.js b/tests/acceptance/crate-test.js index fe28d846506..37976c5c6d2 100644 --- a/tests/acceptance/crate-test.js +++ b/tests/acceptance/crate-test.js @@ -132,30 +132,6 @@ module('Acceptance | crate page', function (hooks) { assert.dom('[data-test-heading] [data-test-team-name]').hasText('thehydroimpulseteam'); }); - test('crates having normal dependencies', async function (assert) { - this.server.loadFixtures(); - - await visit('/crates/nanomsg'); - - assert.dom('[data-test-dependencies] li').exists({ count: 2 }); - }); - - test('crates having build dependencies', async function (assert) { - this.server.loadFixtures(); - - await visit('/crates/nanomsg'); - - assert.dom('[data-test-build-dependencies] li').exists({ count: 1 }); - }); - - test('crates having dev dependencies', async function (assert) { - this.server.loadFixtures(); - - await visit('/crates/nanomsg'); - - assert.dom('[data-test-dev-dependencies] li').exists({ count: 1 }); - }); - test('crates having user-owners', async function (assert) { this.server.loadFixtures(); From e605162dcefb0ba49d828ea8576b82251c3d7cd7 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Sat, 27 Feb 2021 11:27:59 +0100 Subject: [PATCH 5/6] crate.version: Remove obsolete dependency loading call --- app/routes/crate/version.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/routes/crate/version.js b/app/routes/crate/version.js index 605a42a5f6d..6243fd09545 100644 --- a/app/routes/crate/version.js +++ b/app/routes/crate/version.js @@ -32,7 +32,6 @@ export default class VersionRoute extends Route { setupController(controller, model) { super.setupController(...arguments); - model.version.loadDepsTask.perform(); if (!model.version.authorNames) { model.version.loadAuthorsTask.perform(); } From c1a356f5766edad9dff4235e10e6d9f675fdc369 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Sat, 27 Feb 2021 12:16:18 +0100 Subject: [PATCH 6/6] Remove unused `LinkToDep` component --- app/components/link-to-dep.hbs | 6 ------ app/components/link-to-dep.module.css | 3 --- 2 files changed, 9 deletions(-) delete mode 100644 app/components/link-to-dep.hbs delete mode 100644 app/components/link-to-dep.module.css diff --git a/app/components/link-to-dep.hbs b/app/components/link-to-dep.hbs deleted file mode 100644 index 824a1718c3b..00000000000 --- a/app/components/link-to-dep.hbs +++ /dev/null @@ -1,6 +0,0 @@ - - {{ @dep.crate_id }} {{ format-req @dep.req }} - -{{#if @dep.optional}} - optional -{{/if}} diff --git a/app/components/link-to-dep.module.css b/app/components/link-to-dep.module.css deleted file mode 100644 index 3a0b535acfa..00000000000 --- a/app/components/link-to-dep.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.optional { - font-size: 80%; -}