diff --git a/app/components/crate-header.hbs b/app/components/crate-header.hbs index 49cd6cb7336..426773c52b1 100644 --- a/app/components/crate-header.hbs +++ b/app/components/crate-header.hbs @@ -22,8 +22,8 @@ + + Dependencies + + Dependents diff --git a/app/components/crate-header.js b/app/components/crate-header.js index 76df2e710a4..22b270e4516 100644 --- a/app/components/crate-header.js +++ b/app/components/crate-header.js @@ -3,7 +3,6 @@ import { inject as service } from '@ember/service'; import Component from '@glimmer/component'; export default class CrateHeader extends Component { - @service router; @service session; @computed('args.crate.owner_user', 'session.currentUser.id') 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/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/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%; -} 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/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/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(); } 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/app/templates/crate/version.hbs b/app/templates/crate/version.hbs index 0ca7fb8570a..39146540648 100644 --- a/app/templates/crate/version.hbs +++ b/app/templates/crate/version.hbs @@ -1,6 +1,10 @@ {{page-title this.crate.name}} - +
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 new file mode 100644 index 00000000000..0543f508fde --- /dev/null +++ b/tests/acceptance/crate-navtabs-test.js @@ -0,0 +1,67 @@ +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_DEPS = '[data-test-deps-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_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_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_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') + .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_DEPS).hasAttribute('href', '/crates/nanomsg/dependencies').hasNoAttribute('data-test-active'); + assert + .dom(TAB_REV_DEPS) + .hasAttribute('href', '/crates/nanomsg/reverse_dependencies') + .hasAttribute('data-test-active'); + assert.dom(TAB_SETTINGS).doesNotExist(); + }); +}); 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();