Skip to content

Commit 0f16504

Browse files
committed
Implement "Dependencies" page
1 parent 0dbb294 commit 0f16504

File tree

11 files changed

+321
-0
lines changed

11 files changed

+321
-0
lines changed

app/components/crate-header.hbs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@
3535
{{@crate.versions.length}} Versions
3636
</nav.Tab>
3737

38+
<nav.Tab
39+
@link={{if
40+
@versionNum
41+
(link "crate.version-dependencies" @crate @versionNum)
42+
(link "crate.dependencies" @crate)
43+
}}
44+
data-test-deps-tab
45+
>
46+
Dependencies
47+
</nav.Tab>
48+
3849
<nav.Tab @link={{link "crate.reverse-dependencies" @crate}} data-test-rev-deps-tab>
3950
Dependents
4051
</nav.Tab>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<div
2+
local-class="
3+
row
4+
{{if @dependency.optional "optional"}}
5+
{{if this.focused "focused"}}
6+
"
7+
...attributes
8+
>
9+
<span local-class="range" data-test-range>
10+
{{format-req @dependency.req}}
11+
</span>
12+
13+
<span>
14+
<LinkTo
15+
@route="crate"
16+
@model={{@dependency.crate_id}}
17+
local-class="link"
18+
{{on "focusin" (fn this.setFocused true)}}
19+
{{on "focusout" (fn this.setFocused false)}}
20+
data-test-release-track-link
21+
>
22+
{{@dependency.crate_id}}
23+
</LinkTo>
24+
25+
<span local-class="metadata">
26+
{{#if @dependency.optional}}
27+
optional
28+
{{/if}}
29+
</span>
30+
</span>
31+
</div>

app/components/dependency-list/row.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { action } from '@ember/object';
2+
import Component from '@glimmer/component';
3+
import { tracked } from '@glimmer/tracking';
4+
5+
export default class VersionRow extends Component {
6+
@tracked focused = false;
7+
8+
@action setFocused(value) {
9+
this.focused = value;
10+
}
11+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
.row {
2+
--bg-color: var(--grey200);
3+
--hover-bg-color: hsl(217, 37%, 98%);
4+
--range-color: var(--grey900);
5+
--crate-color: var(--grey700);
6+
--shadow: 0 1px 3px hsla(51, 90%, 42%, .35);
7+
8+
display: flex;
9+
align-items: center;
10+
position: relative;
11+
font-size: 18px;
12+
padding: 15px 25px;
13+
background-color: white;
14+
border-radius: 7px;
15+
box-shadow: var(--shadow);
16+
transition: all 300ms;
17+
18+
&:hover, &.focused {
19+
background-color: var(--hover-bg-color);
20+
transition: all 0ms;
21+
}
22+
23+
&.focused {
24+
box-shadow: 0 0 0 3px var(--yellow500), var(--shadow);
25+
}
26+
27+
&.optional {
28+
--range-color: var(--grey600);
29+
--crate-color: var(--grey600);
30+
}
31+
32+
[title], :global(.ember-tooltip-target) {
33+
position: relative;
34+
z-index: 1;
35+
cursor: help;
36+
}
37+
38+
:global(.ember-tooltip) {
39+
word-break: break-all;
40+
}
41+
42+
@media only screen and (max-width: 550px) {
43+
display: block
44+
}
45+
}
46+
47+
.range {
48+
margin-right: 15px;
49+
min-width: 100px;
50+
color: var(--range-color);
51+
font-variant: tabular-nums;
52+
}
53+
54+
.link {
55+
color: var(--crate-color);
56+
font-weight: 500;
57+
margin-right: 15px;
58+
outline: none;
59+
60+
&:hover {
61+
color: var(--crate-color);
62+
}
63+
64+
&::after {
65+
content: '';
66+
position: absolute;
67+
left: 0;
68+
top: 0;
69+
right: 0;
70+
bottom: 0;
71+
}
72+
}
73+
74+
.metadata {
75+
color: var(--grey600);
76+
text-transform: uppercase;
77+
letter-spacing: .7px;
78+
font-size: 13px;
79+
80+
a {
81+
position: relative;
82+
color: var(--grey600);
83+
84+
&:hover {
85+
color: var(--grey900);
86+
}
87+
}
88+
89+
svg {
90+
height: 1em;
91+
width: auto;
92+
margin-right: 2px;
93+
margin-bottom: -.1em;
94+
}
95+
96+
:global(.ember-tooltip) {
97+
text-transform: none;
98+
letter-spacing: normal;
99+
}
100+
}

app/router.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ Router.map(function () {
1212
this.route('crates');
1313
this.route('crate', { path: '/crates/:crate_id' }, function () {
1414
this.route('versions');
15+
this.route('dependencies');
1516
this.route('version', { path: '/:version_num' });
17+
this.route('version-dependencies', { path: '/:version_num/dependencies' });
1618

1719
this.route('reverse-dependencies', { path: 'reverse_dependencies' });
1820

app/routes/crate/dependencies.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Route from '@ember/routing/route';
2+
3+
export default class VersionRoute extends Route {
4+
async model() {
5+
let crate = this.modelFor('crate');
6+
let versions = await crate.get('versions');
7+
8+
let { defaultVersion } = crate;
9+
let version = versions.find(version => version.num === defaultVersion) ?? versions.lastObject;
10+
11+
this.replaceWith('crate.version-dependencies', crate, version.num);
12+
}
13+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Route from '@ember/routing/route';
2+
import { inject as service } from '@ember/service';
3+
4+
export default class VersionRoute extends Route {
5+
@service notifications;
6+
7+
async model(params) {
8+
let crate = this.modelFor('crate');
9+
let versions = await crate.get('versions');
10+
11+
let requestedVersion = params.version_num;
12+
let version = versions.find(version => version.num === requestedVersion);
13+
if (!version) {
14+
this.notifications.error(`Version '${requestedVersion}' of crate '${crate.name}' does not exist`);
15+
this.replaceWith('crate.index');
16+
}
17+
18+
try {
19+
await version.loadDepsTask.perform();
20+
} catch {
21+
this.notifications.error(
22+
`Failed to load the list of dependencies for the '${crate.name}' crate. Please try again later!`,
23+
);
24+
this.replaceWith('crate.index');
25+
}
26+
27+
return version;
28+
}
29+
30+
setupController(controller, model) {
31+
controller.set('version', model);
32+
controller.set('crate', this.modelFor('crate'));
33+
}
34+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.list {
2+
list-style: none;
3+
margin: 0;
4+
padding: 0;
5+
6+
li {
7+
&:not(:first-child) {
8+
margin-top: 10px;
9+
}
10+
}
11+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{{page-title this.crate.name}}
2+
3+
<CrateHeader
4+
@crate={{this.crate}}
5+
@version={{this.version}}
6+
@versionNum={{this.version.num}}
7+
/>
8+
9+
<h3 local-class="heading">Dependencies</h3>
10+
{{#if this.version.normalDependencies}}
11+
<ul local-class="list" data-test-dependencies>
12+
{{#each this.version.normalDependencies as |dependency|}}
13+
<li><DependencyList::Row @dependency={{dependency}} /></li>
14+
{{/each}}
15+
</ul>
16+
{{else}}
17+
<div local-class="no-deps" data-test-no-dependencies>
18+
This version of the "{{this.crate.name}}" crate has no dependencies
19+
</div>
20+
{{/if}}
21+
22+
{{#if this.version.buildDependencies}}
23+
<h3 local-class="heading">Build-Dependencies</h3>
24+
<ul local-class="list" data-test-build-dependencies>
25+
{{#each this.version.buildDependencies as |dependency|}}
26+
<li><DependencyList::Row @dependency={{dependency}} /></li>
27+
{{/each}}
28+
</ul>
29+
{{/if}}
30+
31+
{{#if this.version.devDependencies}}
32+
<h3 local-class="heading">Dev-Dependencies</h3>
33+
<ul local-class="list" data-test-dev-dependencies>
34+
{{#each this.version.devDependencies as |dependency|}}
35+
<li><DependencyList::Row @dependency={{dependency}} /></li>
36+
{{/each}}
37+
</ul>
38+
{{/if}}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { currentURL, visit } from '@ember/test-helpers';
2+
import { module, test } from 'qunit';
3+
4+
import percySnapshot from '@percy/ember';
5+
import a11yAudit from 'ember-a11y-testing/test-support/audit';
6+
import { getPageTitle } from 'ember-page-title/test-support';
7+
8+
import { setupApplicationTest } from 'cargo/tests/helpers';
9+
10+
import axeConfig from '../axe-config';
11+
12+
module('Acceptance | crate dependencies page', function (hooks) {
13+
setupApplicationTest(hooks);
14+
15+
test('shows the lists of dependencies', async function (assert) {
16+
this.server.loadFixtures();
17+
18+
await visit('/crates/nanomsg/dependencies');
19+
assert.equal(currentURL(), '/crates/nanomsg/0.6.1/dependencies');
20+
assert.equal(getPageTitle(), 'nanomsg - crates.io: Rust Package Registry');
21+
22+
assert.dom('[data-test-dependencies] li').exists({ count: 2 });
23+
assert.dom('[data-test-build-dependencies] li').exists({ count: 1 });
24+
assert.dom('[data-test-dev-dependencies] li').exists({ count: 1 });
25+
26+
await percySnapshot(assert);
27+
await a11yAudit(axeConfig);
28+
});
29+
30+
test('empty list case', async function (assert) {
31+
this.server.create('crate', { name: 'nanomsg' });
32+
this.server.create('version', { crateId: 'nanomsg', num: '0.6.1' });
33+
34+
await visit('/crates/nanomsg/dependencies');
35+
36+
assert.dom('[data-test-no-dependencies]').exists();
37+
assert.dom('[data-test-dependencies] li').doesNotExist();
38+
assert.dom('[data-test-build-dependencies] li').doesNotExist();
39+
assert.dom('[data-test-dev-dependencies] li').doesNotExist();
40+
});
41+
42+
test('shows error message if loading of dependencies fails', async function (assert) {
43+
this.server.loadFixtures();
44+
45+
this.server.get('/api/v1/crates/:crate_name/:version_num/dependencies', {}, 500);
46+
47+
await visit('/crates/nanomsg/dependencies');
48+
assert.equal(currentURL(), '/crates/nanomsg');
49+
50+
assert
51+
.dom('[data-test-notification-message="error"]')
52+
.hasText("Failed to load the list of dependencies for the 'nanomsg' crate. Please try again later!");
53+
});
54+
});

tests/acceptance/crate-navtabs-test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { setupApplicationTest } from 'cargo/tests/helpers';
55

66
const TAB_README = '[data-test-readme-tab] a';
77
const TAB_VERSIONS = '[data-test-versions-tab] a';
8+
const TAB_DEPS = '[data-test-deps-tab] a';
89
const TAB_REV_DEPS = '[data-test-rev-deps-tab] a';
910
const TAB_SETTINGS = '[data-test-settings-tab] a';
1011

@@ -20,6 +21,7 @@ module('Acceptance | crate navigation tabs', function (hooks) {
2021

2122
assert.dom(TAB_README).hasAttribute('href', '/crates/nanomsg').hasAttribute('data-test-active');
2223
assert.dom(TAB_VERSIONS).hasAttribute('href', '/crates/nanomsg/versions').hasNoAttribute('data-test-active');
24+
assert.dom(TAB_DEPS).hasAttribute('href', '/crates/nanomsg/dependencies').hasNoAttribute('data-test-active');
2325
assert
2426
.dom(TAB_REV_DEPS)
2527
.hasAttribute('href', '/crates/nanomsg/reverse_dependencies')
@@ -31,6 +33,19 @@ module('Acceptance | crate navigation tabs', function (hooks) {
3133

3234
assert.dom(TAB_README).hasAttribute('href', '/crates/nanomsg').hasNoAttribute('data-test-active');
3335
assert.dom(TAB_VERSIONS).hasAttribute('href', '/crates/nanomsg/versions').hasAttribute('data-test-active');
36+
assert.dom(TAB_DEPS).hasAttribute('href', '/crates/nanomsg/dependencies').hasNoAttribute('data-test-active');
37+
assert
38+
.dom(TAB_REV_DEPS)
39+
.hasAttribute('href', '/crates/nanomsg/reverse_dependencies')
40+
.hasNoAttribute('data-test-active');
41+
assert.dom(TAB_SETTINGS).doesNotExist();
42+
43+
await click(TAB_DEPS);
44+
assert.equal(currentURL(), '/crates/nanomsg/0.6.1/dependencies');
45+
46+
assert.dom(TAB_README).hasAttribute('href', '/crates/nanomsg/0.6.1').hasNoAttribute('data-test-active');
47+
assert.dom(TAB_VERSIONS).hasAttribute('href', '/crates/nanomsg/versions').hasNoAttribute('data-test-active');
48+
assert.dom(TAB_DEPS).hasAttribute('href', '/crates/nanomsg/0.6.1/dependencies').hasAttribute('data-test-active');
3449
assert
3550
.dom(TAB_REV_DEPS)
3651
.hasAttribute('href', '/crates/nanomsg/reverse_dependencies')
@@ -42,6 +57,7 @@ module('Acceptance | crate navigation tabs', function (hooks) {
4257

4358
assert.dom(TAB_README).hasAttribute('href', '/crates/nanomsg').hasNoAttribute('data-test-active');
4459
assert.dom(TAB_VERSIONS).hasAttribute('href', '/crates/nanomsg/versions').hasNoAttribute('data-test-active');
60+
assert.dom(TAB_DEPS).hasAttribute('href', '/crates/nanomsg/dependencies').hasNoAttribute('data-test-active');
4561
assert
4662
.dom(TAB_REV_DEPS)
4763
.hasAttribute('href', '/crates/nanomsg/reverse_dependencies')

0 commit comments

Comments
 (0)