diff --git a/app/components/badge-build-info.js b/app/components/badge-build-info.js new file mode 100644 index 00000000000..2fe3efbaef6 --- /dev/null +++ b/app/components/badge-build-info.js @@ -0,0 +1,41 @@ +import Ember from 'ember'; + +import { formatDay } from 'cargo/helpers/format-day'; + +export default Ember.Component.extend({ + tagName: 'span', + classNames: ['build_info'], + + build_info: Ember.computed('crate.max_build_info_stable', 'crate.max_build_info_beta', 'crate.max_build_info_nightly', function() { + if (this.get('crate.max_build_info_stable')) { + return 'stable'; + } else if (this.get('crate.max_build_info_beta')) { + return 'beta'; + } else if (this.get('crate.max_build_info_nightly')) { + return 'nightly'; + } else { + return null; + } + }), + color: Ember.computed('build_info', function() { + if (this.get('build_info') === 'stable') { + return 'brightgreen'; + } else if (this.get('build_info') === 'beta') { + return 'yellow'; + } else { + return 'orange'; + } + }), + version_display: Ember.computed('build_info', 'crate.max_build_info_stable', 'crate.max_build_info_beta', 'crate.max_build_info_nightly', function() { + if (this.get('build_info') === 'stable') { + return this.get('crate.max_build_info_stable'); + } else if (this.get('build_info') === 'beta') { + return formatDay(this.get('crate.max_build_info_beta')); + } else { + return formatDay(this.get('crate.max_build_info_nightly')); + } + }), + version_for_shields: Ember.computed('version_display', function() { + return this.get('version_display').replace(/-/g, '--'); + }), +}); diff --git a/app/controllers/crate/build-info.js b/app/controllers/crate/build-info.js new file mode 100644 index 00000000000..d27182af5ee --- /dev/null +++ b/app/controllers/crate/build-info.js @@ -0,0 +1,49 @@ +import Ember from 'ember'; + +const { computed } = Ember; + +function flattenBuildInfo(buildOrdering, builds) { + if (!buildOrdering || !builds) { + return []; + } + + return buildOrdering.map(version => { + const thisVersion = builds[version]; + + return { + version, + 'x86_64-apple-darwin': thisVersion['x86_64-apple-darwin'], + 'x86_64-pc-windows-gnu': thisVersion['x86_64-pc-windows-gnu'], + 'x86_64-pc-windows-msvc': thisVersion['x86_64-pc-windows-msvc'], + 'x86_64-unknown-linux-gnu': thisVersion['x86_64-unknown-linux-gnu'], + }; + }); +} + +export default Ember.Controller.extend({ + id: computed.alias('model.crate.id'), + name: computed.alias('model.crate.name'), + version: computed.alias('model.num'), + build_info: computed.alias('model.build_info'), + stable_build: computed('build_info.ordering.stable', 'build_info.stable', function() { + const ordering = this.get('build_info.ordering.stable'); + const stable = this.get('build_info.stable'); + + return flattenBuildInfo(ordering, stable); + }), + beta_build: computed('build_info.ordering.beta', 'build_info.beta', function() { + const ordering = this.get('build_info.ordering.beta'); + const beta = this.get('build_info.beta'); + + return flattenBuildInfo(ordering, beta); + }), + nightly_build: computed('build_info.ordering.nightly', 'build_info.nightly', function() { + const ordering = this.get('build_info.ordering.nightly'); + const nightly = this.get('build_info.nightly'); + + return flattenBuildInfo(ordering, nightly); + }), + has_stable_builds: computed.gt('stable_build.length', 0), + has_beta_builds: computed.gt('beta_build.length', 0), + has_nightly_builds: computed.gt('nightly_build.length', 0), +}); diff --git a/app/helpers/build-info-pass-fail.js b/app/helpers/build-info-pass-fail.js new file mode 100644 index 00000000000..200199fc2d5 --- /dev/null +++ b/app/helpers/build-info-pass-fail.js @@ -0,0 +1,13 @@ +import Ember from 'ember'; + +export function buildInfoPassFail(status) { + if (status === true) { + return '✅ Pass'; + } else if (status === false) { + return '❌ Fail'; + } else { + return ''; + } +} + +export default Ember.Helper.helper(params => buildInfoPassFail(params[0])); diff --git a/app/helpers/format-day.js b/app/helpers/format-day.js new file mode 100644 index 00000000000..33c2d80b102 --- /dev/null +++ b/app/helpers/format-day.js @@ -0,0 +1,8 @@ +import Ember from 'ember'; +import moment from 'moment'; + +export function formatDay(date) { + return date ? moment(date).utc().format('YYYY-MM-DD') : null; +} + +export default Ember.Helper.helper(params => formatDay(params[0])); diff --git a/app/helpers/target-icons.js b/app/helpers/target-icons.js new file mode 100644 index 00000000000..bd98ae6a70f --- /dev/null +++ b/app/helpers/target-icons.js @@ -0,0 +1,10 @@ +import Ember from 'ember'; + +export function targetIcons(targets) { + return Ember.String.htmlSafe(targets.map(function(target) { + const filename = target.split(' ')[0].toLowerCase(); + return `${target}`; + }).join('')); +} + +export default Ember.Helper.helper(params => targetIcons(params[0])); diff --git a/app/models/build-info.js b/app/models/build-info.js new file mode 100644 index 00000000000..a2e1d5ea516 --- /dev/null +++ b/app/models/build-info.js @@ -0,0 +1,58 @@ +import DS from 'ember-data'; +import Ember from 'ember'; + +const TIER1 = { + 'x86_64-unknown-linux-gnu': 'Linux', + 'x86_64-apple-darwin': 'macOS', + 'x86_64-pc-windows-gnu': 'Windows (GNU)', + 'x86_64-pc-windows-msvc': 'Windows (MSVC)', +}; + +const caseInsensitive = (a, b) => a.toLowerCase().localeCompare(b.toLowerCase()); + +export default DS.Model.extend({ + version: DS.belongsTo('version', { async: true }), + ordering: DS.attr(), + stable: DS.attr(), + beta: DS.attr(), + nightly: DS.attr(), + + latest_positive_results: Ember.computed('ordering', 'stable', 'beta', 'nightly', function() { + const passingTargets = results => ( + Object.entries(results) + .filter(([, value]) => value === true) + .map(([key]) => TIER1[key]) + .sort(caseInsensitive) + ); + + const positiveResults = (versionOrdering, channelResults) => { + const latestVersion = versionOrdering[versionOrdering.length - 1]; + const latestResults = channelResults[latestVersion] || {}; + return [latestVersion, passingTargets(latestResults)]; + }; + + let results = {}; + + const addChannelToResults = (key) => { + const channelOrdering = this.get('ordering')[key]; + const channelResults = this.get(key); + + const [version, targets] = positiveResults(channelOrdering, channelResults); + + if (targets.length > 0) { + results[key] = { version, targets }; + } + }; + + addChannelToResults('stable'); + addChannelToResults('beta'); + addChannelToResults('nightly'); + + return results; + }), + + has_any_positive_results: Ember.computed('latest_positive_results', function() { + const results = this.get('latest_positive_results'); + return Object.keys(results).length > 0; + }), +}); diff --git a/app/models/crate.js b/app/models/crate.js index 9bd5893ad33..9d2e40fd78f 100644 --- a/app/models/crate.js +++ b/app/models/crate.js @@ -17,6 +17,9 @@ export default DS.Model.extend({ documentation: DS.attr('string'), repository: DS.attr('string'), exact_match: DS.attr('boolean'), + max_build_info_nightly: DS.attr('date'), + max_build_info_beta: DS.attr('date'), + max_build_info_stable: DS.attr('string'), versions: DS.hasMany('versions', { async: true }), badges: DS.attr(), diff --git a/app/models/version.js b/app/models/version.js index 3da95b4300d..6d02500a208 100644 --- a/app/models/version.js +++ b/app/models/version.js @@ -15,6 +15,7 @@ export default DS.Model.extend({ async: false }), authors: DS.hasMany('users', { async: true }), + build_info: DS.belongsTo('build-info', { async: true }), dependencies: DS.hasMany('dependency', { async: true }), version_downloads: DS.hasMany('version-download', { async: true }), diff --git a/app/router.js b/app/router.js index fb1be2e9a59..53015a9de93 100644 --- a/app/router.js +++ b/app/router.js @@ -18,6 +18,7 @@ Router.map(function() { this.route('download'); this.route('versions'); this.route('version', { path: '/:version_num' }); + this.route('build_info', { path: '/:version_num/build_info' }); this.route('reverse_dependencies'); diff --git a/app/routes/crate/build_info.js b/app/routes/crate/build_info.js new file mode 100644 index 00000000000..e31b685aa2e --- /dev/null +++ b/app/routes/crate/build_info.js @@ -0,0 +1,24 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({ + model(params) { + const requestedVersion = params.version_num; + const crate = this.modelFor('crate'); + + // Find version model + return crate.get('versions') + .then(versions => { + const version = versions.find(version => version.get('num') === requestedVersion); + if (!version) { + this.controllerFor('application').set('nextFlashError', + `Version '${requestedVersion}' of crate '${crate.get('name')}' does not exist`); + } + return version; + }); + }, + + serialize(model) { + let version_num = model ? model.get('num') : ''; + return { version_num }; + }, +}); diff --git a/app/styles/crate.scss b/app/styles/crate.scss index 5eae23ec0c5..00dd2aa9f49 100644 --- a/app/styles/crate.scss +++ b/app/styles/crate.scss @@ -352,6 +352,35 @@ } } +.build-info { + .build-info-channel { + margin: 10px 0; + img { + width: 20px; + margin-bottom: -3px; + } + } +} + +#crate-build-info { + padding-bottom: 50px; + border-bottom: 5px solid $gray-border; + margin-bottom: 30px; + + .description { + margin-bottom: 30px; + } + + table { + border: 1px solid $gray-border; + td, th { + border: 1px solid $gray-border; + padding: 5px 10px; + text-align: left; + } + } +} + #crate-downloads { @include display-flex; @include flex-wrap(wrap); diff --git a/app/templates/components/badge-build-info.hbs b/app/templates/components/badge-build-info.hbs new file mode 100644 index 00000000000..f0a6cb8d356 --- /dev/null +++ b/app/templates/components/badge-build-info.hbs @@ -0,0 +1,6 @@ +{{#if build_info}} +Known to build on {{ build_info }} {{ version_display }} +{{/if}} diff --git a/app/templates/components/crate-row.hbs b/app/templates/components/crate-row.hbs index 8337a333805..12b06c61421 100644 --- a/app/templates/components/crate-row.hbs +++ b/app/templates/components/crate-row.hbs @@ -11,6 +11,7 @@ {{#each crate.annotated_badges as |badge|}} {{component badge.component_name badge=badge data-test-badge=badge.badge_type}} {{/each}} + {{badge-build-info crate=crate}}
diff --git a/app/templates/crate/build-info.hbs b/app/templates/crate/build-info.hbs new file mode 100644 index 00000000000..05b337d6d2c --- /dev/null +++ b/app/templates/crate/build-info.hbs @@ -0,0 +1,83 @@ +{{title name}} + +
+ {{#link-to 'crate' id}}⬅ Back to {{ name }}{{/link-to}} +
+ +
+
+
+ +

Build info for {{ name }}

+

{{ version }}

+
+
+
+ +
+ {{#if has_stable_builds}} +

Stable channel

+ + + + + + + + + {{#each stable_build as |build|}} + + + + + + + + {{/each}} +
Rust VersionLinux 64 bitmacOS 64 bitWindows (GNU) 64 bitWindows (MSVC) 64 bit
{{ build.version }}{{ build-info-pass-fail build.x86_64-unknown-linux-gnu }}{{ build-info-pass-fail build.x86_64-apple-darwin }}{{ build-info-pass-fail build.x86_64-pc-windows-gnu }}{{ build-info-pass-fail build.x86_64-pc-windows-msvc }}
+ {{/if}} + + {{#if has_beta_builds}} +

Beta channel

+ + + + + + + + + {{#each beta_build as |build|}} + + + + + + + + {{/each}} +
Rust VersionLinux 64 bitmacOS 64 bitWindows (GNU) 64 bitWindows (MSVC) 64 bit
{{ format-day build.version }}{{ build-info-pass-fail build.x86_64-unknown-linux-gnu }}{{ build-info-pass-fail build.x86_64-apple-darwin }}{{ build-info-pass-fail build.x86_64-pc-windows-gnu }}{{ build-info-pass-fail build.x86_64-pc-windows-msvc }}
+ {{/if}} + + {{#if has_nightly_builds}} +

Nightly channel

+ + + + + + + + + {{#each nightly_build as |build|}} + + + + + + + + {{/each}} +
Rust VersionLinux 64 bitmacOS 64 bitWindows (GNU) 64 bitWindows (MSVC) 64 bit
{{ format-day build.version }}{{ build-info-pass-fail build.x86_64-unknown-linux-gnu }}{{ build-info-pass-fail build.x86_64-apple-darwin }}{{ build-info-pass-fail build.x86_64-pc-windows-gnu }}{{ build-info-pass-fail build.x86_64-pc-windows-msvc }}
+ {{/if}} +
diff --git a/app/templates/crate/version.hbs b/app/templates/crate/version.hbs index 32f6cb4bf06..5af909a80d0 100644 --- a/app/templates/crate/version.hbs +++ b/app/templates/crate/version.hbs @@ -123,6 +123,37 @@ {{/each}}
+ {{#if currentVersion.build_info.has_any_positive_results }} +
+

Works on

+ + {{#if currentVersion.build_info.latest_positive_results.stable }} +
+ Stable {{ currentVersion.build_info.latest_positive_results.stable.version }} + on {{ target-icons currentVersion.build_info.latest_positive_results.stable.targets }} +
+ {{/if}} + + {{#if currentVersion.build_info.latest_positive_results.beta }} +
+ Beta {{ format-day currentVersion.build_info.latest_positive_results.beta.version }} + on {{ target-icons currentVersion.build_info.latest_positive_results.beta.targets }} +
+ {{/if}} + + {{#if currentVersion.build_info.latest_positive_results.nightly }} +
+ Nightly {{ format-day currentVersion.build_info.latest_positive_results.nightly.version }} + on {{ target-icons currentVersion.build_info.latest_positive_results.nightly.targets }} +
+ {{/if}} + + {{#link-to 'crate.build_info' currentVersion.num}} + More build info + {{/link-to}} +
+ {{/if}} +

Authors