#112 coreos-download: restyling coreos download page
Merged 3 years ago by dustymabe. Opened 4 years ago by abai.
fedora-web/ abai/websites coreos-download  into  master

@@ -6,26 +6,7 @@ 

  

  {% block content %}

  {{submenu(edition="coreos", active="download")}}

- <div class="bg-tranparent py-5">

-   <div class="container">

-     <div class="row align-items-center">

-       <div class="col-12">

-         <h1 class="font-weight-light">{% trans trimmed %}Download Fedora CoreOS.{% endtrans %}</h1>

-         <h5 class="font-weight-light">{% trans trimmed %}Run your containers today in the latest release of Fedora CoreOS.{% endtrans %}</h5>

-         <p>{% trans trimmed mailing_link_start='<a href="https://lists.fedoraproject.org/archives/list/coreos-status@lists.fedoraproject.org/">'|safe, tracker_link_start='<a href="https://github.com/coreos/fedora-coreos-tracker">'|safe, link_end='</a>'|safe %}Subscribe to the {{ mailing_link_start}}coreos-status mailing list{{ link_end }} to receive important operational notices from the Fedora CoreOS team. Give feedback and follow Fedora CoreOS development in the {{ tracker_link_start }}Fedora CoreOS issue tracker{{ link_end }}.{% endtrans %}</p>

-         <p>

-           {% trans trimmed docs_link_start='<a href="https://docs.fedoraproject.org/en-US/fedora-coreos/getting-started/">'|safe, link_end='</a>'|safe %}

-           Start running Fedora CoreOS with the {{ docs_link_start }}Getting Started{{ link_end }} guide.

-           {% endtrans %}

-         </p>

-       </div>

-     </div>

-     <br>

-   </div>

-   <div class="container">

-     <div id="coreos-download-app"></div>

-   </div>

- </div>

+ <div id="coreos-download-app"></div>

  {% endblock %}

  

  {% block js %}

@@ -5,38 +5,44 @@ 

  

  {% block content %}

  {{submenu(edition="coreos", active="overview")}}

- <div class="bg-tranparent">

+ <div class="bg-tranparent font-weight-light">

    <div class="container">

-     <div class="row align-items-center">

-       <div class="col-12 pt-5">

-         <h1 class="font-weight-light">Fedora CoreOS.</h1>

-         <h5 class="font-weight-light">{% trans trimmed %}Automatically updating Linux OS for containerized workloads.{% endtrans %}</h5>

-         <a class="btn btn-lg btn-outline-fedora-magenta mt-2" href="{{url_for('coreos_download', lang_code=lang_code)}}">{% trans trimmed %}Download Now{% endtrans %}</a>

+     <div class="row align-items-center my-5 pt-3">

+       <div class="col-md-8">

+         <h1 class="font-weight-light text-left mb-3">Getting Started with Fedora CoreOS</h1>

+         <h5 class="font-weight-light pr-3">

+           {% trans trimmed %}

+           Fedora CoreOS is an automatically-updating, minimal operating system for running containerized workloads securely and at scale. It is currently available on multiple platforms, with more coming soon.

+           {% endtrans %}

+         </h5>

        </div>

-     </div>

-   </div>

- </div>

- <div class="bg-white pt-5">

- </div>

- <div class="bg-light border py-5">

-   <div class="container">

-     <div class="row">

-       <div class="col-12">

-         {% trans trimmed %}

-         Fedora CoreOS is an automatically-updating, minimal operating system for running containerized workloads securely and at scale. It is currently available on multiple platforms, with more coming soon.

+       <div class="col-md-4">

+         <a class="btn btn-lg btn-outline-fedora-magenta my-3 col-12" href="{{url_for('coreos_download', lang_code=lang_code)}}">{% trans trimmed %}Download Now{% endtrans %}</a>

+       </div>

+       <div class="col-12 mt-5">

+         <p class="mb-1">

+         {% trans trimmed docs_link_start='<a href="https://docs.fedoraproject.org/en-US/fedora-coreos/getting-started/">'|safe, mailing_link_start='<a href="https://lists.fedoraproject.org/archives/list/coreos-status@lists.fedoraproject.org/">'|safe, tracker_link_start='<a href="https://github.com/coreos/fedora-coreos-tracker">'|safe, link_end='</a>'|safe %}

+           Subscribe to the {{ mailing_link_start}}coreos-status mailing list{{ link_end }} to receive important operational notices from the Fedora CoreOS team.

+           Give feedback and follow Fedora CoreOS development in the {{ tracker_link_start }}Fedora CoreOS issue tracker{{ link_end }}.

+           To get started with running Fedora CoreOS, see the {{ docs_link_start }}Getting Started{{ link_end }} guide.

          {% endtrans %}

+         </p>

        </div>

      </div>

    </div>

  </div>

- 

- <div class="bg-gray-200 py-5">

-   <div class="container">

-     <h3 class="font-weight-light">Getting Started with Fedora CoreOS</h3>

-     {% trans trimmed docs_link_start='<a href="https://docs.fedoraproject.org/en-US/fedora-coreos/getting-started/">'|safe, link_end='</a>'|safe %}

-     To get started with running Fedora CoreOS, see the {{ docs_link_start }}Getting Started{{ link_end }} guide.

-     {% endtrans %}

+ <div class="bg-white font-weight-light">

+   <div class="container my-5">

+     <h1 class="font-weight-light text-left mb-4">Release Notes</h1>

+     <div id="coreos-release-notes"></div>

    </div>

  </div>

+ {% endblock %}

  

+ {% block js %}

+ <!-- Use the Vue runtime build only,to avoid use of eval() in the full build,

+   which is blocked by the CSP. -->

+ <script src="{{ url_for('static', filename='js/vue.runtime.min.js') }}"></script>

+ <script src="{{ url_for('static', filename='js/coreos-release-notes.js') }}"></script>

+ {{ super() }}

  {% endblock %}

@@ -43,6 +43,9 @@ 

    "metal_virtualized": "metal-virtualized",

    "cloud_operators": "cloud-operators"

  }

+ function isEmptyObj(obj) {

+   return Object.entries(obj).length === 0 && obj.constructor === Object;

+ }

  function getMember(obj, member) {

    return (member in obj) ? obj[member] : null;

  }
@@ -94,6 +97,8 @@ 

      streamUrl: "",

      // fetched {stream, metadata, architectures, updates} object from stream.json

      streamData: null,

+     // fetched {stream, metadata, architectures, updates} object from stream.json for all streams

+     streamDataAll: { stable: {}, testing: {}, next: {} },

      loading: false,

      // loaded stream data to render

      streamDisplay: {
@@ -103,9 +108,48 @@ 

        cloud: {}

      },

    },

-   watch: { stream: function() {

-     this.refreshStream();

-   } },

+   watch: {

+     stream: function() {

+       this.refreshStream();

+     },

+     // watching nested data: https://stackoverflow.com/a/46331968

+     "streamDataAll.stable": function(newVal, oldVal) {

+       if (isEmptyObj(this.streamDataAll.stable)) {

+         return

+       }

+ 

+       stableReleaseVersion = "v " + this.streamDataAll.stable.architectures.x86_64.artifacts.metal.release;

+       $("#stable-version").text(stableReleaseVersion);

+ 

+       $("#stable-json").empty();

+       $("#stable-json").append(`<a class="text-gray-600" href="${baseUrl}/stable.json">JSON</a>`);

+       $("#stable-json").append(` — <span>${this.timeSince(this.streamDataAll.stable.metadata['last-modified'])}</span>`);

+     },

+     "streamDataAll.testing": function(newVal, oldVal) {

+       if (isEmptyObj(this.streamDataAll.testing)) {

+         return

+       }

+ 

+       testingReleaseVersion = isEmptyObj(this.streamDataAll.testing) ? "" : "v " + this.streamDataAll.testing.architectures.x86_64.artifacts.metal.release;

+       $("#testing-version").text(testingReleaseVersion);

+ 

+       $("#testing-json").empty();

+       $("#testing-json").append(`<a class="text-gray-600" href="${baseUrl}/testing.json">JSON</a>`);

+       $("#testing-json").append(` — <span>${this.timeSince(this.streamDataAll.testing.metadata['last-modified'])}</span>`);

+     },

+     "streamDataAll.next": function(newVal, oldVal) {

+       if (isEmptyObj(this.streamDataAll.next)) {

+         return

+       }

+ 

+       nextReleaseVersion = isEmptyObj(this.streamDataAll.next) ? "" : "v " + this.streamDataAll.next.architectures.x86_64.artifacts.metal.release;

+       $("#next-version").text(nextReleaseVersion);

+ 

+       $("#next-json").empty();

+       $("#next-json").append(`<a class="text-gray-600" href="${baseUrl}/next.json">JSON</a>`);

+       $("#next-json").append(` — <span>${this.timeSince(this.streamDataAll.next.metadata['last-modified'])}</span>`);

+     }

+   },

    methods: {

      getObjectUrl: function(path) {

        return getArtifactUrl(this.streamUrl, path);
@@ -137,70 +181,196 @@ 

          return stringize(Math.floor(elapsed/msPerYear), "year");

        }

      },

+     // Callback function for the navigation bar

+     // Effects:

+     // - hides the other tabs other than the clicked one

+     // - replace the current URL parameter with the clicked one

      toggleHidden: function(e) {

-       const id_list = Object.values(IdPool);

+       const idList = Object.values(IdPool);

        Object.entries(tabInnerText).map(pair => {

          const key = pair[0];

          const val = pair[1];

          if (val === e.target.innerText) {

            const downloadPageUrl = window.location.href.match(/^.*\/coreos\/download/)[0];

-           history.pushState(null, null, `${downloadPageUrl}?tab=${key}&stream=${coreos_download_app.stream}`);

-           const show_id = IdPool[key];

-           id_list.map(id => document.getElementById(id).hidden = (id !== show_id));

-           this.shownId = show_id;

+           history.replaceState(null, null, `${downloadPageUrl}?tab=${key}&stream=${coreos_download_app.stream}`);

+           const showId = IdPool[key];

+           idList.map(id => document.getElementById(id).hidden = (id !== showId));

+           this.shownId = showId;

          }

        });

      },

+     // Render a navbar section

      getNavbar: function(h) {

-       cloud_icon = h('i', { class: "fas fa-cloud mr-2" })

-       nav_cloud_launchable_btn = h('button', { class: "nav-link col-12 h-100 overflow-hidden".concat(this.shownId === IdPool.cloud_launchable ? " active" : ""), attrs: { "data-toggle": "tab" }, on: { click: this.toggleHidden } }, [ cloud_icon, tabInnerText.cloud_launchable ]);

-       nav_cloud_launchable = h('li', { class: "nav-item col-4" }, [ nav_cloud_launchable_btn ]);

+       cloudIcon = h('i', { class: "fas fa-cloud mr-2" })

+       navCloudLaunchableBtn = h('button', { class: "nav-link col-12 h-100 overflow-hidden".concat(this.shownId === IdPool.cloud_launchable ? " active" : ""), attrs: { "data-toggle": "tab" }, on: { click: this.toggleHidden } }, [ cloudIcon, tabInnerText.cloud_launchable ]);

+       navCloudLaunchable = h('li', { class: "nav-item col-12 col-sm-4" }, [ navCloudLaunchableBtn ]);

  

-       server_icon = h('i', { class: "fas fa-server mr-2" })

-       nav_metal_virt_btn = h('button', { class: "nav-link col-12 h-100 overflow-hidden".concat(this.shownId === IdPool.metal_virtualized ? " active" : ""), attrs: { "data-toggle": "tab" }, on: { click: this.toggleHidden } }, [ server_icon, tabInnerText.metal_virtualized ]);

-       nav_metal_virt = h('li', { class: "nav-item col-4" }, [ nav_metal_virt_btn ]);

+       serverIcon = h('i', { class: "fas fa-server mr-2" })

+       navMetalVirtBtn = h('button', { class: "nav-link col-12 h-100 overflow-hidden".concat(this.shownId === IdPool.metal_virtualized ? " active" : ""), attrs: { "data-toggle": "tab" }, on: { click: this.toggleHidden } }, [ serverIcon, tabInnerText.metal_virtualized ]);

+       navMetalVirt = h('li', { class: "nav-item col-12 col-sm-4" }, [ navMetalVirtBtn ]);

  

-       cloud_upload_icon = h('i', { class: "fas fa-cloud-upload-alt mr-2" })

-       nav_cloud_operators_btn = h('button', { class: "nav-link col-12 h-100 overflow-hidden".concat(this.shownId === IdPool.cloud_operators ? " active" : ""), attrs: { "data-toggle": "tab" }, on: { click: this.toggleHidden } }, [ cloud_upload_icon, tabInnerText.cloud_operators ]);

-       nav_cloud_operators = h('li', { class: "nav-item col-4" }, [ nav_cloud_operators_btn ]);

+       cloudUploadIcon = h('i', { class: "fas fa-cloud-upload-alt mr-2" })

+       navCloudOperatorsBtn = h('button', { class: "nav-link col-12 h-100 overflow-hidden".concat(this.shownId === IdPool.cloud_operators ? " active" : ""), attrs: { "data-toggle": "tab" }, on: { click: this.toggleHidden } }, [ cloudUploadIcon, tabInnerText.cloud_operators ]);

+       navCloudOperators = h('li', { class: "nav-item col-12 col-sm-4" }, [ navCloudOperatorsBtn ]);

  

-       navbar = h('ul', { class: "nav nav-tabs" }, [ nav_cloud_launchable, nav_metal_virt, nav_cloud_operators ]);

+       navbar = h('ul', { class: "nav nav-tabs" }, [ navCloudLaunchable, navMetalVirt, navCloudOperators ]);

        return navbar;

      },

-     // Add dropdown options of streams

+     // Render a dropdown list with options of streams

      getStreamName: function(h) {

        const self = this;

        if (this.streamData === null) return;

-       option_stable = h('option', { attrs: { value: "stable", selected: this.stream === "stable" ? "selected" : null }}, "stable");

-       option_testing = h('option', { attrs: { value: "testing", selected: this.stream === "testing" ? "selected" : null }}, "testing");

-       option_next = h('option', { attrs: { value: "next", selected: this.stream === "next" ? "selected" : null }}, "next");

-       selectOptions = h('select', {

-         class: "mx-1",

+ 

+       function onClick(e) {

+         const downloadPageUrl = window.location.href.match(/^.*\/coreos\/download/)[0];

+         const currentShownKey = Object.keys(IdPool).find(key => IdPool[key] === self.shownId);

+         coreos_download_app.stream = e.target.value;

+         history.replaceState(null, null, `${downloadPageUrl}?tab=${currentShownKey}&stream=${coreos_download_app.stream}`);

+       }

+ 

+       btnDropdownToggle = h('button', { class: "btn btn-sm bg-gray-200 dropdown-toggle py-0", attrs: { type: "button", id: "dropdownMenuStreams", "data-toggle": "dropdown", "aria-haspopup": true, "aria-expanded": false } }, self.stream );

+       btnDropdownStable = h('button', { class: "dropdown-item", attrs: { type: "button", value: "stable" }, on: { click: onClick } }, "stable");

+       btnDropdownTesting = h('button', { class: "dropdown-item", attrs: { type: "button", value: "testing" }, on: { click: onClick } }, "testing");

+       btnDropdownNext = h('button', { class: "dropdown-item", attrs: { type: "button", value: "next" }, on: { click: onClick } }, "next");

+       divDropdownMenu = h('div', { class: "dropdown-menu", attrs: { "aria-labelledby": "dropdownMenuStreams" } }, [ btnDropdownStable, btnDropdownTesting, btnDropdownNext ]);

+       divDropdown = h('div', { class: "d-inline dropdown ml-2" }, [ btnDropdownToggle, divDropdownMenu ])

+ 

+       streamName = h('p', { class: "mt-5 mb-2 ml-3" }, [

+         "Currently Selected Stream:",

+         divDropdown ]);

+       return streamName;

+     },

+     // Introduction section for streams, the section above the navigation bar for platforms

+     getStreamIntro: function(h) {

+       const overviewPageUrl = window.location.href.match(/^.*\/coreos/)[0];

+       title = h('h2', { class: "font-weight-light text-center pb-3 mb-3" }, "Fedora CoreOS is available across 3 different release streams:");

+ 

+       if (this.loading) {

+         return title;

+       }

+ 

+       viewAllStreamsBtn = h('button',

+       {

+         class: "d-block mx-auto mb-5 py-1 btn btn-sm btn-fedora-purple",

+         on: {

+           click: function(e) {

+             e.preventDefault();

+             window.open(`${overviewPageUrl}`);

+           }

+         }

+       }, "View All Streams");

+ 

+       // Release info section with three tabs: stable, testing, next

+       // NOTE: in order for the button line up at the same horizontal level, use a fixed height `9em` for <p> elements of stream summaries.

+       stableIcon = h("i", {

+         class: "fas fa-shield-alt fa-2x rounded-circle bg-fedora-blue text-white p-3 ml-4",

+       }, "");

+       stableHeading = h("h3", { class: "font-weight-light" }, "Stable");

+       stableReleaseVersion = h("h6", { class: "text-gray-500 mb-0", attrs: { id: "stable-version" }}, isEmptyObj(this.streamDataAll.stable) ? "" : "v " + this.streamDataAll.stable.architectures.x86_64.artifacts.metal.release);

+       stableJSON = h('p', { class: "text-gray-500", attrs: { id: "stable-json" } }, [

+         h('span', {}, [

+           h('a', { class: "font-weight-bold text-gray-500", attrs: { href: `${baseUrl}/stable.json` } }, "JSON")

+         ]),

+         (isEmptyObj(this.streamDataAll.stable)) ? null : " — ",

+         (isEmptyObj(this.streamDataAll.stable)) ? null : h('span', { class: "font-weight-normal" }, this.timeSince(this.streamDataAll.stable.metadata['last-modified']))

+       ]);

+       stableIconContainer = h("div", { class: "col-4" }, [ stableIcon ])

+       stableReleaseJSONContainer = h("div", { class: "col-8" }, [ stableHeading, stableReleaseVersion, stableJSON ])

+       stableHeadingContainer = h("div", { class: "row" }, [ stableIconContainer, stableReleaseJSONContainer ])

+ 

+       stableIntroText = h("p", { class: "pl-3 pr-2", style: { height: "9em" } }, "The Stable Stream should be used by production clusters. Versions of Fedora CoreOS are battle-tested within the Testing and Next streams before being promoted.");

+       stableReleaseLink = h('button',

+       {

+         class: "d-block mx-auto mb-4 py-1 btn btn-sm btn-fedora-blue",

          on: {

-           change: function(e) {

-             const downloadPageUrl = window.location.href.match(/^.*\/coreos\/download/)[0];

-             const currentShownKey = Object.keys(IdPool).find(key => IdPool[key] === self.shownId);

-             coreos_download_app.stream = e.target.value;

-             history.pushState(null, null, `${downloadPageUrl}?tab=${currentShownKey}&stream=${coreos_download_app.stream}`);

+           click: function(e) {

+             e.preventDefault();

+             window.open(`${overviewPageUrl}?stream=stable`);

            }

          }

-       }, [

-         option_stable,

-         option_testing,

-         option_next

+       }, "View Stable Releases");

+ 

+       // then Testing stream

+       testingIcon = h("i", {

+         class: "fas fa-flask fa-2x rounded-circle bg-fedora-green text-white p-3 ml-4",

+       }, "");

+       testingHeading = h("h3", { class: "font-weight-light" }, "Testing");

+       testingReleaseVersion = h("h6", { class: "text-gray-500 mb-0", attrs: { id: "testing-version" }}, isEmptyObj(this.streamDataAll.testing) ? "" : "v " + this.streamDataAll.testing.architectures.x86_64.artifacts.metal.release);

+       testingJSON = h('p', { class: "text-gray-500", attrs: { id: "testing-json" } }, [

+         h('span', {}, [

+           h('a', { class: "font-weight-bold text-gray-500", attrs: { href: `${baseUrl}/testing.json` } }, "JSON")

+         ]),

+         (isEmptyObj(this.streamDataAll.testing)) ? null : " — ",

+         (isEmptyObj(this.streamDataAll.testing)) ? null : h('span', { class: "font-weight-normal" }, this.timeSince(this.streamDataAll.testing.metadata['last-modified']))

        ]);

-       streamName = h('p', {}, [

-         "Stream: ",

-         selectOptions,

-         " (",

+       testingIconContainer = h("div", { class: "col-4" }, [ testingIcon ])

+       testingReleaseJSONContainer = h("div", { class: "col-8" }, [ testingHeading, testingReleaseVersion, testingJSON ])

+       testingHeadingContainer = h("div", { class: "row" }, [ testingIconContainer, testingReleaseJSONContainer ])

+ 

+       testingIntroText = h("p", { class: "pl-3 pr-2", style: { height: "9em" } }, "The Testing stream consists of promoted Next releases. Mix a few Testing machines into your production clusters to catch any bugs specific to your hardware or configuration.");

+       testingReleaseLink = h('button',

+       {

+         class: "d-block mx-auto mb-4 py-1 btn btn-sm btn-fedora-green",

+         on: {

+           click: function(e) {

+             e.preventDefault();

+             window.open(`${overviewPageUrl}?stream=testing`);

+           }

+         }

+       }, "View Testing Releases");

+ 

+       // then Next stream

+       nextIcon = h("i", {

+         class: "fas fa-layer-group fa-2x rounded-circle bg-fedora-orange text-white p-3 ml-4",

+       }, "");

+       nextHeading = h("h3", { class: "font-weight-light" }, "Next");

+       nextReleaseVersion = h("h6", { class: "text-gray-500 mb-0", attrs: { id: "next-version" }}, isEmptyObj(this.streamDataAll.next) ? "" : "v " + this.streamDataAll.next.architectures.x86_64.artifacts.metal.release);

+       nextJSON = h('p', { class: "text-gray-500", attrs: { id: "next-json" } }, [

          h('span', {}, [

-           h('a', { attrs: { href: this.getObjectUrl(this.stream + '.json') } }, "JSON")

+           h('a', { class: "font-weight-bold text-gray-500", attrs: { href: `${baseUrl}/next.json` } }, "JSON")

          ]),

-         ")",

-         (this.streamData.metadata) ? "—" : null,

-         (this.streamData.metadata) ? h('span', {}, this.timeSince(this.streamData.metadata['last-modified'])) : null

+         (isEmptyObj(this.streamDataAll.next)) ? null : " — ",

+         (isEmptyObj(this.streamDataAll.next)) ? null : h('span', { class: "font-weight-normal" }, this.timeSince(this.streamDataAll.next.metadata['last-modified']))

        ]);

-       return streamName;

+       nextIconContainer = h("div", { class: "col-4" }, [ nextIcon ])

+       nextReleaseJSONContainer = h("div", { class: "col-8" }, [ nextHeading, nextReleaseVersion, nextJSON ])

+       nextHeadingContainer = h("div", { class: "row" }, [ nextIconContainer, nextReleaseJSONContainer ])

+ 

+ 

+       nextIntroText = h("p", { class: "pl-3 pr-2", style: { height: "9em" } }, "The Next stream closely tracks current development work and is released frequently. The newest versions of the Linux kernel, Systemd, and other components will be available for testing.");

+       nextReleaseLink = h('button',

+       {

+         class: "d-block mx-auto mb-4 py-1 btn btn-sm btn-fedora-orange",

+         on: {

+           click: function(e) {

+             e.preventDefault();

+             window.open(`${overviewPageUrl}?stream=next`);

+           }

+         }

+       }, "View Next Releases");

+ 

+       stableDiv = h('div', {

+         class: "col-12 col-lg-4 border-left border-fedora-blue pt-3",

+         style: {

+           "border-width": "10px !important",

+         }

+       }, [stableHeadingContainer, stableIntroText, stableReleaseLink])

+       testingDiv = h('div', {

+         class: "col-12 col-lg-4 border-left border-fedora-green pt-3",

+         style: {

+           "border-width": "10px !important",

+         }

+       }, [testingHeadingContainer, testingIntroText, testingReleaseLink])

+       nextDiv = h('div', {

+         class: "col-12 col-lg-4 border-left border-fedora-orange pt-3",

+         style: {

+           "border-width": "10px !important",

+         }

+       }, [nextHeadingContainer, nextIntroText, nextReleaseLink])

+ 

+       streamsIntroDiv = h('div', { class: "row my-3" }, [stableDiv, testingDiv, nextDiv]);

+       wrapperDiv = h('div', {}, [title, viewAllStreamsBtn, streamsIntroDiv]);

+       return wrapperDiv;

      },

      isAws: function(platform) {

        return platform == "aws";
@@ -318,15 +488,32 @@ 

          }

        }

      },

+     // Load all stream metadata for rendering

      refreshStream: function() {

-       this.loading = true

-       this.streamUrl = baseUrl

-       fetchStreamData(this.streamUrl, this.stream).then(streamData => {

-         this.loading = false;

-         this.streamData = Object.entries(streamData).length === 0 && streamData.constructor === Object ? null : streamData;

-         this.loadStreamDisplay();

-       });

+       const self = this;

+       self.loading = true

+       self.streamUrl = baseUrl

+       fetchStreamData(baseUrl, "stable")

+       .then(streamData => {

+         self.streamDataAll.stable = streamData;

+         return fetchStreamData(baseUrl, "testing");

+       })

+       .then(streamData => {

+         self.streamDataAll.testing = streamData;

+         return fetchStreamData(baseUrl, "next");

+       })

+       .then(streamData => {

+         self.streamDataAll.next = streamData;

+         return;

+       })

+       .then(() =>{

+         const streamData = self.streamDataAll[self.stream];

+         self.loading = false;

+         self.streamData = isEmptyObj(streamData) ? null : streamData;

+         self.loadStreamDisplay();

+       })

      },

+     // Render the `Verify signature & SHA256` modal template

      getSignatureAndShaModal: function(h) {

        return h('div', { class: "modal", attrs: { id: "signatureAndShaModal", tabindex: "-1", role: "dialog", "aria-labelledby": "signatureAndShaModalLabel", "aria-hidden": "true" }}, [

          h('div', { class: "modal-dialog modal-lg modal-dialog-centered", attrs: { role: "document" }}, [
@@ -351,6 +538,9 @@ 

      }

    },

    render: function(h) {

+     if(window.location.href.match(/^.*\/coreos\/download/) == null) {

+       return

+     }

      const downloadPageUrl = window.location.href.match(/^.*\/coreos\/download/)[0];

      searchParams = new URLSearchParams(window.location.search);

      // switch to specified tab if `tab` parameter is set
@@ -390,15 +580,17 @@ 

        searchParams.set('stream', 'stable');

      }

      // Update the url with the parameters

-     history.pushState(null, null, `${downloadPageUrl}?${searchParams.toString()}`);

+     history.replaceState(null, null, `${downloadPageUrl}?${searchParams.toString()}`);

  

-     var signature_sha256_verification_modal = this.getSignatureAndShaModal(h);

-     var stream_select_container = h('div', { class: "pb-0 pt-3 mb-3" }, [ h('div', { class: "container" }, [ this.getStreamName(h), this.getNavbar(h) ]) ]);

+     var signatureSha256VerificationModal = this.getSignatureAndShaModal(h);

+     h1Title = h('h1', { class: "font-weight-light text-center my-5" }, "Download Fedora CoreOS");

+     streamSelectContainer = h('div', { class: "pb-0 pt-3 mb-3" }, [ this.getStreamIntro(h), this.getStreamName(h), this.getNavbar(h) ]);

      if (this.loading) {

-       return h('div', {}, [ stream_select_container, "Loading..."] );

+       streamInfoDiv = h('div', { class: "bg-light pt-3" }, [ h('div', { class: "container font-weight-light" }, [ streamSelectContainer ]) ]);

+       downloadDiv = h('div', { class: "bg-white pb-5" }, [ h('div', { class: "container font-weight-light" }, "Loading...") ]);

+       return h('div', {}, [ h1Title, streamInfoDiv, downloadDiv ]);

      }

      else if (this.streamData) {

-       cloudLaunchableTitle = h('h3', { class:"font-weight-light" }, "Cloud Launchable");

        cloudLaunchableSection = {};

        cloudLaunchable = {};

        virtualizedTitle = h('h3', { class:"font-weight-light" }, "Virtualized");
@@ -407,7 +599,6 @@ 

        bareMetalTitle = h('h3', { class:"font-weight-light" }, "Bare Metal");

        bareMetalSection = {};

        bareMetal = {};

-       cloudTitle = h('h3', { class:"font-weight-light" }, "For Cloud Operators");

        cloudSection = {};

        cloud = {};

  
@@ -450,6 +641,11 @@ 

        cloudLaunchable = h('div', { class: "col-12 py-2 my-2" }, [ cloudLaunchableSection ]);

  

        function createDownloadsSubSection(displayDownloads, contentType, showTitle, imageType) {

+         verifyBlurb =

+         `<div class="mb-3">

+           Verify your download using the detached signature after importing <a href="https://getfedora.org/security/">Fedora's GPG signing keys</a>.

+           The detached signature is for the released artifact itself. If there is a good signature from one of the Fedora keys, and the SHA256 checksum matches, then the download is valid.

+         </div>`

          return displayDownloads ? h('div', { class: "pb-2" }, [

            showTitle ? h('span', {}, contentType + ": ") : null,

            displayDownloads.location ? h('span', {}, [
@@ -466,8 +662,8 @@ 

                    else {

                      $("#modal-body").empty();

                      let p = document.createElement('p');

-                     let a_checksum = null;

-                     let a_signature = null;

+                     let aChecksum = null;

+                     let aSignature = null;

                      // Show SHA256 and initialize the <a> tags if data is available

                      if(displayDownloads.sha256) {

                        let d = document.createElement('div');
@@ -475,27 +671,28 @@ 

                            .html("SHA256: " + displayDownloads.sha256)

                            .appendTo(p);

  

-                       a_checksum = document.createElement('a');

-                       $(a_checksum).attr("href", "data:text/plain;charset=utf-8," + encodeURIComponent("SHA256 (" + getFilename(displayDownloads.location) + ") = " + displayDownloads.sha256))

+                       aChecksum = document.createElement('a');

+                       $(aChecksum).attr("href", "data:text/plain;charset=utf-8," + encodeURIComponent("SHA256 (" + getFilename(displayDownloads.location) + ") = " + displayDownloads.sha256))

                                     .attr("download", getFilename(displayDownloads.location) + "-CHECKSUM")

                                     .html("checksum file");

                      }

                      if(displayDownloads.signature) {

-                       a_signature = document.createElement('a');

-                       $(a_signature).attr("href", displayDownloads.signature)

+                       aSignature = document.createElement('a');

+                       $(aSignature).attr("href", displayDownloads.signature)

                                      .html("signature");

                      }

                      $(p).appendTo("#modal-body");

+                     $(verifyBlurb).appendTo("#modal-body");

  

                      // Download the Checksum file and Signature

                      let ol = document.createElement('ol');

-                     if (a_checksum || a_signature) {

+                     if (aChecksum || aSignature) {

                        let li = document.createElement('li');

                        p = document.createElement('p');

                        $(p).append("Download the ")

-                           .append(a_checksum)

-                           .append(a_checksum && a_signature ? " and " : "")

-                           .append(a_signature);

+                           .append(aChecksum)

+                           .append(aChecksum && aSignature ? " and " : "")

+                           .append(aSignature);

                        $(p).appendTo(li);

                        $(li).appendTo(ol);

                      }
@@ -594,31 +791,28 @@ 

        }

        cloud = h('div', { class: "col-12 py-2 my-2" }, [ cloudSection ]);

  

-       verifyBlurb = h('div', {}, [

-         h('div', { class:"font-weight-light" }, [

-           "Verify your download using the detached signature after importing ",

-           h('a', { attrs: { href: "https://getfedora.org/security/" } }, "Fedora's GPG signing keys"),

-           ". The detached signature is for the released artifact itself. If there is a good signature from one of the Fedora keys, and the SHA256 checksum matches, then the download is valid."

-         ])

-       ]);

+       let bareMetalContainer = h('div', { class: "col-lg-6" }, [ bareMetalTitle, bareMetal ]);

+       let virtualizedContainer = h('div', { class: "col-lg-6" }, [ virtualizedTitle, virtualized ]);

  

-       let bare_metal_container = h('div', { class: "col-6" }, [ bareMetalTitle, verifyBlurb, bareMetal ]);

-       let virtualized_container = h('div', { class: "col-6" }, [ virtualizedTitle, verifyBlurb, virtualized ]);

+       let cloudLaunchableContainer = h('div', { class: "col-12 py-2 my-2", attrs: { id: IdPool.cloud_launchable, hidden: this.shownId !== IdPool.cloud_launchable } }, [ cloudLaunchable ]);

+       let metalVirtContainer = h('div', { class: "row col-12 py-2 my-2", attrs: { id: IdPool.metal_virtualized, hidden: this.shownId !== IdPool.metal_virtualized } }, [ bareMetalContainer, virtualizedContainer ]);

+       let cloudOperatorsContainer = h('div', { class: "col-12 py-2 my-2", attrs: { id: IdPool.cloud_operators, hidden: this.shownId !== IdPool.cloud_operators } }, [ cloud ]);

  

-       let cloud_launchable_container = h('div', { class: "col-12 py-2 my-2", attrs: { id: IdPool.cloud_launchable, hidden: this.shownId !== IdPool.cloud_launchable } }, [ cloudLaunchableTitle, cloudLaunchable ]);

-       let metal_virt_container = h('div', { class: "row col-12 py-2 my-2", attrs: { id: IdPool.metal_virtualized, hidden: this.shownId !== IdPool.metal_virtualized } }, [ bare_metal_container, virtualized_container ]);

-       let cloud_operators_container = h('div', { class: "col-12 py-2 my-2", attrs: { id: IdPool.cloud_operators, hidden: this.shownId !== IdPool.cloud_operators } }, [ cloudTitle, verifyBlurb, cloud ]);

+       streamInfoDiv = h('div', { class: "bg-light pt-3" }, [ h('div', { class: "container font-weight-light" }, [ streamSelectContainer ]) ]);

+       downloadDiv = h('div', { class: "bg-white pb-5" }, [

+         h('div', { class: "container font-weight-light" }, [

+           signatureSha256VerificationModal,

+           cloudLaunchableContainer,

+           metalVirtContainer,

+           cloudOperatorsContainer

+         ])

+       ])

  

-       return h('div', {}, [

-         signature_sha256_verification_modal,

-         stream_select_container,

-         cloud_launchable_container,

-         metal_virt_container,

-         cloud_operators_container

-       ]);

+       return h('div', {}, [h1Title, streamInfoDiv, downloadDiv]);

      }

      else {

-       return h('div', {}, "No stream data found!");

+       errorDiv = h('div', { class: "bg-transparent py-5" }, [ h('div', { class: "container font-weight-light" }, "No stream data found!") ]);

+       return h('div', {}, [ errorDiv ]);

      }

    }

  })

@@ -0,0 +1,707 @@ 

+ // There are four layers of async fetch calls

+ //  - releases.json

+ //  - builds.json

+ //  - meta.json

+ //  - commitmeta.json

+ 

+ // In this implementation, only render the page after fetching all

+ // of the four json files in the first place, and re-render if the user changes

+ // stream.

+ // NOTE: set the `initialBuildsShown` to 5 means only 5 releases will be rendered,

+ // which means the overhead for waiting for fetching 5 releases might not be large

+ // (~600ms for fetching all jsons and ~300ms for consecutive reloads).

+ 

+ const baseProdUrl = 'https://builds.coreos.fedoraproject.org/prod/streams'

+ const baseDevelUrl = 'https://builds.coreos.fedoraproject.org/devel/streams'

+ 

+ const initialBuildsShown = 5;

+ 

+ // pkgdiff enum to str

+ const diffType = ["added", "removed", "upgraded", "downgraded"];

+ const importantPkgs = ["kernel", "systemd", "rpm-ostree", "ignition", "podman"];

+ 

+ function timestampToPrettyString(date) {

+   date = new Date(date);

+   const year = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date);

+   const month = new Intl.DateTimeFormat('en', { month: 'short' }).format(date);

+   const day = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(date);

+ 

+   return `${month} ${day}, ${year}`;

+ }

+ 

+ function getBaseUrl(stream, developer) {

+     return stream != "developer"

+         ? `${baseProdUrl}/${stream}`

+         : `${baseDevelUrl}/${developer}`;

+ }

+ 

+ function sortPkgDiff(meta) {

+   if ("pkgdiff" in meta) {

+       var newdiff = {};

+       diffType.forEach(t => newdiff[t] = []);

+       meta["pkgdiff"].forEach(d => newdiff[diffType[d[1]]].push(d));

+       meta["pkgdiff"] = newdiff;

+   }

+ }

+ 

+ function findImportantPkgs(commitmeta) {

+   var r = [];

+   commitmeta["rpmostree.rpmdb.pkglist"].forEach(pkg => {

+       if (importantPkgs.includes(pkg[0])) {

+           r.push(pkg);

+       }

+   });

+   return r;

+ }

+ 

+ // The actual fetch function for `releases.json`

+ function fetchReleases(base) {

+   return fetch(`${base}/releases.json`)

+       .then(response => response.ok ? response.json() : {"releases": []})

+       .then(data => {

+           return data.releases.map(release => release.version);

+       });

+ }

+ 

+ // The actual fetch function for `builds.json`

+ function fetchBuilds(base) {

+     return fetch(`${base}/builds.json`)

+         .then(response => response.ok ? response.json() : {"builds": []})

+         .then(data => {

+             if (!('schema-version' in data) || data["schema-version"] != "1.0.0") {

+                 // in legacy mode, just assume we only built x86_64

+                 return [true, data.builds.map(id => ({'id': id, 'arches': ['x86_64'], 'meta': null, 'commitmeta': null}))];

+             } else {

+                 return [false, data.builds.map(build => ({'id': build.id, 'arches': build.arches, 'meta': null, 'commitmeta': null}))];

+             }

+         });

+ }

+ 

+ // Gather a metadata list of builds between releases

+ // Pre-condition: currentReleaseIdx <= targetReleaseIdx

+ function gatherMetadataBtwReleases(currentReleaseIdx, targetReleaseIdx, config) {

+   ({ builds, base, legacy } = config);

+   let metaPromiseList = [];

+ 

+   // no need to fetch the older release each time, since pkgdiff's are accumulated among

+   // changes after and not including older release.

+   // in the case of oldest release, however, we need to fetch the metadata for current / older / oldest

+   // release, since they are pointing to the same release.

+   if (currentReleaseIdx == targetReleaseIdx) {

+     let metaPromise = fetchBuildMeta(base, builds[currentReleaseIdx], legacy);

+     metaPromiseList.push(metaPromise);

+     return Promise.all(metaPromiseList);

+   }

+ 

+   for (let i = currentReleaseIdx; i < targetReleaseIdx; i++) {

+     let metaPromise = fetchBuildMeta(base, builds[i], legacy);

+     metaPromiseList.push(metaPromise);

+   }

+   return Promise.all(metaPromiseList);

+ }

+ 

+ // Get an accumulated pkgdiff given a list of metadata

+ // e.g. given a list of build metadata fetched between two consecutive releases

+ // we can compute the overall accumulated pkgdiff between two releases

+ function getPkgDiffFromMetaList (metaList) {

+   function getPkgDiffReducer(pkgDiffAcc, currentMeta) {

+     // NOTE: pkgDiffAcc is the most recent diff accumulated, and currentMeta has the older pkgdiff

+     if (! ("pkgdiff" in currentMeta[1])) {

+       return pkgDiffAcc;

+     }

+     currentMeta[1].pkgdiff.map(d => {

+       const pkgName = d[0];

+       const diffType = d[1];

+       const pkgInfo = d[2];

+       let pkgNamesAcc = pkgDiffAcc.map(dAcc => dAcc[0]);

+       // if the pkgdiff is first encountered, add it to accumulator

+       if (pkgNamesAcc.indexOf(pkgName) < 0) {

+         pkgDiffAcc.push(d);

+         return;

+       }

+ 

+       // added pkgdiff type

+       if (diffType == 0) {

+         let pkgDiffAccCpy = [...pkgDiffAcc];

+         for (let i = 0; i < pkgDiffAccCpy.length; i++) {

+           const dAcc = pkgDiffAccCpy[i];

+           // added first then later removed

+           if (dAcc[0] == pkgName && dAcc[1] == 1) {

+             // replace with empty list and remove later

+             pkgDiffAccCpy[i] = [];

+             break;

+           }

+           // added first then later upgraded

+           if (dAcc[0] == pkgName && dAcc[1] == 2) {

+             pkgDiffAccCpy[i] = d;

+             pkgDiffAccCpy[i][2].NewPackage[1] = dAcc[2].NewPackage[1];

+             break;

+           }

+           // added first then later downgraded

+           if (dAcc[0] == pkgName && dAcc[1] == 3) {

+             pkgDiffAccCpy[i] = d;

+             pkgDiffAccCpy[i][2].NewPackage[1] = dAcc[2].NewPackage[1];

+             break;

+           }

+         }

+         pkgDiffAccCpy = pkgDiffAccCpy.filter(dAcc => dAcc.length != 0);

+         pkgDiffAcc = [...pkgDiffAccCpy];

+       }

+ 

+       // removed pkgdiff type

+       if (diffType == 1) {

+         let pkgDiffAccCpy = [...pkgDiffAcc];

+         for (let i = 0; i < pkgDiffAccCpy.length; i++) {

+           const dAcc = pkgDiffAccCpy[i];

+           // removed first then later added

+           if (dAcc[0] == pkgName && dAcc[1] == 0) {

+             // replace with empty list and remove later

+             pkgDiffAccCpy[i] = [];

+             break;

+           }

+         }

+         pkgDiffAccCpy = pkgDiffAccCpy.filter(dAcc => dAcc.length != 0);

+         pkgDiffAcc = [...pkgDiffAccCpy];

+       }

+ 

+       // upgraded pkgdiff type

+       if (diffType == 2) {

+         let pkgDiffAccCpy = [...pkgDiffAcc];

+         for (let i = 0; i < pkgDiffAccCpy.length; i++) {

+           const dAcc = pkgDiffAccCpy[i];

+           // upgraded first then later removed

+           if (dAcc[0] == pkgName && dAcc[1] == 1) {

+             // should be removing the previous version

+             pkgDiffAccCpy[i][2].PreviousPackage[1] = pkgInfo.PreviousPackage[1];

+             break;

+           }

+           // upgraded first then later upgraded again

+           if (dAcc[0] == pkgName && dAcc[1] == 2) {

+             pkgDiffAccCpy[i] = d;

+             pkgDiffAccCpy[i][2].NewPackage[1] = dAcc[2].NewPackage[1];

+             break;

+           }

+           // upgraded first then later downgraded

+           if (dAcc[0] == pkgName && dAcc[1] == 3) {

+             // checks if versions have changed

+             let strcmp = (s1, s2) => s1.localeCompare(s2);

+             if (strcmp(pkgDiffAccCpy[i][2].NewPackage[1], pkgInfo.PreviousPackage[1]) == 0) {

+               pkgDiffAccCpy[i] = [];

+             } else if (strcmp(pkgDiffAccCpy[i][2].NewPackage[1], pkgInfo.PreviousPackage[1]) > 0) {

+               // overall, an upgrade

+               pkgDiffAccCpyp[i] = d;

+               pkgDiffAccCpy[i].NewPackage[1] = dAcc[2].NewPackage[1];

+             } else {

+               // overall, a downgrade

+               pkgDiffAccCpy[i].PreviousPackage[1] = pkgInfo.PreviousPackage[1];

+             }

+             break;

+           }

+         }

+         pkgDiffAccCpy = pkgDiffAccCpy.filter(dAcc => dAcc.length != 0);

+         pkgDiffAcc = [...pkgDiffAccCpy];

+       }

+ 

+       // downgraded pkgdiff type

+       if (diffType == 3) {

+         let pkgDiffAccCpy = [...pkgDiffAcc];

+         for (let i = 0; i < pkgDiffAccCpy.length; i++) {

+           const dAcc = pkgDiffAccCpy[i];

+           // downgraded first then later removed

+           if (dAcc[0] == pkgName && dAcc[1] == 1) {

+             // should be removing the previous version

+             pkgDiffAccCpy[i][2].PreviousPackage[1] = pkgInfo.PreviousPackage[1];

+             break;

+           }

+           // downgraded first then later upgraded

+           if (dAcc[0] == pkgName && dAcc[1] == 2) {

+             // checks if versions have changed

+             let strcmp = (s1, s2) => s1.localeCompare(s2);

+             if (strcmp(pkgDiffAccCpy[i][2].NewPackage[1], pkgInfo.PreviousPackage[1]) == 0) {

+               pkgDiffAccCpy[i] = [];

+             } else if (strcmp(pkgDiffAccCpy[i][2].NewPackage[1], pkgInfo.PreviousPackage[1]) > 0) {

+               // overall, an upgrade

+               pkgDiffAccCpy[i].PreviousPackage[1] = pkgInfo.PreviousPackage[1];

+             } else {

+               // overall, a downgrade

+               pkgDiffAccCpyp[i] = d;

+               pkgDiffAccCpy[i].NewPackage[1] = dAcc[2].NewPackage[1];

+             }

+             break;

+           }

+           // upgraded first then later downgraded

+           if (dAcc[0] == pkgName && dAcc[1] == 3) {

+             pkgDiffAccCpy[i] = d;

+             pkgDiffAccCpy[i][2].NewPackage[1] = dAcc[2].NewPackage[1];

+             break;

+           }

+         }

+         pkgDiffAccCpy = pkgDiffAccCpy.filter(dAcc => dAcc.length != 0);

+         pkgDiffAcc = [...pkgDiffAccCpy];

+       }

+ 

+     })

+ 

+     return pkgDiffAcc;

+ 

+   }

+ 

+   let pkgdiff = metaList.reduce(getPkgDiffReducer, []);

+   return pkgdiff;

+ }

+ 

+ // Fetch the metadata of the release `builds[fromIdx]`

+ // and also calculate the accumulated pkgdiff between the last release `builds[toIdx]`

+ // Note: meta.json is used for pkgdiff and commitmeta.json contains pkglist

+ function fetchBuild(base, legacy, builds, fromIdx, toIdx) {

+   let config = { builds, base, legacy };

+   return gatherMetadataBtwReleases(fromIdx, toIdx, config).then(metaList => {

+     let build = builds[fromIdx];

+     let [basearch, meta] = metaList[0];

+     meta.pkgdiff = getPkgDiffFromMetaList(metaList);

+     sortPkgDiff(meta);

+     build.meta = meta;

+     // and fetch extra commit metadata in async

+     return fetchBuildCommitMeta(base, build, basearch, legacy).then(commitmeta => {

+       commitmeta["importantPkgs"] = findImportantPkgs(commitmeta);

+       commitmeta["showImportantPkgsOnly"] = true;

+       build.commitmeta = commitmeta;

+       builds[fromIdx] = build;

+     });

+   });

+ }

+ 

+ // The actual fetch function for `meta.json`

+ function fetchBuildMeta(base, build, legacy) {

+   if (legacy) {

+       return fetch(`${base}/${build.id}/meta.json`)

+           .then(response => Promise.all([build.arches[0], response.ok ? response.json() : {}]));

+   }

+   // XXX: just fetch the meta for the first arch right now

+   return fetch(`${base}/${build.id}/${build.arches[0]}/meta.json`)

+       .then(response => Promise.all([build.arches[0], response.ok ? response.json() : {}]));

+ 

+   // return Promise.all(build.arches.map(arch => {

+   //     fetch(`${base}/${build.id}/${arch}/meta.json`)

+   //         .then(response => Promise.all([arch, response.ok ? response.json() : {}]));

+   // }));

+ }

+ 

+ // The actual fetch function for `commitmeta.json`

+ function fetchBuildCommitMeta(base, build, basearch, legacy) {

+   if (legacy) {

+       return fetch(`${base}/${build.id}/commitmeta.json`)

+           .then(response => response.ok ? response.json() : {});

+   }

+   return fetch(`${base}/${build.id}/${basearch}/commitmeta.json`)

+       .then(response => response.ok ? response.json() : {});

+ }

+ 

+ var coreos_release_notes = new Vue({

+   el: '#coreos-release-notes',

+   created: function() { this.refreshBuilds() },

+   data: {

+     // source of truth for streams

+     streamList: ['stable', 'testing', 'next'],

+     // currently selected stream

+     stream: 'stable',

+     // if current stream is "developer", currently entered developer

+     developer: "",

+     // current url to builds/ dir for stream

+     buildsUrl: "",

+     // whether the currently selected stream has a legacy layout

+     // https://github.com/coreos/coreos-assembler/pull/580

+     legacy: false,

+     // list of {id, arches, meta, commitmeta} build objects

+     // XXX: in non-legacy mode, meta and commitmeta are those

+     // of the first arch, but in the future these would be e.g.

+     // meta[arch] and commitmeta[arch]

+     releases: [],

+     // list of unshown {id, arches, meta, commitmeta} build objects

+     unshown_builds: [],

+     // toggles "Loading..."

+     loading: true

+   },

+   watch: {

+     stream: function() {

+       this.refreshBuilds();

+     }

+   },

+   methods: {

+     getPkgNevra: function(tuple) {

+       return `${tuple[0]}-${tuple[1]}.${tuple[2]}`;

+     },

+     getPkgNevraFull: function(tuple) {

+         if (tuple[1] != 0) {

+             return `${tuple[0]}-${tuple[1]}:${tuple[2]}-${tuple[3]}.${tuple[4]}`;

+         }

+         return `${tuple[0]}-${tuple[2]}-${tuple[3]}.${tuple[4]}`;

+     },

+     getPkgEvra: function(tuple) {

+         return `${tuple[1]}.${tuple[2]}`;

+     },

+     getNavbar: function(h) {

+       const self = this;

+       const changeStream = e => {

+         if (e.target.innerText === "Stable Stream") {

+             self.stream = "stable"

+         }

+         if (e.target.innerText === "Testing Stream") {

+             self.stream = "testing"

+         }

+         if (e.target.innerText === "Next Stream") {

+             self.stream = "next"

+         }

+         const overviewPageUrl = window.location.href.match(/^.*\/coreos/)[0];

+         history.replaceState(null, null, `${overviewPageUrl}?stream=${self.stream}`);

+       }

+       let shieldIcon = h('i', { class: "fas fa-shield-alt mr-2" })

+       let navStableBtn = h('button', { class: "nav-link col-12 h-100 overflow-hidden".concat(this.stream === "stable" ? " active" : ""), attrs: { "data-toggle": "tab" }, on: { click: changeStream } }, [ shieldIcon, "Stable Stream" ]);

+       let navStable = h('li', { class: "nav-item col-12 col-sm-4" }, [ navStableBtn ]);

+ 

+       let flaskIcon = h('i', { class: "fas fa-flask mr-2" })

+       let navTestingBtn = h('button', { class: "nav-link col-12 h-100 overflow-hidden".concat(this.stream === "testing" ? " active" : ""), attrs: { "data-toggle": "tab" }, on: { click: changeStream } }, [ flaskIcon, "Testing Stream" ]);

+       let navTesting = h('li', { class: "nav-item col-12 col-sm-4" }, [ navTestingBtn ]);

+ 

+       let layerIcon = h('i', { class: "fas fa-layer-group mr-2" })

+       let navNextBtn = h('button', { class: "nav-link col-12 h-100 overflow-hidden".concat(this.stream === "next" ? " active" : ""), attrs: { "data-toggle": "tab" }, on: { click: changeStream } }, [ layerIcon, "Next Stream" ]);

+       let navNext = h('li', { class: "nav-item col-12 col-sm-4" }, [ navNextBtn ]);

+ 

+       let navbar = h('ul', { class: "nav nav-tabs" }, [ navStable, navTesting, navNext ]);

+       return navbar;

+     },

+     getReleaseNoteCards: function(h) {

+       const self = this;

+       // check if all release metadata has been fetched

+       if (self.loading) {

+         return;

+       }

+ 

+       rows = [];

+       self.releases.forEach((build, idx) => {

+         // checked if build metadata has been fetched

+         if (build.arches.length == 0 || build.meta == null || build.commitmeta == null) {

+           return;

+         }

+ 

+         // Left pane consists of Build ID and Arch info

+         let headingListArches = [];

+         let headingBuildId = h('h5', { class: "font-weight-normal" }, build.id);

+         build.arches.forEach((arch, _) => {

+           headingListArches.push(h('h6', {}, arch));

+         });

+         let leftPane = h('div', { class: "col-lg-2" }, [ headingBuildId, headingListArches ]);

+ 

+         // Right pane consists of detailed package information

+         let date = h('p', {}, `Release Date: ${timestampToPrettyString(build.meta['coreos-assembler.build-timestamp'])}`);

+         // List of important packages and versions

+         let importantPkgsElements = [];

+         build.commitmeta.importantPkgs.forEach((pkg, _) => {

+           importantPkgsElements.push(pkg[0]);

+           importantPkgsElements.push(h('span', { class: "mr-2 badge badge-pill badge-light" }, pkg[2]));

+         });

+ 

+         // Summary of pkglist and pkgdiffs with expand buttons

+         let pkgSummaryElements = []

+           .concat(`${build.commitmeta['rpmostree.rpmdb.pkglist'].length} packages (`)

+           .concat(

+             h('a', {

+               attrs: {

+                 href: "#"

+               },

+               on: {

+                 click: function(e) {

+                   e.preventDefault();

+                   let totalPkgListElement = e.target.parentElement.nextSibling;

+                   if (totalPkgListElement.hidden == true) {

+                     totalPkgListElement.hidden = false;

+                     e.target.innerText = 'collapse';

+                   } else {

+                     totalPkgListElement.hidden = true;

+                     e.target.innerText = 'expand';

+                   }

+                 }

+               }

+             }, 'expand')

+           )

+           .concat('); ')

+         // Next, append the pkgdiffs summary

+         // `added` summary and expand button

+         if (build.meta.pkgdiff != {} && build.meta.pkgdiff.added.length > 0) {

+           pkgSummaryElements = pkgSummaryElements.concat(

+             `${build.meta.pkgdiff.added.length} added (`

+           )

+           .concat(

+             h('a', {

+               attrs: {

+                 href: "#"

+               },

+               on: {

+                 click: function(e) {

+                   e.preventDefault();

+                   let totalPkgListElement = e.target.parentElement

+                                              .nextSibling

+                                              .nextSibling;

+                   if (totalPkgListElement.hidden == true) {

+                     totalPkgListElement.hidden = false;

+                     e.target.innerText = 'collapse';

+                   } else {

+                     totalPkgListElement.hidden = true;

+                     e.target.innerText = 'expand';

+                   }

+                 }

+               }

+             }, 'expand')

+           )

+           .concat('); ');

+         }

+ 

+         // `removed` summary and expand button

+         if (build.meta.pkgdiff != {} && build.meta.pkgdiff.removed.length > 0) {

+           pkgSummaryElements = pkgSummaryElements.concat(

+             `${build.meta.pkgdiff.removed.length} removed (`

+           )

+           .concat(

+             h('a', {

+               attrs: {

+                 href: "#"

+               },

+               on: {

+                 click: function(e) {

+                   e.preventDefault();

+                   let totalPkgListElement = e.target.parentElement

+                                              .nextSibling

+                                              .nextSibling

+                                              .nextSibling;

+                   if (totalPkgListElement.hidden == true) {

+                     totalPkgListElement.hidden = false;

+                     e.target.innerText = 'collapse';

+                   } else {

+                     totalPkgListElement.hidden = true;

+                     e.target.innerText = 'expand';

+                   }

+                 }

+               }

+             }, 'expand')

+           )

+           .concat('); ');

+         }

+ 

+         // `upgraded` summary and expand button

+         if (build.meta.pkgdiff != {} && build.meta.pkgdiff.upgraded.length > 0) {

+           pkgSummaryElements = pkgSummaryElements.concat(

+             `${build.meta.pkgdiff.upgraded.length} upgraded (`

+           )

+           .concat(

+             h('a', {

+               attrs: {

+                 href: "#"

+               },

+               on: {

+                 click: function(e) {

+                   e.preventDefault();

+                   let totalPkgListElement = e.target.parentElement

+                                              .nextSibling

+                                              .nextSibling

+                                              .nextSibling

+                                              .nextSibling;

+                   if (totalPkgListElement.hidden == true) {

+                     totalPkgListElement.hidden = false;

+                     e.target.innerText = 'collapse';

+                   } else {

+                     totalPkgListElement.hidden = true;

+                     e.target.innerText = 'expand';

+                   }

+                 }

+               }

+             }, 'expand')

+           )

+           .concat('); ');

+         }

+ 

+         // `downgraded` summary and expand button

+         if (build.meta.pkgdiff != {} && build.meta.pkgdiff.downgraded.length > 0) {

+           pkgSummaryElements = pkgSummaryElements.concat(

+             `${build.meta.pkgdiff.downgraded.length} downgraded (`

+           )

+           .concat(

+             h('a', {

+               attrs: {

+                 href: "#"

+               },

+               on: {

+                 click: function(e) {

+                   e.preventDefault();

+                   let totalPkgListElement = e.target.parentElement

+                                              .nextSibling

+                                              .nextSibling

+                                              .nextSibling

+                                              .nextSibling

+                                              .nextSibling;

+                   if (totalPkgListElement.hidden == true) {

+                     totalPkgListElement.hidden = false;

+                     e.target.innerText = 'collapse';

+                   } else {

+                     totalPkgListElement.hidden = true;

+                     e.target.innerText = 'expand';

+                   }

+                 }

+               }

+             }, 'expand')

+           )

+           .concat('); ');

+         }

+ 

+         let pkgSummaryDiv = h('div', { class: "mt-3" }, pkgSummaryElements);

+ 

+         // Package list

+         let totalPkgsElementsList = [];

+         let totalPkgsHeading = [];

+         if (build.commitmeta['rpmostree.rpmdb.pkglist'].length > 0) {

+           build.commitmeta['rpmostree.rpmdb.pkglist'].forEach((pkg, _) => {

+             totalPkgsElementsList.push(h('li', {}, self.getPkgNevraFull(pkg)));

+           });

+           totalPkgsHeading = h('p', { class: "mt-3" }, "Package List:")

+         }

+         let totalPkgsElements = h('div', { attrs: { hidden: true } }, [ totalPkgsHeading, h('ul', {}, totalPkgsElementsList) ]);

+ 

+         // Added package list

+         let addedPkgsElementsList = [];

+         let addedPkgsHeading = [];

+         if (build.meta.pkgdiff != {} && build.meta.pkgdiff.added.length > 0) {

+           build.meta.pkgdiff.added.forEach((pkg, _) => {

+             addedPkgsElementsList.push(h('li', {}, self.getPkgNevra(pkg[2]["NewPackage"])));

+           });

+           addedPkgsHeading = h('p', { class: "mt-3" }, "Added:")

+         }

+         let addedPkgsElements = h('div', { attrs: { hidden: true } }, [ addedPkgsHeading, h('ul', {}, addedPkgsElementsList) ]);

+ 

+         // Removed package list

+         let removedPkgsElementsList = [];

+         let removedPkgsHeading = [];

+         if (build.meta.pkgdiff != {} && build.meta.pkgdiff.removed.length > 0) {

+           build.meta.pkgdiff.removed.forEach((pkg, _) => {

+             removedPkgsElementsList.push(h('li', {}, self.getPkgNevra(pkg[2]["PreviousPackage"])));

+           });

+           removedPkgsHeading = h('p', { class: "mt-3" }, "Removed:");

+         }

+         let removedPkgsElements = h('div', { attrs: { hidden: true } }, [ removedPkgsHeading, h('ul', {}, removedPkgsElementsList) ]);

+ 

+         // Upgraded package list

+         let upgradedPkgsElementsList = [];

+         let upgradedPkgsHeading = [];

+         if (build.meta.pkgdiff != {} && build.meta.pkgdiff.upgraded.length > 0) {

+           build.meta.pkgdiff.upgraded.forEach((pkg, _) => {

+             upgradedPkgsElementsList.push(h('li', {}, `${pkg[2]["PreviousPackage"][0]} ${self.getPkgEvra(pkg[2]["PreviousPackage"])} → ${self.getPkgEvra(pkg[2]["NewPackage"])}`));

+           });

+           upgradedPkgsHeading = h('p', { class: "mt-3" }, "Upgraded:");

+         }

+         let upgradedPkgsElements = h('div', { attrs: { hidden: true } }, [ upgradedPkgsHeading, h('ul', {}, upgradedPkgsElementsList) ]);

+ 

+         // Downgraded package list

+         let downgradedPkgsElementsList = [];

+         let downgradedPkgsHeading = [];

+         if (build.meta.pkgdiff != {} && build.meta.pkgdiff.downgraded.length > 0) {

+           build.meta.pkgdiff.downgraded.forEach((pkg, _) => {

+             downgradedPkgsElementsList.push(h('li', {}, `${pkg[2]["PreviousPackage"][0]} ${self.getPkgEvra(pkg[2]["PreviousPackage"])} → ${self.getPkgEvra(pkg[2]["NewPackage"])}`));

+           });

+           downgradedPkgsHeading = h('p', { class: "mt-3" }, "Downgraded:");

+         }

+         let downgradedPkgsElements = h('div', { attrs: { hidden: true } }, [ downgradedPkgsHeading, h('ul', {}, downgradedPkgsElementsList) ]);

+ 

+         let rightPane = h('div', { class: "col-lg-10 border-bottom mb-5 pb-4" }, [ date, importantPkgsElements, pkgSummaryDiv, totalPkgsElements, addedPkgsElements, removedPkgsElements, upgradedPkgsElements, downgradedPkgsElements ]);

+         let row = h('div', { class: "row" }, [ leftPane, rightPane ]);

+         rows.push(row);

+       })

+       return h('div', { class: "my-5" }, rows);

+     },

+     refreshBuilds: function() {

+         this.loading = true

+         this.releasesUrl = getBaseUrl(this.stream, this.developer);

+         this.buildsUrl = getBaseUrl(this.stream, this.developer) + "/builds";

+         fetchReleases(this.releasesUrl).then(releaseVersions => {

+           fetchBuilds(this.buildsUrl).then(result => {

+             [legacy, builds] = result;

+             // first populate and show the build list

+             this.legacy = legacy;

+             this.releases = [];

+             this.unshown_builds = [];

+             // counter for the number of release metadata fetched since fetch is asnyc operation

+             let counter = 0;

+ 

+             // get the index list of release builds in the build list

+             const releaseIdxList = builds.map((build, idx) => releaseVersions.includes(build.id) ? idx : -1).filter(idx => idx != -1);

+             const numReleases = releaseIdxList.length;

+ 

+             // fetch the metadata and compute the pkgdiff for subsequent releases

+             // since the oldest release does not have a pkgdiff, the pkgdiff for oldest release is an empty array

+             for (let i = 0; i < numReleases; i++) {

+               const releaseIdx = releaseIdxList[i];

+               // in case of oldest release, there's no older release

+               const nextReleaseIdx = releaseIdxList[i + 1] == null ? releaseIdxList[i] : releaseIdxList[i + 1];

+               if (i < initialBuildsShown) {

+                 // NOTE: here only the `builds` array have the actual values, all other variables are pointers to the elements of this array

+                 this.releases.push(builds[releaseIdx]);

+                 // fetchBuild mutates the `builds` array

+                 fetchBuild(this.buildsUrl, this.legacy, builds, releaseIdx, nextReleaseIdx)

+                 .then(() => {

+                   counter++;

+                   if (counter === numReleases) {

+                     // fetched all metadata

+                     this.loading = false;

+                   }

+                 });

+               } else {

+                 // XXX: unshown/unprocessed releases, could be handled later according to needs

+                 this.unshown_builds.push(builds[releaseIdx]);

+                 counter++;

+                 if (counter === numReleases) {

+                   // fetched all metadata

+                   this.loading = false;

+                 }

+               }

+             }

+           });

+         });

+     }

+   },

+   render: function(h) {

+     // Duplicate logic from coreos-download.js

+     // URL paramters checking and setting default values

+     if(window.location.href.match(/^.*\/coreos/) == null) {

+       return

+     }

+     const overviewPageUrl = window.location.href.match(/^.*\/coreos/)[0];

+     searchParams = new URLSearchParams(window.location.search);

+     // switch to specified stream if `stream` parameter is set

+     if (searchParams.has('stream')) {

+       switch(searchParams.get('stream')) {

+         case 'stable':

+           this.stream = "stable";

+           break;

+         case 'testing':

+           this.stream = "testing";

+           break;

+         case 'next':

+           this.stream = "next";

+           break;

+         default:

+           this.stream = "stable";

+       }

+     } else {

+       searchParams.set('stream', 'stable');

+     }

+     // Update the url with the parameters

+     history.replaceState(null, null, `${overviewPageUrl}?${searchParams.toString()}`);

+ 

+     let navBar = this.getNavbar(h);

+ 

+     if (this.loading) {

+       let loadingDiv = h('div', { class: "bg-white pb-5" }, [ h('div', { class: "container font-weight-light" }, "Loading...") ]);

+       return h('div', {}, [ navBar, loadingDiv ]);

+     } else {

+       let releaseNoteCards = this.getReleaseNoteCards(h);

+       return h('div', {}, [ navBar, releaseNoteCards ]);

+     }

+   }

+ });

Restyles the Fedora CoreOS Download page based on the initial proposed
design https://pagure.io/fedora-websites/issue/964#comment-576026.

Moreover, dustymabe and abai thought it could be a good idea to add release
stream info and release notes to the top level coreos page:
https://getfedora.org/en/coreos/.

Signed-off-by: Allen Bai abai@redhat.com

rebased onto 2824dd3d0cb52fd2a64d42423b0e13b5fb017c13

4 years ago

1 new commit added

  • coreos-download: add release note section
4 years ago

Adds a release note section on the overview page of Fedora CoreOS Download. This gives users general information about the last 5 releases for each of stable, testing and next stream.

For each release, list the versions of important packages and package information about what was added, removed, upgraded and downgraded. In addition, for future improvements, adding notes on bugfix information might also be helpful.

Demo:
gif demo

Overview page:
image demo overview page

Download page:
image demo download page

Cleaning up the code and adding comments in coreos-download.js tomorrow and will remove the [WIP] after the cleanup.

Is there anything else we should be adding right now? I think bugfix information would be very helpful/informative in the future, but I'm assuming that would require extra maintenance effort for each release..

cc @dustymabe @jlebon

we probably don't want to link users directly to https://builds.coreos.fedoraproject.org/browser - it's not an official app we want people to rely on

The mockups look grat abai. We did discuss maybe for now (until we get proper release notes) we keep the package diff but make it less prominent. So for example we have a summary and make each of them clickable. For example in the release notes we highlight the primary packages we care about but then say:

478 packages (50 upgraded, 2 added, 3 removed)

and have the 478 packages link show the full package list for that revision and the 50 upgraded, 2 added, 3 removed show the package diff from the previous release. WDYT?

1 new commit added

  • coreos-download: use releases instead of builds for release note
3 years ago

@dustymabe sorry for delayed reply but yes I'm working towards that and the new commit already commented out the direct links for the FCOS release browser. Been working on getting the accumulated pkgdiff between releases working given a list of builds and just pushed a commit to address it.
Now I'm working on get the pkg summary like you mentioned in the previous comment. ;) This feature should be rolled out quickly.

1 new commit added

  • coreos-download: add summary for packages and pkgdiffs
3 years ago

4 new commits added

  • coreos-download: add summary for packages and pkgdiffs
  • coreos-download: use releases instead of builds for release note
  • coreos-download: add release note section
  • coreos-download: restyling coreos download page
3 years ago

Overview page:
overview page

Download page:
download page

GIf demo:
gif demo

Ready for another look. The pkgdiffs and builds shown on the page are now all releases and accumulated pkgdiffs between releases are computed from the intermediate builds pkgdiffs.

cc @dustymabe @jlebon

rebased onto 4682c25

3 years ago

Rebased on the master branch

The renderings look great! I'm no js expert so my review on the code isn't worth much. Only question I have is if we want to leave that one section commented out. Do we have plans for it in the future?

@dustymabe The section commented out is for the four buttons linked to the releases browser, which we earlier decided to not expose to public (loudly).. The buttons come from the initial design but I cannot think of other places I can link to at the moment.

Pulled the PR and ran it locally successfully!

Not familiar with JS enough to give a technical review on the code, but it WFM!

LGTM

1 new commit added

  • coreos-download: add buttons for releases in download page
3 years ago

Added a final touch that exposes the buttons back to the download page but instead of linking to release browser, the buttons are linked corresponding release notes. And also added URL parameter stream in the release notes like in the download page. For example, now users can view the testing stream release notes via https://getfedora.org/en/coreos?stream=testing.

Pull-Request has been merged by dustymabe

3 years ago