├── robots.txt ├── webapp ├── __init__.py ├── admin │ └── logic.py ├── blog │ └── __init__.py ├── docs │ └── __init__.py ├── publisher │ ├── __init__.py │ └── github │ │ └── views.py ├── store │ ├── __init__.py │ └── content │ │ └── distros │ │ ├── kde-neon.yaml │ │ ├── debian.yaml │ │ ├── pop.yaml │ │ ├── elementary.yaml │ │ ├── fedora.yaml │ │ ├── raspbian.yaml │ │ ├── kubuntu.yaml │ │ └── manjaro.yaml ├── tutorials │ └── __init__.py ├── api │ ├── __init__.py │ └── exceptions.py ├── extensions.py ├── configs │ ├── snapcraft.py │ ├── limenet.py │ └── sdrsatcom.py ├── snapcraft │ ├── snapcraft.png │ ├── content │ │ └── snapcraft_live.yaml │ └── logic.py ├── first_snap │ └── content │ │ ├── moos │ │ ├── package.yaml │ │ ├── snapcraft.yaml │ │ └── test.yaml │ │ ├── ruby │ │ ├── package.yaml │ │ ├── snapcraft.yaml │ │ └── test.yaml │ │ ├── rust │ │ ├── package.yaml │ │ ├── snapcraft.yaml │ │ └── test.yaml │ │ ├── c │ │ ├── package.yaml │ │ └── test.yaml │ │ ├── node │ │ ├── package.yaml │ │ ├── snapcraft.yaml │ │ └── test.yaml │ │ ├── golang │ │ ├── package.yaml │ │ ├── snapcraft.yaml │ │ └── test.yaml │ │ ├── java │ │ ├── package.yaml │ │ └── test.yaml │ │ ├── flutter │ │ ├── package.yaml │ │ ├── snapcraft.yaml │ │ └── test.yaml │ │ ├── pre-built │ │ ├── package.yaml │ │ ├── snapcraft.yaml │ │ └── test.yaml │ │ ├── python │ │ ├── package.yaml │ │ ├── snapcraft.yaml │ │ └── test.yaml │ │ ├── ros │ │ ├── package.yaml │ │ ├── snapcraft.yaml │ │ ├── build.yaml │ │ └── test.yaml │ │ └── ros2 │ │ ├── package.yaml │ │ ├── snapcraft.yaml │ │ ├── build.yaml │ │ └── test.yaml ├── decorators.py ├── config.py └── login │ └── macaroon.py ├── tests ├── api │ └── __init__.py ├── docs │ └── __init__.py ├── login │ └── __init__.py ├── store │ └── __init__.py ├── first_snap │ └── __init__.py ├── metrics │ ├── __init__.py │ └── tests_metrics.py ├── publisher │ ├── __init__.py │ ├── snaps │ │ └── __init__.py │ ├── tests_account_logout.py │ └── tests_reserved_name_dispute.py ├── snapcraft │ ├── __init__.py │ └── tests_public.py ├── __init__.py └── tests_requests.py ├── .eslintignore ├── templates ├── store │ ├── snap-details │ │ ├── _description.html │ │ ├── _screenshots.html │ │ └── _templates.html │ ├── _snap-header-developer-information.html │ ├── _empty-category-partial.html │ ├── _snap-header-information.html │ └── _generated-featured-snap-partial.html ├── publisher │ ├── _publisher_layout.html │ ├── _noscript.html │ ├── listing.html │ ├── register-name-dispute-success.html │ └── settings.html ├── _layout.html ├── docs │ ├── _notification.html │ └── _search-bar.html ├── home │ └── _fsf_yaml_show_more.html ├── partials │ ├── _code-snippet.html │ ├── blog-card--minimal.html │ ├── _verified_developer.html │ ├── _star_developer.html │ ├── _snap-details-image-layout.html │ ├── livestream-notification.html │ ├── blog-card.html │ ├── _snap-details-video-layout.html │ ├── _hiri-case-study.html │ ├── _publisher-featured-snap.html │ └── search-bar.html ├── admin │ └── admin.html ├── _footer-brandstore.html ├── _header_empty.html ├── sitemap │ ├── sitemap.xml │ └── sitemap-index.xml ├── first-snap │ ├── language.html │ └── _layout_fsf.html ├── 50X.html ├── 503.html ├── 404.html ├── 410.html └── tutorials │ └── index.html ├── static ├── js │ ├── brand-store │ │ ├── App │ │ │ └── index.js │ │ ├── Snaps │ │ │ └── index.js │ │ ├── Members │ │ │ ├── index.js │ │ │ └── memberRoles.js │ │ ├── Publisher │ │ │ ├── index.ts │ │ │ └── Publisher.tsx │ │ ├── Reviewer │ │ │ ├── index.ts │ │ │ └── Reviewer.tsx │ │ ├── Settings │ │ │ └── index.ts │ │ ├── Navigation │ │ │ └── index.js │ │ ├── SectionNav │ │ │ └── index.ts │ │ ├── StoreNotFound │ │ │ ├── index.ts │ │ │ └── StoreNotFound.tsx │ │ ├── ReviewerAndPublisher │ │ │ ├── index.ts │ │ │ └── ReviewerAndPublisher.tsx │ │ ├── types │ │ │ ├── global.d.ts │ │ │ └── shared.ts │ │ ├── hooks │ │ │ └── index.ts │ │ ├── brand-store.js │ │ ├── selectors │ │ │ └── index.ts │ │ └── store │ │ │ └── index.ts │ ├── publisher │ │ ├── listing │ │ │ ├── components │ │ │ │ ├── App │ │ │ │ │ └── index.ts │ │ │ │ ├── ImageUpload │ │ │ │ │ └── index.ts │ │ │ │ ├── LicenseInputs │ │ │ │ │ └── index.ts │ │ │ │ ├── MetricsInputs │ │ │ │ │ └── index.ts │ │ │ │ ├── PreviewForm │ │ │ │ │ └── index.ts │ │ │ │ ├── Screenshots │ │ │ │ │ └── index.ts │ │ │ │ ├── CategoriesInput │ │ │ │ │ └── index.ts │ │ │ │ ├── MultipleInputs │ │ │ │ │ └── index.ts │ │ │ │ ├── ListingFormInput │ │ │ │ │ └── index.ts │ │ │ │ └── ListingDescriptionField │ │ │ │ │ └── index.ts │ │ │ ├── sections │ │ │ │ ├── ListingDetailsSection │ │ │ │ │ └── index.ts │ │ │ │ ├── ContactInformationSection │ │ │ │ │ └── index.ts │ │ │ │ └── AdditionalInformationSection │ │ │ │ │ ├── index.ts │ │ │ │ │ └── AdditionalInformationSection.tsx │ │ │ ├── utils │ │ │ │ ├── formatFileSize.ts │ │ │ │ ├── validateAspectRatio.ts │ │ │ │ ├── validateImageDimensions.ts │ │ │ │ └── index.ts │ │ │ ├── types │ │ │ │ └── index.d.ts │ │ │ └── index.tsx │ │ ├── settings │ │ │ ├── components │ │ │ │ └── App │ │ │ │ │ └── index.ts │ │ │ ├── utils │ │ │ │ └── index.ts │ │ │ ├── types │ │ │ │ ├── index.d.ts │ │ │ │ └── SettingsData.d.ts │ │ │ └── index.tsx │ │ ├── shared │ │ │ ├── PageHeader │ │ │ │ └── index.ts │ │ │ ├── SaveAndPreview │ │ │ │ └── index.ts │ │ │ └── SearchAutocomplete │ │ │ │ └── index.ts │ │ ├── metrics │ │ │ ├── graphs │ │ │ │ └── territories.js │ │ │ ├── filters.js │ │ │ └── config.js │ │ ├── release │ │ │ ├── components │ │ │ │ ├── releasesConfirmDetails │ │ │ │ │ ├── types.js │ │ │ │ │ ├── globalRow.js │ │ │ │ │ ├── cancelProgressiveRow.js │ │ │ │ │ └── closeChannelsRow.js │ │ │ │ ├── historyPanel.js │ │ │ │ ├── historyIcon.js │ │ │ │ ├── globalNotification.js │ │ │ │ ├── releasesConfirmActions.js │ │ │ │ └── releasesTable │ │ │ │ │ └── revisionCell.js │ │ │ ├── reducers │ │ │ │ ├── options.js │ │ │ │ ├── releases.js │ │ │ │ ├── currentTrack.js │ │ │ │ ├── defaultTrack.js │ │ │ │ ├── architectures.js │ │ │ │ ├── revisions.js │ │ │ │ ├── modal.js │ │ │ │ ├── availableRevisionsSelect.js │ │ │ │ ├── globalNotification.js │ │ │ │ ├── branches.js │ │ │ │ ├── history.js │ │ │ │ ├── defaultTrack.test.js │ │ │ │ ├── currentTrack.test.js │ │ │ │ ├── availableRevisionsSelect.test.js │ │ │ │ ├── pendingCloses.js │ │ │ │ ├── releases.test.js │ │ │ │ ├── index.js │ │ │ │ ├── branches.test.js │ │ │ │ ├── architectures.test.js │ │ │ │ ├── revisions.test.js │ │ │ │ └── modal.test.js │ │ │ └── actions │ │ │ │ ├── pendingCloses.js │ │ │ │ ├── currentTrack.js │ │ │ │ ├── revisions.js │ │ │ │ ├── modal.js │ │ │ │ ├── globalNotification.js │ │ │ │ ├── index.js │ │ │ │ ├── architectures.js │ │ │ │ ├── pendingCloses.test.js │ │ │ │ ├── currentTrack.test.js │ │ │ │ ├── revisions.test.js │ │ │ │ ├── branches.js │ │ │ │ ├── modal.test.js │ │ │ │ ├── gaEventTracking.js │ │ │ │ ├── architectures.test.js │ │ │ │ ├── globalNotification.test.js │ │ │ │ └── channelMap.js │ │ ├── tour │ │ │ ├── __mocks__ │ │ │ │ └── constants.js │ │ │ ├── constants.js │ │ │ ├── tourBar.js │ │ │ ├── metricsEvents.js │ │ │ └── tourOverlayMask.js │ │ ├── market │ │ │ ├── markdown.js │ │ │ ├── initIcon.test.js │ │ │ ├── initMedia.js │ │ │ ├── whitelistBlacklist.js │ │ │ ├── initIcon.js │ │ │ ├── initMedia.test.js │ │ │ ├── stickyListingBar.js │ │ │ ├── storageCommands.js │ │ │ ├── initBanner.js │ │ │ ├── license.js │ │ │ └── publicMetrics.js │ │ ├── builds │ │ │ └── components │ │ │ │ ├── select.js │ │ │ │ ├── select.test.js │ │ │ │ ├── triggerBuild.test.js │ │ │ │ └── repoConnect.test.js │ │ ├── form │ │ │ ├── AccordionHelp.test.js │ │ │ └── AccordionHelp.js │ │ └── publisher.js │ ├── public │ │ ├── blog.js │ │ ├── search.js │ │ ├── store.js │ │ ├── brand-store.js │ │ ├── publisher-details.js │ │ ├── about │ │ │ └── index.js │ │ ├── homepage.js │ │ ├── fsf.js │ │ ├── distro-install.js │ │ ├── newsletter.js │ │ ├── store-details.js │ │ ├── modal.js │ │ └── scroll-to.js │ ├── base │ │ ├── cookie-policy.js │ │ ├── global-nav.js │ │ ├── base.js │ │ └── navigation.js │ ├── libs │ │ ├── mouse.js │ │ ├── throttle.js │ │ ├── mobile.js │ │ ├── formHelpers.js │ │ ├── shallowDiff.js │ │ ├── shallowDiff.test.js │ │ ├── debounce.js │ │ └── iframeSize.js │ └── config │ │ └── swiper.config.js ├── images │ ├── rocket.png │ └── yt_play_btn.svg └── sass │ ├── _utilities_text-wrap.scss │ ├── _patterns_table.scss │ ├── _snapcraft_p-breadcrumbs.scss │ ├── _snapcraft_p-table-mobile-card.scss │ ├── _patterns_nested-inputs.scss │ ├── _snapcraft_p-verified.scss │ ├── _utilities_disabled.scss │ ├── _utilities_flex.scss │ ├── _patterns_sticky_footer.scss │ ├── _snapcraft_p-docs.scss │ ├── _patterns_blog-list.scss │ ├── _utilities_no-limit.scss │ ├── _snapcraft_p-action-form.scss │ ├── _patterns_modal.scss │ ├── _snapcraft-publisher.scss │ ├── _snapcraft_p-notifications.scss │ ├── _snapcraft_embedded_card_modal.scss │ ├── _snapcraft_store.scss │ ├── _patterns_p-password-toggle-input.scss │ ├── _patterns_testimonial.scss │ ├── _patterns_button_spinner.scss │ ├── _snapcraft_dispute-list.scss │ ├── _snapcraft_custom-cols.scss │ ├── _snapcraft-publicise.scss │ ├── _snapcraft_banner.scss │ ├── _patterns_pagination.scss │ ├── _snapcraft_p-form-validation.scss │ ├── _snapcraft_p-side-panel.scss │ ├── _snapcraft_p-autocomplete.scss │ ├── _snapcraft-p-videos.scss │ ├── _snapcraft_p-navigation.scss │ ├── _search-form.scss │ ├── _utilities_crop.scss │ ├── _patterns_blog-post.scss │ ├── _utilities_margin-sizing.scss │ ├── _patterns_first-snap-flow.scss │ ├── _snapcraft_p-sticky-admin-footer.scss │ ├── _button-overrides.scss │ ├── _patterns_distro_banner.scss │ ├── _snapcraft_distro-chart.scss │ ├── _snapcraft_metrics.scss │ └── _snapcraft_show-more.scss ├── renovate.json ├── .github ├── pull_request_template.md ├── issue_template.md ├── dependabot.yml └── workflows │ ├── run-percy.yml │ └── docs-links.yaml ├── security.txt ├── .env ├── entrypoint ├── .dockerignore ├── deleted.yaml ├── .babelrc ├── webpack.config.js ├── .eslintrc.js ├── konf ├── limenet.snapcraft.io.yaml └── sdrsatcom.snapcraft.io.yaml ├── tsconfig.json ├── BRANDSTORES.md ├── requirements.txt ├── .stylelintrc ├── webpack.config.entry.js └── .gitignore /robots.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/docs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/login/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/store/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webapp/admin/logic.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webapp/blog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webapp/docs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/first_snap/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/metrics/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/publisher/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/snapcraft/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webapp/publisher/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webapp/store/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webapp/tutorials/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/publisher/snaps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | static/js/modules 2 | static/js/dist -------------------------------------------------------------------------------- /templates/store/snap-details/_description.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /webapp/api/__init__.py: -------------------------------------------------------------------------------- 1 | from . import requests # noqa 2 | -------------------------------------------------------------------------------- /static/js/brand-store/App/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./App"; 2 | -------------------------------------------------------------------------------- /static/js/brand-store/Snaps/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Snaps"; 2 | -------------------------------------------------------------------------------- /static/js/brand-store/Members/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Members"; 2 | -------------------------------------------------------------------------------- /static/js/brand-store/Publisher/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Publisher"; 2 | -------------------------------------------------------------------------------- /static/js/brand-store/Reviewer/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Reviewer"; 2 | -------------------------------------------------------------------------------- /static/js/brand-store/Settings/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Settings"; 2 | -------------------------------------------------------------------------------- /static/js/brand-store/Navigation/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Navigation"; 2 | -------------------------------------------------------------------------------- /static/js/brand-store/SectionNav/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./SectionNav"; 2 | -------------------------------------------------------------------------------- /static/js/publisher/listing/components/App/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./App"; 2 | -------------------------------------------------------------------------------- /static/js/brand-store/StoreNotFound/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./StoreNotFound"; 2 | -------------------------------------------------------------------------------- /static/js/publisher/settings/components/App/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./App"; 2 | -------------------------------------------------------------------------------- /static/js/publisher/shared/PageHeader/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./PageHeader"; 2 | -------------------------------------------------------------------------------- /static/js/publisher/shared/SaveAndPreview/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./SaveAndPreview"; 2 | -------------------------------------------------------------------------------- /webapp/extensions.py: -------------------------------------------------------------------------------- 1 | from flask_wtf.csrf import CSRFProtect 2 | 3 | 4 | csrf = CSRFProtect() 5 | -------------------------------------------------------------------------------- /static/images/rocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edlerd/snapcraft.io/main/static/images/rocket.png -------------------------------------------------------------------------------- /static/js/public/blog.js: -------------------------------------------------------------------------------- 1 | import { newsletter } from "./newsletter"; 2 | 3 | export { newsletter }; 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logging.getLogger("talisker.context").disabled = True 4 | -------------------------------------------------------------------------------- /webapp/configs/snapcraft.py: -------------------------------------------------------------------------------- 1 | WEBAPP_CONFIG = {"LAYOUT": "_layout.html", "STORE_NAME": "Snap store"} 2 | -------------------------------------------------------------------------------- /static/js/brand-store/ReviewerAndPublisher/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./ReviewerAndPublisher"; 2 | -------------------------------------------------------------------------------- /static/js/public/search.js: -------------------------------------------------------------------------------- 1 | import { getColour } from "../libs/colours"; 2 | 3 | export { getColour }; 4 | -------------------------------------------------------------------------------- /static/js/publisher/listing/components/ImageUpload/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./ImageUpload"; 2 | -------------------------------------------------------------------------------- /static/js/publisher/listing/components/LicenseInputs/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./LicenseInputs"; 2 | -------------------------------------------------------------------------------- /static/js/publisher/listing/components/MetricsInputs/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./MetricsInputs"; 2 | -------------------------------------------------------------------------------- /static/js/publisher/listing/components/PreviewForm/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./PreviewForm"; 2 | -------------------------------------------------------------------------------- /static/js/publisher/listing/components/Screenshots/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Screenshots"; 2 | -------------------------------------------------------------------------------- /static/js/publisher/shared/SearchAutocomplete/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./SearchAutocomplete"; 2 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>canonical-web-and-design/renovate-websites" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /static/js/publisher/listing/components/CategoriesInput/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./CategoriesInput"; 2 | -------------------------------------------------------------------------------- /static/js/publisher/listing/components/MultipleInputs/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./MultipleInputs"; 2 | -------------------------------------------------------------------------------- /static/js/base/cookie-policy.js: -------------------------------------------------------------------------------- 1 | import { cookiePolicy } from "@canonical/cookie-policy"; 2 | 3 | cookiePolicy(); 4 | -------------------------------------------------------------------------------- /static/js/publisher/listing/components/ListingFormInput/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./ListingFormInput"; 2 | -------------------------------------------------------------------------------- /webapp/snapcraft/snapcraft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edlerd/snapcraft.io/main/webapp/snapcraft/snapcraft.png -------------------------------------------------------------------------------- /static/js/public/store.js: -------------------------------------------------------------------------------- 1 | import { storeCategories } from "./store-categories"; 2 | 3 | export { storeCategories }; 4 | -------------------------------------------------------------------------------- /static/js/publisher/listing/sections/ListingDetailsSection/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./ListingDetailsSection"; 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Done 2 | 3 | ## How to QA 4 | 5 | ## Issue / Card 6 | Fixes # 7 | 8 | ## Screenshots 9 | -------------------------------------------------------------------------------- /static/js/brand-store/types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Window { 2 | CSRF_TOKEN: string; 3 | API_URL: string; 4 | } 5 | -------------------------------------------------------------------------------- /static/js/public/brand-store.js: -------------------------------------------------------------------------------- 1 | import { storeCategories } from "./store-categories"; 2 | 3 | export { storeCategories }; 4 | -------------------------------------------------------------------------------- /static/js/publisher/listing/components/ListingDescriptionField/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./ListingDescriptionField"; 2 | -------------------------------------------------------------------------------- /security.txt: -------------------------------------------------------------------------------- 1 | Contact: mailto:security-and-web@canonical.com 2 | Expires: 2024-04-01T00:00:00.000Z 3 | Preferred-Languages: en 4 | -------------------------------------------------------------------------------- /static/js/publisher/listing/sections/ContactInformationSection/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./ContactInformationSection"; 2 | -------------------------------------------------------------------------------- /static/sass/_utilities_text-wrap.scss: -------------------------------------------------------------------------------- 1 | @mixin u-text-wrap { 2 | .u-text-wrap { 3 | overflow-wrap: break-word; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /templates/publisher/_publisher_layout.html: -------------------------------------------------------------------------------- 1 | {% extends webapp_config['LAYOUT'] %} 2 | 3 | {% block meta_copydoc %}{% endblock %} 4 | -------------------------------------------------------------------------------- /webapp/first_snap/content/moos/package.yaml: -------------------------------------------------------------------------------- 1 | name: test-moos-{name} 2 | download: git clone https://github.com/snapcraft-docs/moos 3 | -------------------------------------------------------------------------------- /webapp/first_snap/content/ruby/package.yaml: -------------------------------------------------------------------------------- 1 | name: test-mdl-{name} 2 | download: git clone https://github.com/snapcraft-docs/mdl 3 | -------------------------------------------------------------------------------- /webapp/first_snap/content/rust/package.yaml: -------------------------------------------------------------------------------- 1 | name: test-xsv-{name} 2 | download: git clone https://github.com/snapcraft-docs/xsv 3 | -------------------------------------------------------------------------------- /static/js/publisher/listing/sections/AdditionalInformationSection/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./AdditionalInformationSection"; 2 | -------------------------------------------------------------------------------- /webapp/first_snap/content/c/package.yaml: -------------------------------------------------------------------------------- 1 | name: test-dosbox-{name} 2 | download: git clone https://github.com/snapcraft-docs/dosbox 3 | -------------------------------------------------------------------------------- /webapp/first_snap/content/node/package.yaml: -------------------------------------------------------------------------------- 1 | name: test-wethr-{name} 2 | download: git clone https://github.com/snapcraft-docs/wethr 3 | -------------------------------------------------------------------------------- /static/js/public/publisher-details.js: -------------------------------------------------------------------------------- 1 | import { snapDetailsPosts } from "./snap-details/blog-posts"; 2 | 3 | export { snapDetailsPosts }; 4 | -------------------------------------------------------------------------------- /webapp/first_snap/content/golang/package.yaml: -------------------------------------------------------------------------------- 1 | name: test-httplab-{name} 2 | download: git clone https://github.com/snapcraft-docs/httplab 3 | -------------------------------------------------------------------------------- /webapp/first_snap/content/java/package.yaml: -------------------------------------------------------------------------------- 1 | name: test-freeplane-{name} 2 | download: git clone https://github.com/snapcraft-docs/freeplane 3 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PORT=8004 2 | ENVIRONMENT=devel 3 | FLASK_DEBUG=true 4 | DEVEL=True 5 | SECRET_KEY=local_development_fake_key 6 | LP_API_USERNAME=test_lp_user 7 | -------------------------------------------------------------------------------- /webapp/first_snap/content/flutter/package.yaml: -------------------------------------------------------------------------------- 1 | name: test-super-cool-app-{name} 2 | download: git clone https://github.com/kenvandine/super-cool-app 3 | -------------------------------------------------------------------------------- /webapp/first_snap/content/pre-built/package.yaml: -------------------------------------------------------------------------------- 1 | name: test-geekbench4-{name} 2 | download: git clone https://github.com/snapcraft-docs/geekbench4 3 | -------------------------------------------------------------------------------- /webapp/first_snap/content/python/package.yaml: -------------------------------------------------------------------------------- 1 | name: test-offlineimap-{name} 2 | download: git clone https://github.com/snapcraft-docs/offlineimap 3 | -------------------------------------------------------------------------------- /static/js/base/global-nav.js: -------------------------------------------------------------------------------- 1 | import { createNav } from "@canonical/global-nav"; 2 | createNav({ 3 | maxWidth: "72rem", 4 | showLogins: false, 5 | }); 6 | -------------------------------------------------------------------------------- /webapp/first_snap/content/ros/package.yaml: -------------------------------------------------------------------------------- 1 | name: ros-talker-listener-{name} 2 | download: git clone https://github.com/snapcraft-docs/ros-talker-listener 3 | -------------------------------------------------------------------------------- /webapp/first_snap/content/ros2/package.yaml: -------------------------------------------------------------------------------- 1 | name: ros2-talker-listener-{name} 2 | download: git clone https://github.com/snapcraft-docs/ros2-talker-listener 3 | -------------------------------------------------------------------------------- /static/js/base/base.js: -------------------------------------------------------------------------------- 1 | import "./navigation"; 2 | import "./polyfills"; 3 | import "./dropdown-menu-toggle"; 4 | import "./notification-dismiss"; 5 | import "./ga"; 6 | -------------------------------------------------------------------------------- /static/sass/_patterns_table.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-p-table { 2 | .p-table--vertical-middle { 3 | td { 4 | vertical-align: middle; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /static/js/publisher/metrics/graphs/territories.js: -------------------------------------------------------------------------------- 1 | import map from "../../../public/snap-details/map"; 2 | 3 | export default function (el, data) { 4 | map(el, data); 5 | } 6 | -------------------------------------------------------------------------------- /static/js/publisher/release/components/releasesConfirmDetails/types.js: -------------------------------------------------------------------------------- 1 | export default { 2 | RELEASE: "release", 3 | UPDATE: "update", 4 | CANCELLATION: "cancel", 5 | }; 6 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Expected behaviour 2 | 3 | ### Steps to reproduce the problem 4 | 5 | ### Specs 6 | 7 | - _URL:_ 8 | - _Operating system:_ 9 | - _Browser:_ 10 | -------------------------------------------------------------------------------- /templates/_layout.html: -------------------------------------------------------------------------------- 1 | {% extends "_base-layout.html" %} 2 | 3 | {% block content %}{% endblock %} 4 | 5 | {% block footer %} 6 | {% include "_footer.html" %} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /static/sass/_snapcraft_p-breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-p-breadcrumbs { 2 | .p-breadcrumbs__item::before, 3 | .p-breadcrumbs__item.is-disabled { 4 | color: $color-mid-light; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /static/js/brand-store/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch } from "react-redux"; 2 | import type { AppDispatch } from "../store"; 3 | 4 | export const useAppDispatch = () => useDispatch(); 5 | -------------------------------------------------------------------------------- /webapp/snapcraft/content/snapcraft_live.yaml: -------------------------------------------------------------------------------- 1 | - text: "Building snaps - Preview: multi-arch remote build service" 2 | time: 2019-05-31T13:00:00Z 3 | url: https://www.youtube.com/watch?v=oR3XLnhypts 4 | -------------------------------------------------------------------------------- /static/sass/_snapcraft_p-table-mobile-card.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-p-table-mobile-card { 2 | .p-table--mobile-card { 3 | .p-table--mobile-card__header { 4 | padding-left: 0; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /templates/docs/_notification.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ contents | safe }}

4 |
5 |
6 | -------------------------------------------------------------------------------- /static/js/publisher/release/reducers/options.js: -------------------------------------------------------------------------------- 1 | // Currently these options are only set as initial state 2 | // in release.js 3 | export default function options(state = { flags: {} }) { 4 | return state; 5 | } 6 | -------------------------------------------------------------------------------- /static/js/public/about/index.js: -------------------------------------------------------------------------------- 1 | import { initFSFLanguageSelect } from "../fsf-language-select"; 2 | import initExpandableArea from "../expandable-area"; 3 | 4 | export { initFSFLanguageSelect, initExpandableArea }; 5 | -------------------------------------------------------------------------------- /templates/home/_fsf_yaml_show_more.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | -------------------------------------------------------------------------------- /static/sass/_patterns_nested-inputs.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-p-nested-inputs { 2 | .p-nested-inputs { 3 | margin-left: $sp-x-large; 4 | 5 | &.is-hidden { 6 | display: none; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /templates/partials/_code-snippet.html: -------------------------------------------------------------------------------- 1 |
2 |
{{ snippet_value }}
3 |
4 | -------------------------------------------------------------------------------- /static/js/publisher/release/actions/pendingCloses.js: -------------------------------------------------------------------------------- 1 | export const CLOSE_CHANNEL = "CLOSE_CHANNEL"; 2 | 3 | export function closeChannel(channel) { 4 | return { 5 | type: CLOSE_CHANNEL, 6 | payload: { channel }, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /static/sass/_snapcraft_p-verified.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-p-verified { 2 | .p-verified { 3 | margin-left: 0.25rem; 4 | 5 | .p-star { 6 | margin-top: -3px; 7 | vertical-align: middle; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /static/js/public/homepage.js: -------------------------------------------------------------------------------- 1 | import { initFSFLanguageSelect } from "./fsf-language-select"; 2 | import nps from "./nps"; 3 | import initExpandableArea from "./expandable-area"; 4 | 5 | export { initExpandableArea, initFSFLanguageSelect, nps }; 6 | -------------------------------------------------------------------------------- /static/js/publisher/settings/utils/index.ts: -------------------------------------------------------------------------------- 1 | import getSettingsData from "./getSettingsData"; 2 | import getChanges from "./getChanges"; 3 | import getFormData from "./getFormData"; 4 | 5 | export { getSettingsData, getChanges, getFormData }; 6 | -------------------------------------------------------------------------------- /static/sass/_utilities_disabled.scss: -------------------------------------------------------------------------------- 1 | @mixin u-disabled { 2 | // make anything look disabled (grayed out and not interactive) 3 | .u-disabled { 4 | filter: grayscale(1); 5 | opacity: 0.6; 6 | pointer-events: none; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /static/js/publisher/release/actions/currentTrack.js: -------------------------------------------------------------------------------- 1 | export const SET_CURRENT_TRACK = "SET_CURRENT_TRACK"; 2 | 3 | export function setCurrentTrack(track) { 4 | return { 5 | type: SET_CURRENT_TRACK, 6 | payload: { track }, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /static/js/publisher/release/actions/revisions.js: -------------------------------------------------------------------------------- 1 | export const UPDATE_REVISIONS = "UPDATE_REVISIONS"; 2 | 3 | export function updateRevisions(revisions) { 4 | return { 5 | type: UPDATE_REVISIONS, 6 | payload: { revisions }, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /static/sass/_utilities_flex.scss: -------------------------------------------------------------------------------- 1 | @mixin u-flex { 2 | // There are several issues with using floats for layout in Vanilla 2.0 3 | // This utility tries to give tools to overcome some of them 4 | .u-flex { 5 | display: flex; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /static/js/publisher/listing/utils/formatFileSize.ts: -------------------------------------------------------------------------------- 1 | function formatFileSize(fileSize: number) { 2 | if (fileSize < 1000000) { 3 | return `${fileSize / 1000}kB`; 4 | } 5 | 6 | return `${fileSize / 1000000}MB`; 7 | } 8 | 9 | export default formatFileSize; 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Set update schedule for GitHub Actions 2 | 3 | version: 2 4 | updates: 5 | 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | # Check for updates to GitHub Actions every weekday 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /static/sass/_patterns_sticky_footer.scss: -------------------------------------------------------------------------------- 1 | @mixin p-sticky-footer { 2 | .p-sticky-footer { 3 | margin-top: auto; 4 | } 5 | 6 | html, 7 | body { 8 | min-height: 100vh; 9 | } 10 | 11 | .has-sticky-footer { 12 | display: flex; 13 | flex-direction: column; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /static/sass/_snapcraft_p-docs.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-pattern-docs { 2 | $banner-background: #106363; 3 | 4 | #search-docs.snapcraft-banner-background { 5 | // sass-lint:disable-line no-ids 6 | background: $banner-background; 7 | } 8 | 9 | .emoji { 10 | height: 1rem; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /templates/store/snap-details/_screenshots.html: -------------------------------------------------------------------------------- 1 |
2 | {% if video %} 3 | {% include "partials/_snap-details-video-layout.html" %} 4 | {% elif screenshots %} 5 | {% include "partials/_snap-details-image-layout.html" %} 6 | {% endif %} 7 |
8 | -------------------------------------------------------------------------------- /static/js/publisher/tour/__mocks__/constants.js: -------------------------------------------------------------------------------- 1 | // this specifically needs to be mocked 2 | // not to rely on getting font size from browser 3 | export const REM = 16; 4 | 5 | export const MASK_OFFSET = 8; 6 | export const SCROLL_MARGIN = 400; 7 | export const SCROLL_OFFSET_TOP = 10; 8 | export const SCROLL_OFFSET_BOTTOM = 10; 9 | -------------------------------------------------------------------------------- /static/js/publisher/listing/types/index.d.ts: -------------------------------------------------------------------------------- 1 | // Empty export to mark this file as a module. 2 | // This is required to augment global scope. 3 | export {}; 4 | 5 | declare global { 6 | interface Window { 7 | SENTRY_DSN: string; 8 | CSRF_TOKEN: string; 9 | listingData: any; 10 | tourSteps: any; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /static/js/publisher/release/reducers/releases.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_RELEASES } from "../actions/releases"; 2 | 3 | export default function revisions(state = [], action) { 4 | switch (action.type) { 5 | case UPDATE_RELEASES: 6 | return [...action.payload.releases]; 7 | default: 8 | return state; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /templates/partials/blog-card--minimal.html: -------------------------------------------------------------------------------- 1 |
2 | {% if article.image %} 3 | 4 | {{ article.image }} 5 | 6 | {% endif %} 7 |

8 | {{ article.title.rendered|safe }} 9 |

10 |
11 | -------------------------------------------------------------------------------- /static/js/publisher/release/reducers/currentTrack.js: -------------------------------------------------------------------------------- 1 | import { SET_CURRENT_TRACK } from "../actions/currentTrack"; 2 | 3 | export default function currentTrack(state = "", action) { 4 | switch (action.type) { 5 | case SET_CURRENT_TRACK: 6 | return action.payload.track; 7 | default: 8 | return state; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /static/sass/_patterns_blog-list.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-p-blog-list { 2 | .p-blog-list__item { 3 | &.is-current { 4 | color: $color-x-dark; 5 | cursor: default; 6 | font-weight: bold; 7 | pointer-events: none; 8 | 9 | &:hover { 10 | text-decoration: none; 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /templates/store/_snap-header-developer-information.html: -------------------------------------------------------------------------------- 1 | {% if developer[1] %} 2 | 3 | {% endif %} 4 | 5 | {{ developer[0] }} 6 | Developer 7 | 8 | {% if developer[1] %} 9 | 10 | {% endif %} 11 | -------------------------------------------------------------------------------- /static/sass/_utilities_no-limit.scss: -------------------------------------------------------------------------------- 1 | @mixin u-no-limit { 2 | // because it's 2019 yet we still have "max-width: 25em" on headings 3 | // no matter what the context. 4 | // Can be removed when https://github.com/canonical-web-and-design/vanilla-framework/issues/1794 5 | // is fixed 6 | .u-no-limit { 7 | max-width: 100%; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /static/js/publisher/release/actions/modal.js: -------------------------------------------------------------------------------- 1 | export const OPEN_MODAL = "OPEN_MODAL"; 2 | export const CLOSE_MODAL = "CLOSE_MODAL"; 3 | 4 | export function openModal(payload) { 5 | return { 6 | type: OPEN_MODAL, 7 | payload, 8 | }; 9 | } 10 | 11 | export function closeModal() { 12 | return { 13 | type: CLOSE_MODAL, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /static/js/publisher/release/reducers/defaultTrack.js: -------------------------------------------------------------------------------- 1 | import { SET_DEFAULT_TRACK_SUCCESS } from "../actions/defaultTrack"; 2 | 3 | export default function defaultTrack(state = "latest", action) { 4 | switch (action.type) { 5 | case SET_DEFAULT_TRACK_SUCCESS: 6 | return action.payload; 7 | default: 8 | return state; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /static/sass/_snapcraft_p-action-form.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-action-form { 2 | .p-action-form { 3 | margin-bottom: $sp-small; 4 | 5 | &:last-child { 6 | margin-bottom: 0; 7 | } 8 | 9 | @media only screen and (min-width: $breakpoint-small) { 10 | display: inline-block; 11 | margin-bottom: 0; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /templates/partials/_verified_developer.html: -------------------------------------------------------------------------------- 1 | 2 | Verified account 3 | Verified account 4 | 5 | -------------------------------------------------------------------------------- /static/js/publisher/release/reducers/architectures.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_ARCHITECTURES } from "../actions/architectures"; 2 | 3 | export default function architectures(state = [], action) { 4 | switch (action.type) { 5 | case UPDATE_ARCHITECTURES: 6 | return [...action.payload.architectures]; 7 | default: 8 | return state; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /templates/partials/_star_developer.html: -------------------------------------------------------------------------------- 1 | 2 | Star developer 3 | Star developer 4 | 5 | -------------------------------------------------------------------------------- /static/js/public/fsf.js: -------------------------------------------------------------------------------- 1 | import initAccordion from "./accordion"; 2 | import { initFSFLanguageSelect } from "./fsf-language-select"; 3 | import firstSnapFlow from "./first-snap-flow"; 4 | import initExpandableArea from "./expandable-area"; 5 | 6 | export { 7 | initAccordion, 8 | initExpandableArea, 9 | initFSFLanguageSelect, 10 | firstSnapFlow, 11 | }; 12 | -------------------------------------------------------------------------------- /static/js/publisher/settings/types/index.d.ts: -------------------------------------------------------------------------------- 1 | // Empty export to mark this file as a module. 2 | // This is required to augment global scope. 3 | export {}; 4 | 5 | declare global { 6 | interface Window { 7 | SENTRY_DSN: string; 8 | CSRF_TOKEN: string; 9 | settingsData: any; 10 | countries: Array<{ key: string; name: string }>; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /static/images/yt_play_btn.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/js/publisher/release/components/historyPanel.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | import RevisionsList from "./revisionsList"; 4 | 5 | export default class HistoryPanel extends Component { 6 | render() { 7 | return ( 8 |
9 | 10 |
11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /static/sass/_patterns_modal.scss: -------------------------------------------------------------------------------- 1 | @mixin vf-p-modal-snapcraft { 2 | .p-modal { 3 | position: fixed; // make it stick to screen regardless of scroll 4 | z-index: 100; // put it on top of everything (including global nav) 5 | 6 | .p-modal__dialog.is-centered-mobile { 7 | left: 0; 8 | position: relative; 9 | right: 0; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /static/sass/_snapcraft-publisher.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-publisher { 2 | .p-strip--image.is-deep { 3 | background-position: center; 4 | min-height: 35vh; 5 | } 6 | 7 | .snap-installs-placeholder { 8 | height: 100%; 9 | width: 100%; 10 | 11 | .p-icon--spinner { 12 | height: $sp-x-large; 13 | width: $sp-x-large; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /entrypoint: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -e 4 | 5 | RUN_COMMAND="talisker.gunicorn.gevent webapp.app:create_app() --bind $1 --worker-class gevent --workers 2 --name talisker-`hostname`" 6 | 7 | if [ "${FLASK_DEBUG}" = true ] || [ "${FLASK_DEBUG}" = 1 ]; then 8 | RUN_COMMAND="${RUN_COMMAND} --reload --log-level debug --timeout 9999" 9 | fi 10 | 11 | ${RUN_COMMAND} 12 | 13 | -------------------------------------------------------------------------------- /static/js/libs/mouse.js: -------------------------------------------------------------------------------- 1 | class Mouse { 2 | constructor() { 3 | this.position = { x: 0, y: 0 }; 4 | 5 | window.addEventListener("mousemove", this.updatePosition.bind(this)); 6 | } 7 | 8 | updatePosition(e) { 9 | this.position = { 10 | x: e.x, 11 | y: e.y, 12 | }; 13 | } 14 | } 15 | 16 | const mouse = new Mouse(); 17 | 18 | export default mouse; 19 | -------------------------------------------------------------------------------- /static/js/publisher/listing/utils/validateAspectRatio.ts: -------------------------------------------------------------------------------- 1 | function validateAspectRatio( 2 | width: number, 3 | height: number, 4 | ratio: { width: number; height: number } 5 | ) { 6 | const aspectRatio = ratio.width / ratio.height; 7 | const expectedHeight = width / aspectRatio; 8 | 9 | return expectedHeight === height; 10 | } 11 | 12 | export default validateAspectRatio; 13 | -------------------------------------------------------------------------------- /static/js/publisher/release/reducers/revisions.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_REVISIONS } from "../actions/revisions"; 2 | 3 | export default function revisions(state = {}, action) { 4 | switch (action.type) { 5 | case UPDATE_REVISIONS: 6 | return { 7 | ...state, 8 | ...action.payload.revisions, 9 | }; 10 | default: 11 | return state; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /static/sass/_snapcraft_p-notifications.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-p-notification { 2 | .p-snapcraft-nps-banner { 3 | flex-direction: row; 4 | flex-wrap: wrap; 5 | margin-top: -$spv--large; 6 | 7 | &__form { 8 | margin-top: 1.1rem; 9 | } 10 | } 11 | 12 | .p-notification__message, 13 | .p-notification__message p { 14 | max-width: 100%; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /static/js/libs/throttle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Throttle 3 | * @param {Function} func Function to run. 4 | * @param {Number} wait Time to wait between tries. 5 | */ 6 | 7 | export default function throttle(func, wait) { 8 | let time = Date.now(); 9 | return function () { 10 | if (time + wait - Date.now() < 0) { 11 | func(); 12 | time = Date.now(); 13 | } 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /static/sass/_snapcraft_embedded_card_modal.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-embedded-card-modal { 2 | .p-embedded-card-modal-dialog { 3 | @media screen and (min-width: $breakpoint-small) { 4 | position: absolute; 5 | top: 1.5rem; 6 | width: 80%; 7 | } 8 | } 9 | 10 | .p-heading-label { 11 | margin-bottom: $spv--small; 12 | margin-top: $spv--medium; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /templates/admin/admin.html: -------------------------------------------------------------------------------- 1 | {% set page_slug = "admin" %} 2 | {% extends "_base-layout.html" %} 3 | {% block content %} 4 |
5 |
6 | 7 | 12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Don't include caches, logs and documentation 2 | *.log 3 | .circleci 4 | .codecov 5 | .git 6 | .github 7 | .gitignore 8 | .vscode 9 | cache.sqlite 10 | README.md 11 | HACKING.md 12 | BRANDSTORES.md 13 | tests 14 | 15 | # The run script isn't needed 16 | run 17 | .docker-project 18 | 19 | # Environemnt 20 | .env.local 21 | 22 | # Node is only needed for building, not running 23 | node_modules 24 | -------------------------------------------------------------------------------- /static/js/publisher/release/actions/globalNotification.js: -------------------------------------------------------------------------------- 1 | export const SHOW_NOTIFICATION = "SHOW_NOTIFICATION"; 2 | export const HIDE_NOTIFICATION = "HIDE_NOTIFICATION"; 3 | 4 | export function showNotification(payload) { 5 | return { 6 | type: SHOW_NOTIFICATION, 7 | payload, 8 | }; 9 | } 10 | 11 | export function hideNotification() { 12 | return { 13 | type: HIDE_NOTIFICATION, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /templates/_footer-brandstore.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /deleted.yaml: -------------------------------------------------------------------------------- 1 | docs/snaps/philosophy: 2 | docs/core/store: 3 | docs/core/snapd: 4 | docs/core/versions: 5 | docs/build-snaps/get-started-snapcraft: 6 | docs/build-snaps/scriptlets: 7 | docs/build-snaps/metadata: 8 | docs/build-snaps/ci-integration: 9 | docs/build-snaps/advanced-features: 10 | docs/reference/: 11 | docs/reference/snap-command: 12 | docs/reference/snapcraft-command: 13 | docs/reference/confinement: 14 | docs/reference/env: 15 | -------------------------------------------------------------------------------- /static/js/publisher/metrics/filters.js: -------------------------------------------------------------------------------- 1 | function selector(selector, name) { 2 | const dropDown = document.querySelector(selector); 3 | 4 | function onChange() { 5 | let params = new URLSearchParams(window.location.search); 6 | 7 | params.set(name, this.value); 8 | 9 | window.location.search = params.toString(); 10 | } 11 | 12 | dropDown.addEventListener("change", onChange); 13 | } 14 | 15 | export { selector }; 16 | -------------------------------------------------------------------------------- /static/js/public/distro-install.js: -------------------------------------------------------------------------------- 1 | import screenshots from "./snap-details/screenshots"; 2 | import videos from "./snap-details/videos"; 3 | import initExpandableArea from "./expandable-area"; 4 | import triggerEventWhenVisible from "./ga-scroll-event"; 5 | import { initLinkScroll } from "./scroll-to"; 6 | 7 | export { 8 | screenshots, 9 | initExpandableArea, 10 | triggerEventWhenVisible, 11 | initLinkScroll, 12 | videos, 13 | }; 14 | -------------------------------------------------------------------------------- /static/js/config/swiper.config.js: -------------------------------------------------------------------------------- 1 | const SCREENSHOTS = { 2 | freeMode: true, 3 | watchOverflow: true, 4 | slidesPerView: "auto", 5 | roundLengths: true, 6 | spaceBetween: 32, 7 | breakpoints: { 8 | 620: { 9 | spaceBetween: 16, 10 | }, 11 | }, 12 | navigation: { 13 | nextEl: `.swiper-button__next`, 14 | prevEl: `.swiper-button__prev`, 15 | }, 16 | }; 17 | 18 | export { SCREENSHOTS as SCREENSHOTS_CONFIG }; 19 | -------------------------------------------------------------------------------- /static/js/publisher/release/components/historyIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | export default function HistoryIcon({ onClick }) { 5 | return ( 6 | 7 | History 8 | 9 | ); 10 | } 11 | 12 | HistoryIcon.propTypes = { 13 | onClick: PropTypes.func.isRequired, 14 | }; 15 | -------------------------------------------------------------------------------- /static/js/publisher/tour/constants.js: -------------------------------------------------------------------------------- 1 | export const REM = parseFloat( 2 | getComputedStyle(document.documentElement).fontSize 3 | ); 4 | export const MASK_OFFSET = REM / 2; // .5rem is a default spacing unit in Vanilla 5 | 6 | export const SCROLL_MARGIN = 400; // if element highlighted element doesn't fit into this top/bottom area of the screen, we scroll it into view 7 | export const SCROLL_OFFSET_TOP = 10; 8 | export const SCROLL_OFFSET_BOTTOM = 10; 9 | -------------------------------------------------------------------------------- /static/sass/_snapcraft_store.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-store { 2 | .p-strip--snap-store { 3 | background-image: 4 | linear-gradient( 5 | 44deg, 6 | #171717 0%, 7 | #181818 9%, 8 | #262626 34%, 9 | #2d2d2d 67%, 10 | #383838 88%, 11 | #2e2e2e 100%, 12 | #393939 100% 13 | ); 14 | } 15 | 16 | .p-icon--snap-store { 17 | height: 10rem; 18 | width: 10rem; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /templates/_header_empty.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@babel/plugin-transform-runtime"], 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "browsers": ["last 2 versions"] 9 | }, 10 | "modules": false 11 | } 12 | ], 13 | "@babel/preset-react", 14 | "@babel/preset-typescript" 15 | ], 16 | "env": { 17 | "test": { 18 | "presets": ["@babel/preset-env"] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /static/sass/_patterns_p-password-toggle-input.scss: -------------------------------------------------------------------------------- 1 | @mixin p-password-toggle-input { 2 | .p-password-toggle-input { 3 | position: relative; 4 | 5 | .p-password-toggle-input__button { 6 | color: $color-link; 7 | padding-left: 0.5rem; 8 | padding-right: 0.5rem; 9 | position: absolute; 10 | right: 0; 11 | top: 0; 12 | 13 | &:hover { 14 | background-color: inherit; 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /static/js/public/newsletter.js: -------------------------------------------------------------------------------- 1 | import { buttonLoading } from "../libs/formHelpers"; 2 | 3 | const MKTOFORM_ID = "mktoForm_3376"; 4 | 5 | function newsletter() { 6 | const form = document.getElementById(MKTOFORM_ID); 7 | 8 | if (!form) { 9 | return; 10 | } 11 | 12 | const button = form.querySelector("button"); 13 | 14 | form.addEventListener("submit", () => { 15 | buttonLoading(button, ""); 16 | }); 17 | } 18 | 19 | export { newsletter }; 20 | -------------------------------------------------------------------------------- /static/js/publisher/listing/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import * as Sentry from "@sentry/react"; 4 | import { Integrations } from "@sentry/tracing"; 5 | import App from "./components/App"; 6 | 7 | Sentry.init({ 8 | dsn: window.SENTRY_DSN, 9 | integrations: [new Integrations.BrowserTracing()], 10 | tracesSampleRate: 1.0, 11 | }); 12 | 13 | ReactDOM.render(, document.getElementById("main-content")); 14 | -------------------------------------------------------------------------------- /static/js/publisher/settings/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import * as Sentry from "@sentry/react"; 4 | import { Integrations } from "@sentry/tracing"; 5 | import App from "./components/App"; 6 | 7 | Sentry.init({ 8 | dsn: window.SENTRY_DSN, 9 | integrations: [new Integrations.BrowserTracing()], 10 | tracesSampleRate: 1.0, 11 | }); 12 | 13 | ReactDOM.render(, document.getElementById("main-content")); 14 | -------------------------------------------------------------------------------- /.github/workflows/run-percy.yml: -------------------------------------------------------------------------------- 1 | name: Run percy 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '0 3 * * tue,wed,thu,fri,sat' 6 | 7 | jobs: 8 | snapshots: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | 13 | - name: Install deps 14 | run: yarn install 15 | 16 | - name: Run Percy 17 | run: export PERCY_TOKEN=${{ secrets.PERCY_TOKEN }}; ./node_modules/.bin/percy snapshot snapshots.yml -------------------------------------------------------------------------------- /static/sass/_patterns_testimonial.scss: -------------------------------------------------------------------------------- 1 | @mixin vf-p-testimonial { 2 | .p-testimonial { 3 | border: 0 none; 4 | margin: 0; 5 | margin-bottom: $sp-large * 2; 6 | padding: 0; 7 | 8 | &__title { 9 | margin-top: $sp-medium; 10 | } 11 | 12 | &__content { 13 | font-style: normal; 14 | } 15 | 16 | &__logo { 17 | height: 3rem; 18 | 19 | img { 20 | max-height: 100%; 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /webapp/first_snap/content/flutter/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: ${name} 2 | version: '1.0' 3 | summary: Super Cool App 4 | description: Super Cool App that does everything! 5 | 6 | confinement: strict 7 | base: core18 8 | 9 | parts: 10 | ${name}: 11 | plugin: flutter 12 | source: https://github.com/kenvandine/super-cool-app.git 13 | flutter-target: lib/main.dart 14 | 15 | apps: 16 | ${name}: 17 | command: super_cool_app 18 | extensions: [flutter-dev] 19 | -------------------------------------------------------------------------------- /static/js/libs/mobile.js: -------------------------------------------------------------------------------- 1 | function isMobile() { 2 | // If the mobile menu button is visible, we're on mobile 3 | const mobileMenuButton = document.querySelector( 4 | ".p-navigation__toggle--open" 5 | ); 6 | 7 | // Use offsetWidth and offsetHeight to figure out if an element is visibile 8 | return ( 9 | mobileMenuButton && 10 | !(mobileMenuButton.offsetWidth === 0 && mobileMenuButton.offsetHeight === 0) 11 | ); 12 | } 13 | 14 | export { isMobile }; 15 | -------------------------------------------------------------------------------- /static/js/publisher/release/actions/index.js: -------------------------------------------------------------------------------- 1 | export * from "./architectures"; 2 | export * from "./availableRevisionsSelect"; 3 | export * from "./channelMap"; 4 | export * from "./currentTrack"; 5 | export * from "./defaultTrack"; 6 | export * from "./history"; 7 | export * from "./modal"; 8 | export * from "./globalNotification"; 9 | export * from "./pendingCloses"; 10 | export * from "./pendingReleases"; 11 | export * from "./releases"; 12 | export * from "./revisions"; 13 | -------------------------------------------------------------------------------- /webapp/first_snap/content/moos/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: ${name} 2 | version: '0.1' 3 | summary: MOOS Example 4 | description: | 5 | This example includes MOOSDB, the main communication mechanism for all MOOS 6 | apps. 7 | 8 | confinement: devmode 9 | base: core18 10 | 11 | parts: 12 | ${name}: 13 | source: https://github.com/themoos/core-moos/archive/v10.4.0.tar.gz 14 | plugin: cmake 15 | build-packages: [g++] 16 | 17 | apps: 18 | ${name}: 19 | command: bin/MOOSDB 20 | -------------------------------------------------------------------------------- /templates/partials/_snap-details-image-layout.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webapp/store/content/distros/kde-neon.yaml: -------------------------------------------------------------------------------- 1 | name: KDE Neon 2 | color-1: "#20A3A8" 3 | color-2: "#167275" 4 | logo: https://assets.ubuntu.com/v1/d0593902-Distro_Logo_KDE+Neon.svg 5 | logo-mono: https://assets.ubuntu.com/v1/31ced116-Distro_Logo_KDE+Neon_White.svg 6 | install: 7 | - 8 | action: | 9 | Snap can be installed from the command line. Open the Konsole terminal and enter the following: 10 | command: | 11 | sudo apt update 12 | sudo apt install snapd 13 | -------------------------------------------------------------------------------- /webapp/store/content/distros/debian.yaml: -------------------------------------------------------------------------------- 1 | name: Debian 2 | color-1: "#A80030" 3 | color-2: "#750022" 4 | logo: https://assets.ubuntu.com/v1/cfdc1144-Distro_Logo_Debian.svg 5 | logo-mono: https://assets.ubuntu.com/v1/dc7be4e8-Distro_Logo_Debian_White.svg 6 | install: 7 | - 8 | action: | 9 | On Debian 9 (Stretch) and newer, snap can be installed directly from the command line: 10 | command: | 11 | sudo apt update 12 | sudo apt install snapd 13 | sudo snap install core 14 | -------------------------------------------------------------------------------- /static/sass/_patterns_button_spinner.scss: -------------------------------------------------------------------------------- 1 | @mixin p-button-spinner { 2 | .has-spinner { 3 | position: relative; 4 | } 5 | 6 | // fighting with Vanilla specificity 7 | [class*="p-button-"] .p-button__spinner { 8 | display: none; 9 | left: calc(50% - 0.5rem); 10 | position: absolute; 11 | top: calc(50% - 11px); 12 | } 13 | 14 | .has-spinner .p-button__spinner { 15 | display: block; 16 | } 17 | 18 | .has-spinner .p-button__text { 19 | opacity: 0; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /static/sass/_snapcraft_dispute-list.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-p-dispute-list { 2 | .p-snapcraft-dispute-list__item { 3 | .p-snapcraft-dispute-list__link { 4 | display: none; 5 | } 6 | 7 | .p-snapcraft-dispute-list__muted { 8 | color: $color-mid; 9 | } 10 | 11 | .p-snapcraft-dispute-list__icon { 12 | display: inline-block; 13 | } 14 | 15 | &:hover { 16 | .p-snapcraft-dispute-list__link { 17 | display: block; 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /templates/partials/livestream-notification.html: -------------------------------------------------------------------------------- 1 | {% if livestream %} 2 |
3 |
4 |
5 |
LIVE
6 |

7 | Watch our Snapcraft Live Stream  {{ livestream.text }} 8 |

9 |
10 |
11 |
12 | {% endif %} 13 | -------------------------------------------------------------------------------- /static/js/publisher/release/reducers/modal.js: -------------------------------------------------------------------------------- 1 | import { OPEN_MODAL, CLOSE_MODAL } from "../actions/modal"; 2 | 3 | export default function modal(state = { visible: false }, action) { 4 | switch (action.type) { 5 | case OPEN_MODAL: 6 | return { 7 | ...state, 8 | visible: true, 9 | ...action.payload, 10 | }; 11 | case CLOSE_MODAL: 12 | return { 13 | ...state, 14 | visible: false, 15 | }; 16 | default: 17 | return state; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /static/js/publisher/release/reducers/availableRevisionsSelect.js: -------------------------------------------------------------------------------- 1 | import { AVAILABLE_REVISIONS_SELECT_UNRELEASED } from "../constants"; 2 | import { SET_AVAILABLE_REVISIONS_SELECT } from "../actions/availableRevisionsSelect"; 3 | 4 | export default function availableRevisionsSelect( 5 | state = AVAILABLE_REVISIONS_SELECT_UNRELEASED, 6 | action 7 | ) { 8 | switch (action.type) { 9 | case SET_AVAILABLE_REVISIONS_SELECT: 10 | return action.payload.value; 11 | default: 12 | return state; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /webapp/first_snap/content/golang/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: ${name} 2 | version: '1.0' 3 | summary: An interactive web server. 4 | description: | 5 | HTTPLab let you inspect HTTP requests and forge responses. 6 | 7 | confinement: devmode 8 | base: core18 9 | 10 | parts: 11 | ${name}: 12 | plugin: go 13 | go-importpath: github.com/gchaincl/httplab 14 | source: https://github.com/gchaincl/httplab 15 | source-type: git 16 | build-packages: 17 | - gcc 18 | 19 | apps: 20 | ${name}: 21 | command: bin/httplab 22 | -------------------------------------------------------------------------------- /webapp/first_snap/content/node/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: ${name} 2 | version: '1.0' 3 | summary: Command line weather tool. 4 | description: | 5 | Get current weather:- 6 | $ wethr 7 | Get current weather in metric units 8 | $ wethr --metric 9 | Get current weather in imperial units 10 | $ wethr --imperial 11 | 12 | confinement: devmode 13 | base: core18 14 | 15 | parts: 16 | ${name}: 17 | plugin: nodejs 18 | source: https://github.com/twobucks/wethr.git 19 | 20 | apps: 21 | ${name}: 22 | command: wethr 23 | -------------------------------------------------------------------------------- /webapp/first_snap/content/ros/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: ${name} 2 | version: '0.1' 3 | summary: ROS Talker/Listener Example 4 | description: | 5 | This example launches a ROS talker and listener. 6 | 7 | base: core18 8 | confinement: devmode 9 | 10 | parts: 11 | ros-tutorials: 12 | plugin: catkin 13 | source: https://github.com/ros/ros_tutorials.git 14 | source-branch: melodic-devel 15 | source-space: roscpp_tutorials/ 16 | 17 | apps: 18 | ${name}: 19 | command: roslaunch roscpp_tutorials talker_listener.launch 20 | -------------------------------------------------------------------------------- /static/js/brand-store/Publisher/Publisher.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Publisher() { 4 | return ( 5 |
6 |

Publisher

7 |

8 | As a publisher you can{" "} 9 | register a snap name on the Snap store and{" "} 10 | 11 | manage your snaps on the dashboard 12 | 13 | . 14 |

15 |
16 | ); 17 | } 18 | 19 | export default Publisher; 20 | -------------------------------------------------------------------------------- /static/sass/_snapcraft_custom-cols.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-custom-cols { 2 | .col-logo { 3 | // These breakpoints are a combination of the content width - for logos we 4 | // consistently have 6 across each at 146 pixels wide 5 | @media (min-width: 772px) and (max-width: 1083px) { 6 | grid-column-end: span 2; 7 | } 8 | } 9 | 10 | .col-lang-select--3 { 11 | // As above but 6 across by col-2 width 12 | @media (min-width: 772px) and (max-width: 1152px) { 13 | grid-column-end: span 2; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /static/js/publisher/listing/utils/validateImageDimensions.ts: -------------------------------------------------------------------------------- 1 | function validateImageDimensions( 2 | imageWidth: number, 3 | imageHeight: number, 4 | dimensions: { 5 | minWidth: number; 6 | maxWidth: number; 7 | minHeight: number; 8 | maxHeight: number; 9 | } 10 | ) { 11 | return ( 12 | imageWidth >= dimensions.minWidth && 13 | imageWidth <= dimensions.maxWidth && 14 | imageHeight >= dimensions.minHeight && 15 | imageHeight <= dimensions.maxHeight 16 | ); 17 | } 18 | 19 | export default validateImageDimensions; 20 | -------------------------------------------------------------------------------- /static/js/publisher/release/actions/architectures.js: -------------------------------------------------------------------------------- 1 | export const UPDATE_ARCHITECTURES = "UPDATE_ARCHITECTURES"; 2 | 3 | export function updateArchitectures(revisions) { 4 | let archs = []; 5 | 6 | revisions.forEach((revision) => { 7 | archs = archs.concat(revision.architectures); 8 | }); 9 | 10 | // make archs unique and sorted 11 | archs = archs.filter((item, i, ar) => ar.indexOf(item) === i); 12 | 13 | return { 14 | type: UPDATE_ARCHITECTURES, 15 | payload: { 16 | architectures: archs, 17 | }, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /static/js/publisher/release/actions/pendingCloses.test.js: -------------------------------------------------------------------------------- 1 | import { CLOSE_CHANNEL, closeChannel } from "./pendingCloses"; 2 | 3 | describe("pendingCloses actions", () => { 4 | const channel = "test/edge"; 5 | 6 | describe("closeChannel", () => { 7 | it("should create an action to close a channel", () => { 8 | expect(closeChannel(channel).type).toBe(CLOSE_CHANNEL); 9 | }); 10 | 11 | it("should supply a payload with channel", () => { 12 | expect(closeChannel(channel).payload.channel).toEqual(channel); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /templates/publisher/_noscript.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /templates/sitemap/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ base_url }} 5 | weekly 6 | 7 | 8 | {% for link in links %} 9 | 10 | {{ link["url"] }} 11 | {% if "last_udpated" in link %} 12 | {{ link["last_udpated"] }} 13 | {% endif %} 14 | monthly 15 | 16 | {% endfor %} 17 | 18 | -------------------------------------------------------------------------------- /static/js/libs/formHelpers.js: -------------------------------------------------------------------------------- 1 | function buttonLoading(button, text) { 2 | button.disabled = true; 3 | button.classList.add("--dark"); 4 | button.innerHTML = ` ${text}`; 5 | } 6 | 7 | function buttonEnabled(button, text) { 8 | button.disabled = false; 9 | button.classList.remove("--dark"); 10 | button.innerHTML = text; 11 | } 12 | 13 | function buttonDisable(button, text) { 14 | button.disabled = true; 15 | button.innerHTML = text; 16 | } 17 | 18 | export { buttonLoading, buttonEnabled, buttonDisable }; 19 | -------------------------------------------------------------------------------- /static/js/publisher/metrics/config.js: -------------------------------------------------------------------------------- 1 | const COLORS = { 2 | active_devices: "#94519E", 3 | }; 4 | 5 | const TICKS = { 6 | X_FREQUENCY: 7, 7 | Y_FREQUENCY: 5, 8 | }; 9 | 10 | const PADDING = { 11 | top: 0, 12 | left: 72, 13 | bottom: 0, 14 | right: 0, 15 | }; 16 | 17 | const COLOR_SCALE = [ 18 | "#1f78b4", 19 | "#b2df8a", 20 | "#33a02c", 21 | "#fb9a99", 22 | "#e31a1c", 23 | "#fdbf6f", 24 | "#ff7f00", 25 | "#cab2d6", 26 | "#6a3d9a", 27 | "#ffff99", 28 | "#a6cee3", 29 | ]; 30 | 31 | export { COLORS, TICKS, PADDING, COLOR_SCALE }; 32 | -------------------------------------------------------------------------------- /static/js/publisher/release/actions/currentTrack.test.js: -------------------------------------------------------------------------------- 1 | import { SET_CURRENT_TRACK, setCurrentTrack } from "./currentTrack"; 2 | 3 | describe("setCurrentTrack actions", () => { 4 | const track = "test"; 5 | 6 | describe("SET_CURRENT_TRACK", () => { 7 | it("should create an action to set track", () => { 8 | expect(setCurrentTrack(track).type).toBe(SET_CURRENT_TRACK); 9 | }); 10 | 11 | it("should supply a payload with track name", () => { 12 | expect(setCurrentTrack(track).payload.track).toEqual(track); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /webapp/first_snap/content/ruby/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: ${name} 2 | version: "0.5.0" 3 | summary: Markdown lint tool 4 | description: | 5 | Style checker/lint tool for markdown files 6 | 7 | confinement: devmode 8 | base: core18 9 | 10 | parts: 11 | ${name}: 12 | source: https://github.com/snapcraft-docs/mdl.git 13 | plugin: ruby 14 | gems: 15 | - rake 16 | - bundler 17 | override-build: | 18 | snapcraftctl build 19 | rake install 20 | build-packages: 21 | - git 22 | apps: 23 | ${name}: 24 | command: bin/mdl 25 | -------------------------------------------------------------------------------- /webapp/first_snap/content/python/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: ${name} 2 | version: '1.0' 3 | summary: OfflineIMAP 4 | description: | 5 | OfflineIMAP is software that downloads your email mailbox(es) as local 6 | Maildirs. OfflineIMAP will synchronize both sides via IMAP. 7 | confinement: devmode 8 | base: core18 9 | 10 | parts: 11 | ${name}: 12 | plugin: python 13 | python-version: python2 14 | source: https://github.com/snapcraft-docs/offlineimap.git 15 | stage-packages: 16 | - python-six 17 | 18 | apps: 19 | ${name}: 20 | command: bin/offlineimap 21 | -------------------------------------------------------------------------------- /static/js/publisher/market/markdown.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | const markdownLink = document.querySelector(".js-toggle-markdown"); 3 | const markdownInfo = document.querySelector(".js-markdown"); 4 | 5 | markdownLink.addEventListener("click", (e) => { 6 | e.preventDefault(); 7 | markdownInfo.classList.toggle("u-hide"); 8 | if (markdownInfo.classList.contains("u-hide")) { 9 | markdownLink.innerHTML = "Show supported markdown syntax"; 10 | } else { 11 | markdownLink.innerHTML = "Hide supported markdown syntax"; 12 | } 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /static/js/publisher/settings/types/SettingsData.d.ts: -------------------------------------------------------------------------------- 1 | type SettingsData = { 2 | snap_name: string; 3 | snap_id: string; 4 | store: string; 5 | status: string; 6 | update_metadata_on_release: boolean; 7 | private: boolean; 8 | unlisted: boolean; 9 | visibility: string; 10 | whitelist_countries: Array; 11 | blacklist_countries: Array; 12 | territory_distribution_status: string; 13 | whitelist_country_keys: string; 14 | blacklist_country_keys: string; 15 | country_keys_status: string | null; 16 | }; 17 | 18 | export type { SettingsData }; 19 | -------------------------------------------------------------------------------- /static/sass/_snapcraft-publicise.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-publicise { 2 | .snapcraft-publicise__images { 3 | img + img { 4 | margin-left: 1.5rem; 5 | } 6 | } 7 | 8 | .snapcraft-publicise__download { 9 | margin-top: 1.6rem; 10 | } 11 | 12 | .p-list__item { 13 | padding-left: $sph--large; 14 | 15 | &.is-selected { 16 | @include vf-highlight-bar($color-mid-light, left); 17 | 18 | font-weight: 400; 19 | 20 | // make sure to override link visited color 21 | a { 22 | color: $color-dark; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /static/js/brand-store/brand-store.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import * as Sentry from "@sentry/react"; 4 | import { Integrations } from "@sentry/tracing"; 5 | import App from "./App"; 6 | import { store } from "./store"; 7 | import { Provider } from "react-redux"; 8 | 9 | Sentry.init({ 10 | dsn: window.SENTRY_DSN, 11 | integrations: [new Integrations.BrowserTracing()], 12 | tracesSampleRate: 1.0, 13 | }); 14 | 15 | ReactDOM.render( 16 | 17 | 18 | , 19 | document.getElementById("root") 20 | ); 21 | -------------------------------------------------------------------------------- /static/js/publisher/release/reducers/globalNotification.js: -------------------------------------------------------------------------------- 1 | import { 2 | SHOW_NOTIFICATION, 3 | HIDE_NOTIFICATION, 4 | } from "../actions/globalNotification"; 5 | 6 | export default function notification(state = { visible: false }, action) { 7 | switch (action.type) { 8 | case SHOW_NOTIFICATION: 9 | return { 10 | ...state, 11 | visible: true, 12 | ...action.payload, 13 | }; 14 | case HIDE_NOTIFICATION: 15 | return { 16 | ...state, 17 | visible: false, 18 | }; 19 | default: 20 | return state; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /templates/sitemap/sitemap-index.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ base_url }}/sitemap-links.xml 5 | 6 | 7 | {{ base_url }}/store/sitemap.xml 8 | 9 | 10 | {{ base_url }}/blog/sitemap.xml 11 | 12 | 13 | {{ base_url }}/docs/sitemap.xml 14 | 15 | 16 | {{ base_url }}/tutorials/sitemap.xml 17 | 18 | 19 | -------------------------------------------------------------------------------- /static/js/brand-store/selectors/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BrandStores, 3 | CurrentStore, 4 | Snaps, 5 | Invites, 6 | Members, 7 | } from "../types/shared"; 8 | 9 | export const brandStoresListSelector = (state: BrandStores) => 10 | state.brandStores.brandStoresList; 11 | export const currentStoreSelector = (state: CurrentStore) => 12 | state.currentStore.currentStore; 13 | export const snapsSelector = (state: Snaps) => state.snaps.snaps; 14 | export const membersSelector = (state: Members) => state.members.members; 15 | export const invitesSelector = (state: Invites) => state.invites.invites; 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const entry = require("./webpack.config.entry.js"); 4 | const rules = require("./webpack.config.rules.js"); 5 | 6 | const production = process.env.ENVIRONMENT !== "devel"; 7 | 8 | module.exports = { 9 | entry: entry, 10 | output: { 11 | filename: "[name].js", 12 | path: __dirname + "/static/js/dist", 13 | }, 14 | mode: production ? "production" : "development", 15 | devtool: production ? "source-map" : "eval-source-map", 16 | module: { 17 | rules: rules, 18 | }, 19 | resolve: { 20 | extensions: [".ts", ".tsx", ".js"], 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /webapp/first_snap/content/ros2/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: ${name} 2 | version: '0.1' 3 | summary: ROS2 Talker/Listener Example 4 | description: | 5 | This example launches a ROS2 talker and listener. 6 | confinement: devmode 7 | base: core18 8 | 9 | parts: 10 | ros-demos: 11 | plugin: colcon 12 | source: https://github.com/ros2/demos.git 13 | source-branch: dashing 14 | colcon-rosdistro: dashing 15 | colcon-source-space: demo_nodes_cpp 16 | stage-packages: [ros-dashing-ros2launch] 17 | 18 | apps: 19 | ${name}: 20 | command: opt/ros/dashing/bin/ros2 launch demo_nodes_cpp talker_listener.launch.py 21 | -------------------------------------------------------------------------------- /webapp/store/content/distros/pop.yaml: -------------------------------------------------------------------------------- 1 | name: Pop!_OS 2 | color-1: "#48B9C7" 3 | color-2: "#102a4c" 4 | logo: https://assets.ubuntu.com/v1/c7ca0dfa-Distro_Logo_Pop.svg 5 | logo-mono: https://assets.ubuntu.com/v1/91e233bc-PopOS-short-white.svg 6 | install: 7 | - 8 | action: | 9 | Snap can be installed on Pop!_OS from the command line. Open Terminal from the Applications launcher and type the following: 10 | command: | 11 | sudo apt update 12 | sudo apt install snapd 13 | - 14 | action: | 15 | Either log out and back in again, or restart your system, to ensure snap’s paths are updated correctly. -------------------------------------------------------------------------------- /static/js/libs/shallowDiff.js: -------------------------------------------------------------------------------- 1 | const shallowDiff = (firstState, secondState) => { 2 | let diff = Object.keys(firstState).some((key) => { 3 | const value = firstState[key]; 4 | if (secondState[key] === undefined) { 5 | return key; 6 | } else if (JSON.stringify(secondState[key]) !== JSON.stringify(value)) { 7 | return key; 8 | } 9 | }); 10 | 11 | if (!diff) { 12 | diff = Object.keys(secondState).some((key) => { 13 | if (firstState[key] === undefined) { 14 | return key; 15 | } 16 | }); 17 | } 18 | 19 | return diff; 20 | }; 21 | 22 | export { shallowDiff as default }; 23 | -------------------------------------------------------------------------------- /static/js/publisher/market/initIcon.test.js: -------------------------------------------------------------------------------- 1 | import { initIcon } from "./initIcon"; 2 | 3 | describe("initIcon", () => { 4 | it("should throw if there is no icon holder", () => { 5 | expect(function () { 6 | initIcon(); 7 | }).toThrow(); 8 | }); 9 | 10 | it("should render to the holder, without images", () => { 11 | const holder = document.createElement("div"); 12 | holder.id = "icon-holder"; 13 | document.body.appendChild(holder); 14 | 15 | initIcon("#icon-holder", {}, "snap", () => {}); 16 | 17 | expect(holder.querySelectorAll(".p-editable-icon").length).toEqual(1); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /templates/publisher/listing.html: -------------------------------------------------------------------------------- 1 | {% extends "publisher/_publisher_layout.html" %} 2 | 3 | {% block meta_title %} 4 | Listing details for {% if display_title %}{{ display_title }}{% else %}{{ snap_title }}{% endif %} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 15 | 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /tests/metrics/tests_metrics.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import webapp.metrics.metrics as metrics 4 | 5 | 6 | class OsMetricTest(unittest.TestCase): 7 | def test_build_os_info(self): 8 | oses = [ 9 | {"name": "test/-", "values": ["0.1"]}, 10 | {"name": "test2/test", "values": ["0.5", "0.9"]}, 11 | ] 12 | 13 | os_metrics = metrics.OsMetric(None, oses, None, None) 14 | expected_result = [ 15 | {"name": "test2 test", "value": "0.9"}, 16 | {"name": "test", "value": "0.1"}, 17 | ] 18 | 19 | self.assertEqual(os_metrics.os, expected_result) 20 | -------------------------------------------------------------------------------- /static/js/brand-store/Reviewer/Reviewer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useParams } from "react-router-dom"; 3 | 4 | import type { RouteParams } from "../types/shared"; 5 | 6 | function Reviewer() { 7 | const { id } = useParams(); 8 | 9 | return ( 10 |
11 |

Reviewer

12 |

13 | As a reviewer you can{" "} 14 | 15 | review the snaps in this store on the dashboard 16 | 17 | . 18 |

19 |
20 | ); 21 | } 22 | 23 | export default Reviewer; 24 | -------------------------------------------------------------------------------- /static/js/publisher/listing/utils/index.ts: -------------------------------------------------------------------------------- 1 | import getChanges from "./getChanges"; 2 | import getFormData from "./getFormData"; 3 | import getListingData from "./getListingData"; 4 | import validateImageDimensions from "./validateImageDimensions"; 5 | import validateAspectRatio from "./validateAspectRatio"; 6 | import formatFileSize from "./formatFileSize"; 7 | import shouldShowUpdateMetadataWarning from "./shouldShowUpdateMetadataWarning"; 8 | 9 | export { 10 | getChanges, 11 | getFormData, 12 | getListingData, 13 | validateImageDimensions, 14 | validateAspectRatio, 15 | formatFileSize, 16 | shouldShowUpdateMetadataWarning, 17 | }; 18 | -------------------------------------------------------------------------------- /static/sass/_snapcraft_banner.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-banner { 2 | $snapcraft-banner-color: #e5eeee; 3 | 4 | .snapcraft-banner-background { 5 | background-color: $snapcraft-banner-color; 6 | background-image: url("#{$assets-path}9689339a-snapcraft-hero-background--light.png"); 7 | background-position: 50% 0; 8 | background-size: 1440px 1440px; 9 | 10 | @media screen and (max-width: $breakpoint-small) { 11 | background-size: 640px 640px; 12 | } 13 | 14 | @media screen and (min-width: $breakpoint-small) and (max-width: $breakpoint-large) { 15 | background-size: 820px 820px; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /static/js/publisher/market/initMedia.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | import React from "react"; 3 | import Media from "../form/media"; 4 | import { MEDIA_RESTRICTIONS } from "./restrictions"; 5 | 6 | function initMedia(mediaHolder, images, updateState) { 7 | const mediaHolderEl = document.querySelector(mediaHolder); 8 | if (!mediaHolderEl) { 9 | throw new Error("No media holder El"); 10 | } 11 | 12 | ReactDOM.render( 13 | , 18 | mediaHolderEl 19 | ); 20 | } 21 | 22 | export { initMedia }; 23 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "babel-eslint", 3 | plugins: ["jest", "react"], 4 | globals: {}, 5 | env: { 6 | browser: true, 7 | es6: true, 8 | node: true, 9 | "jest/globals": true, 10 | }, 11 | extends: [ 12 | "eslint:recommended", 13 | "plugin:react/recommended", 14 | "plugin:prettier/recommended", 15 | ], 16 | parserOptions: { 17 | sourceType: "module", 18 | ecmaFeatures: { 19 | jsx: true, 20 | }, 21 | }, 22 | rules: { 23 | "linebreak-style": ["error", "unix"], 24 | semi: ["error", "always"], 25 | "object-curly-spacing": ["error", "always"], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /templates/docs/_search-bar.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 8 |
9 |
10 | -------------------------------------------------------------------------------- /static/js/publisher/market/whitelistBlacklist.js: -------------------------------------------------------------------------------- 1 | function whitelistBlacklist(form) { 2 | const type = form["territories"].value; 3 | const whitelist = form["whitelist_countries"]; 4 | const blacklist = form["blacklist_countries"]; 5 | const customType = form["territories_custom_type"].value; 6 | 7 | if (type === "all") { 8 | whitelist.value = ""; 9 | blacklist.value = ""; 10 | } 11 | 12 | if (type === "custom") { 13 | if (customType === "whitelist") { 14 | blacklist.value = ""; 15 | } else if (customType === "blacklist") { 16 | whitelist.value = ""; 17 | } 18 | } 19 | } 20 | 21 | export { whitelistBlacklist }; 22 | -------------------------------------------------------------------------------- /static/js/publisher/market/initIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import Icon from "../form/icon"; 4 | 5 | import { ICON_RESTRICTIONS } from "../market/restrictions"; 6 | 7 | function initIcon(holder, icon, title, updateIcon) { 8 | const holderEl = document.querySelector(holder); 9 | 10 | if (!holderEl) { 11 | throw new Error(`${holder} does not exist.`); 12 | } 13 | 14 | ReactDOM.render( 15 | , 21 | holderEl 22 | ); 23 | } 24 | 25 | export { initIcon }; 26 | -------------------------------------------------------------------------------- /static/js/publisher/market/initMedia.test.js: -------------------------------------------------------------------------------- 1 | import { initMedia } from "./initMedia"; 2 | 3 | describe("initMedia", () => { 4 | it("should throw if there is no media holder", () => { 5 | expect(function () { 6 | initMedia(); 7 | }).toThrow(); 8 | }); 9 | 10 | it("should render to the holder, without images", () => { 11 | const holder = document.createElement("div"); 12 | holder.id = "media-holder"; 13 | document.body.appendChild(holder); 14 | 15 | initMedia("#media-holder", [], () => {}); 16 | 17 | expect( 18 | holder.querySelectorAll(".p-listing-images__add-image").length 19 | ).toEqual(1); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /static/sass/_patterns_pagination.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-p-pagination { 2 | // 10.07.2020 - Ovi: This can be remove once this issue is closed 3 | .p-pagination__link--previous { 4 | margin-inline-end: 0.5rem; 5 | } 6 | 7 | .p-pagination__link--previous, 8 | .p-pagination__link--next { 9 | $color-darken-transparent: rgba(0, 0, 0, 0.1); 10 | 11 | &:hover { 12 | background-color: $color-darken-transparent; 13 | } 14 | 15 | &.is-disabled { 16 | cursor: default; 17 | opacity: 0.5; 18 | pointer-events: none; 19 | 20 | &:hover { 21 | background-color: transparent; 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /webapp/first_snap/content/pre-built/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: ${name} 2 | version: 4.2.0 3 | summary: Cross-Platform Benchmark 4 | description: | 5 | Geekbench 4 measures your system's power and tells 6 | you whether your computer is ready to roar. How 7 | strong is your mobile device or desktop computer? 8 | How will it perform when push comes to crunch? 9 | These are the questions that Geekbench can answer. 10 | confinement: devmode 11 | base: core18 12 | 13 | parts: 14 | ${name}: 15 | plugin: dump 16 | source: http://cdn.geekbench.com/Geekbench-$SNAPCRAFT_PROJECT_VERSION-Linux.tar.gz 17 | 18 | apps: 19 | ${name}: 20 | command: geekbench4 21 | -------------------------------------------------------------------------------- /static/sass/_snapcraft_p-form-validation.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-p-form-validation { 2 | .p-form-validation__field { 3 | position: relative; 4 | 5 | // workaround for Vanilla added spacing 6 | * + & { 7 | margin-bottom: $sp-medium; 8 | margin-top: 0.75rem; 9 | } 10 | 11 | .p-form-validation__counter { 12 | color: $color-negative; 13 | line-height: 42px; 14 | margin: 0; 15 | position: absolute; 16 | right: 0.375rem; 17 | top: 0; 18 | } 19 | } 20 | 21 | // hide validation icon when counter is visible 22 | .has-counter .p-form-validation__input { 23 | background-image: none; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /static/sass/_snapcraft_p-side-panel.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-p-side-panel { 2 | .p-side-panel { 3 | background-color: $color-x-light; 4 | bottom: 0; 5 | display: none; 6 | height: 100%; 7 | max-width: 420px; 8 | overflow: hidden; 9 | overflow-y: scroll; 10 | padding: 1.5rem; 11 | position: absolute; 12 | right: 0; 13 | top: 0; 14 | width: 100%; 15 | z-index: 10; 16 | } 17 | 18 | .p-side-panel--open { 19 | box-shadow: 0 0 16px 1px rgba(51, 51, 51, 0.2); 20 | display: block; 21 | } 22 | 23 | .p-side-panel__actions { 24 | bottom: 0; 25 | position: absolute; 26 | right: 1.5rem; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /webapp/store/content/distros/elementary.yaml: -------------------------------------------------------------------------------- 1 | name: elementary OS 2 | color-1: "#95A3AB" 3 | color-2: "#485A6C" 4 | logo: https://assets.ubuntu.com/v1/c0c09661-Distro_Logo_Elementary.svg 5 | logo-mono: https://assets.ubuntu.com/v1/4f36b56d-Distro_Logo_Elementary_White.svg 6 | install: 7 | - 8 | action: | 9 | Snap can be installed on elementary OS from the command line. Open Terminal from the Applications launcher and type the following: 10 | command: | 11 | sudo apt update 12 | sudo apt install snapd 13 | - action: | 14 | Either log out and back in again, or restart your system, to ensure snap’s paths are updated correctly. 15 | -------------------------------------------------------------------------------- /static/js/public/store-details.js: -------------------------------------------------------------------------------- 1 | import map from "./snap-details/map"; 2 | import screenshots from "./snap-details/screenshots"; 3 | import channelMap from "./snap-details/channelMap"; 4 | import videos from "./snap-details/videos"; 5 | import initReportSnap from "./snap-details/reportSnap"; 6 | import initEmbeddedCardModal from "./snap-details/embeddedCard"; 7 | import { snapDetailsPosts } from "./snap-details/blog-posts"; 8 | import initExpandableArea from "./expandable-area"; 9 | 10 | export { 11 | map, 12 | screenshots, 13 | channelMap, 14 | snapDetailsPosts, 15 | initEmbeddedCardModal, 16 | initExpandableArea, 17 | initReportSnap, 18 | videos, 19 | }; 20 | -------------------------------------------------------------------------------- /static/js/publisher/market/stickyListingBar.js: -------------------------------------------------------------------------------- 1 | import throttle from "../../libs/throttle"; 2 | 3 | export const toggleClassWhenStickyOnTop = (el, className) => { 4 | if (el) { 5 | el.classList.toggle(className, el.getBoundingClientRect().top === 0); 6 | } 7 | }; 8 | 9 | export const toggleShadowWhenSticky = (el) => { 10 | toggleClassWhenStickyOnTop(el, "sticky-shadow"); 11 | }; 12 | 13 | export default function () { 14 | const stickyBar = document.querySelector(".js-sticky-bar"); 15 | toggleShadowWhenSticky(stickyBar); 16 | const onScroll = throttle(() => toggleShadowWhenSticky(stickyBar), 30); 17 | document.addEventListener("scroll", onScroll); 18 | } 19 | -------------------------------------------------------------------------------- /tests/snapcraft/tests_public.py: -------------------------------------------------------------------------------- 1 | import responses 2 | from flask_testing import TestCase 3 | from webapp.app import create_app 4 | 5 | # Make sure tests fail on stray responses. 6 | responses.mock.assert_all_requests_are_fired = True 7 | 8 | 9 | class StorePage(TestCase): 10 | 11 | render_templates = False 12 | 13 | def create_app(self): 14 | app = create_app(testing=True) 15 | app.secret_key = "secret_key" 16 | 17 | return app 18 | 19 | @responses.activate 20 | def test_index(self): 21 | response = self.client.get("/") 22 | 23 | assert response.status_code == 200 24 | self.assert_template_used("index.html") 25 | -------------------------------------------------------------------------------- /static/js/brand-store/StoreNotFound/StoreNotFound.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function StoreNotFound() { 4 | return ( 5 |
6 |
7 |
8 |
9 |

Store not found

10 |

11 | Either this store does not exist or you do not have access to it. 12 | If you believe this to be incorrect please contact your admin. 13 |

14 |
15 |
16 |
17 |
18 | ); 19 | } 20 | 21 | export default StoreNotFound; 22 | -------------------------------------------------------------------------------- /static/js/publisher/release/components/globalNotification.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | 3 | import { hideNotification } from "../actions/globalNotification"; 4 | 5 | import Notification from "./notification"; 6 | 7 | const mapStateToProps = ({ notification }) => { 8 | const { content: children, appearance, status, canDismiss } = notification; 9 | 10 | return { 11 | children, 12 | appearance, 13 | status, 14 | canDismiss, 15 | }; 16 | }; 17 | 18 | const mapDispatchToProps = (dispatch) => ({ 19 | hideNotification: () => dispatch(hideNotification()), 20 | }); 21 | 22 | export default connect(mapStateToProps, mapDispatchToProps)(Notification); 23 | -------------------------------------------------------------------------------- /templates/first-snap/language.html: -------------------------------------------------------------------------------- 1 | {% set fsf_step = "language" %} 2 | {% extends "first-snap/_layout_fsf.html" %} 3 | 4 | {% block fsf_content %} 5 |
6 |

First pick the language your app is written in.

7 |
8 | 9 | {% include "partials/fsf_language.html" %} 10 | {% endblock %} 11 | 12 | {% block scripts %} 13 | 21 | {{ super() }} 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /static/js/publisher/release/reducers/branches.js: -------------------------------------------------------------------------------- 1 | import { OPEN_BRANCHES, CLOSE_BRANCHES } from "../actions/branches"; 2 | 3 | export default function branches(state = [], action) { 4 | switch (action.type) { 5 | case OPEN_BRANCHES: { 6 | const newState = state.slice(0); 7 | if (!newState.includes(action.payload)) { 8 | newState.push(action.payload); 9 | } 10 | return newState; 11 | } 12 | case CLOSE_BRANCHES: { 13 | const newState = state.slice(0); 14 | const index = newState.indexOf(action.payload); 15 | newState.splice(index, 1); 16 | return newState; 17 | } 18 | default: 19 | return state; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /static/sass/_snapcraft_p-autocomplete.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-autocomplete { 2 | .p-autocomplete { 3 | position: relative; 4 | 5 | .p-autocomplete__suggestions { 6 | max-height: 220px; 7 | position: absolute; 8 | width: 100%; 9 | z-index: 1; 10 | } 11 | 12 | .p-autocomplete__suggestion { 13 | padding-bottom: 0.5rem; 14 | padding-top: 0.5rem; 15 | } 16 | } 17 | 18 | .p-autocomplete__result-list { 19 | margin-top: $spv--large; 20 | } 21 | 22 | .p-autocomplete__result { 23 | background-color: $color-light; 24 | display: flex; 25 | justify-content: space-between; 26 | margin-bottom: $spv--large; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /templates/50X.html: -------------------------------------------------------------------------------- 1 | {% extends webapp_config['LAYOUT'] %} 2 | 3 | {% block meta_title %}{{ error_name }}{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |

{{ error_name }}

10 |

Please try again later

11 |

You can check the service status at status.snapcraft.io.

12 |

If the problem persists please check the Snapcraft forum or report an issue.

13 |
14 |
15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /static/js/publisher/market/storageCommands.js: -------------------------------------------------------------------------------- 1 | function storageCommands( 2 | e, 3 | formEl, 4 | snap_name, 5 | ignoreChangesOnUnload, 6 | context = window 7 | ) { 8 | const key = `${snap_name}-command`; 9 | if (e.key === key) { 10 | context.localStorage.removeItem(key); 11 | switch (e.newValue) { 12 | case "edit": 13 | context.focus(); 14 | break; 15 | case "revert": 16 | ignoreChangesOnUnload(); 17 | context.location.reload(true); 18 | break; 19 | case "save": 20 | formEl.dispatchEvent(new Event("submit")); 21 | break; 22 | } 23 | } 24 | 25 | return; 26 | } 27 | 28 | export { storageCommands }; 29 | -------------------------------------------------------------------------------- /static/js/publisher/release/actions/revisions.test.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_REVISIONS, updateRevisions } from "./revisions"; 2 | 3 | describe("revisions actions", () => { 4 | describe("updateRevisions", () => { 5 | let revisions = { 6 | 1: { revision: 1 }, 7 | 2: { revision: 2 }, 8 | 3: { revision: 3, channels: ["stable"] }, 9 | }; 10 | 11 | it("should create an action to update revisions list", () => { 12 | expect(updateRevisions(revisions).type).toBe(UPDATE_REVISIONS); 13 | }); 14 | 15 | it("should supply a payload with revisions map", () => { 16 | expect(updateRevisions(revisions).payload.revisions).toEqual(revisions); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /static/js/publisher/release/reducers/history.js: -------------------------------------------------------------------------------- 1 | import { OPEN_HISTORY, CLOSE_HISTORY } from "../actions/history"; 2 | import { CLOSE_CHANNEL } from "../actions/pendingCloses"; 3 | 4 | export default function history( 5 | state = { 6 | filters: null, 7 | isOpen: false, 8 | }, 9 | action 10 | ) { 11 | switch (action.type) { 12 | case OPEN_HISTORY: 13 | return { 14 | ...state, 15 | isOpen: true, 16 | ...action.payload, 17 | }; 18 | case CLOSE_HISTORY: 19 | case CLOSE_CHANNEL: 20 | return { 21 | ...state, 22 | isOpen: false, 23 | filters: null, 24 | }; 25 | default: 26 | return state; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /static/sass/_snapcraft-p-videos.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft_p-videos { 2 | .youtube-thumbnail-button { 3 | background-color: $color-x-dark; 4 | border: none; 5 | height: 100%; 6 | left: 0; 7 | margin-bottom: 0; 8 | padding: 0; 9 | position: absolute; 10 | top: 0; 11 | width: 100%; 12 | z-index: 1; 13 | 14 | &:hover { 15 | background-color: $color-x-dark !important; 16 | } 17 | 18 | &.fade-out { 19 | opacity: 0; 20 | pointer-events: none; 21 | } 22 | } 23 | 24 | .youtube-play-button { 25 | left: 50%; 26 | position: absolute; 27 | top: 50%; 28 | transform: translate(-50%, -50%); 29 | z-index: 20; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /static/sass/_snapcraft_p-navigation.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-p-navigation { 2 | .p-subnav__item { 3 | // https://github.com/canonical-web-and-design/vanilla-framework/issues/2752 4 | background-color: #333; 5 | 6 | // https://github.com/canonical-web-and-design/vanilla-framework/issues/2753 7 | @media screen and (max-width: 800px) { 8 | padding-left: 0; 9 | } 10 | 11 | // https://github.com/canonical-web-and-design/vanilla-framework/pull/2708 12 | &, 13 | &:active, 14 | &:focus, 15 | &:visited { 16 | color: $colors--dark-theme--text-default; 17 | } 18 | 19 | &:hover { 20 | color: $colors--dark-theme--text-hover; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/publisher/tests_account_logout.py: -------------------------------------------------------------------------------- 1 | import responses 2 | from tests.publisher.endpoint_testing import BaseTestCases 3 | 4 | # Make sure tests fail on stray responses. 5 | responses.mock.assert_all_requests_are_fired = True 6 | 7 | 8 | class LogoutRedirects(BaseTestCases.BaseAppTesting): 9 | def setUp(self): 10 | endpoint_url = "/logout" 11 | 12 | super().setUp(snap_name=None, endpoint_url=endpoint_url, api_url=None) 13 | 14 | @responses.activate 15 | def test_logout(self): 16 | response = self.client.get(self.endpoint_url) 17 | 18 | self.assertEqual(302, response.status_code) 19 | 20 | self.assertEqual("http://localhost/", response.location) 21 | -------------------------------------------------------------------------------- /static/js/brand-store/store/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import brandStoreReducer from "../slices/brandStoreSlice"; 3 | import currentStoreReducer from "../slices/currentStoreSlice"; 4 | import snapsSelector from "../slices/snapsSlice"; 5 | import membersSelector from "../slices/membersSlice"; 6 | import invitesSelector from "../slices/invitesSlice"; 7 | 8 | export const store = configureStore({ 9 | reducer: { 10 | brandStores: brandStoreReducer, 11 | currentStore: currentStoreReducer, 12 | snaps: snapsSelector, 13 | members: membersSelector, 14 | invites: invitesSelector, 15 | }, 16 | }); 17 | 18 | export type AppDispatch = typeof store.dispatch; 19 | -------------------------------------------------------------------------------- /static/js/libs/shallowDiff.test.js: -------------------------------------------------------------------------------- 1 | import shallowDiff from "./shallowDiff"; 2 | 3 | describe("shallowDiff", () => { 4 | it("should return false if nothing is different", () => { 5 | expect(shallowDiff({}, {})).toEqual(false); 6 | }); 7 | 8 | it("should return true if different", () => { 9 | let initialState = { test: true }; 10 | let addedState = { ...initialState, added: true }; 11 | let removedState = {}; 12 | let changeState = { test: false }; 13 | 14 | expect(shallowDiff(initialState, addedState)).toEqual(true); 15 | expect(shallowDiff(initialState, removedState)).toEqual(true); 16 | expect(shallowDiff(initialState, changeState)).toEqual(true); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /static/js/publisher/release/reducers/defaultTrack.test.js: -------------------------------------------------------------------------------- 1 | import defaultTrack from "./defaultTrack"; 2 | 3 | import { SET_DEFAULT_TRACK_SUCCESS } from "../actions/defaultTrack"; 4 | 5 | describe("defaultTrack", () => { 6 | it("should return the initial state", () => { 7 | expect(defaultTrack(undefined, {})).toEqual("latest"); 8 | }); 9 | 10 | describe("on SET_DEFAULT_TRACK_SUCESS", () => { 11 | it("should set the default track", () => { 12 | const result = defaultTrack( 13 | {}, 14 | { 15 | type: SET_DEFAULT_TRACK_SUCCESS, 16 | payload: "test", 17 | } 18 | ); 19 | 20 | expect(result).toEqual("test"); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /templates/partials/blog-card.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% if article.image and article.image.source_url %} 4 | 9 | {% endif %} 10 |

11 | {{ article.title.rendered|safe }} 12 |

13 |

by {{ article.author.name }} on {{ article.date }}

14 | {% if not article.image %} 15 |

{{ article.excerpt.raw }}

16 | {% endif %} 17 |
18 |
19 | -------------------------------------------------------------------------------- /templates/store/_empty-category-partial.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% set i = 0 %} 4 | {% for _ in range(0, 9) %} 5 |
6 |
7 |
8 |
9 |

10 | Placeholder 11 |

12 |
13 |

Placeholder

14 |
15 |
16 |
17 |
18 | {% endfor %} 19 |
20 |
21 | -------------------------------------------------------------------------------- /templates/503.html: -------------------------------------------------------------------------------- 1 | {% extends webapp_config['LAYOUT'] %} 2 | 3 | {% block meta_title %}Maintenance mode enabled{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |

We're experiencing issues or doing maintenance

10 |

Please try again later

11 |

You can check the service status at status.snapcraft.io.

12 |

If the problem persists please check the Snapcraft forum or report an issue.

13 |
14 |
15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /templates/store/_snap-header-information.html: -------------------------------------------------------------------------------- 1 | {% if has_publisher_page %} 2 | {{ display_name(publisher, username) }} 3 | {% else %} 4 | 5 | {{ display_name(publisher, username) }} 6 | Publisher 7 | 8 | {% endif %} 9 | 10 | {% if developer_validation %} 11 | {% if developer_validation == VERIFIED_PUBLISHER %} 12 | {% include "partials/_verified_developer.html" %} 13 | {% endif %} 14 | 15 | {% if developer_validation == STAR_DEVELOPER %} 16 | {% include "partials/_star_developer.html" %} 17 | {% endif %} 18 | {% endif %} -------------------------------------------------------------------------------- /webapp/first_snap/content/rust/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: ${name} 2 | version: '1.0' 3 | summary: A fast CSV command line toolkit written in Rust 4 | description: | 5 | xsv is a command line program for indexing, 6 | slicing, analyzing, splitting and joining CSV 7 | files. Commands should be simple, fast and 8 | composable: 9 | - Simple tasks should be easy. 10 | - Performance trade offs should be exposed 11 | in the CLI interface. 12 | - Composition should not come at the 13 | expense of performance. 14 | 15 | confinement: devmode 16 | base: core18 17 | 18 | parts: 19 | ${name}: 20 | plugin: rust 21 | source: https://github.com/snapcraft-docs/xsv.git 22 | 23 | apps: 24 | ${name}: 25 | command: bin/xsv 26 | -------------------------------------------------------------------------------- /konf/limenet.snapcraft.io.yaml: -------------------------------------------------------------------------------- 1 | domain: limenet.snapcraft.io 2 | 3 | image: prod-comms.docker-registry.canonical.com/limenet.snapcraft.io 4 | 5 | env: 6 | - name: WEBAPP 7 | value: limenet 8 | 9 | useProxy: false 10 | 11 | memoryLimit: 256Mi 12 | 13 | production: 14 | replicas: 5 15 | nginxConfigurationSnippet: | 16 | more_set_headers "Link: ; rel=preconnect; crossorigin, ; rel=preconnect"; 17 | 18 | # Overrides for staging 19 | staging: 20 | replicas: 3 21 | nginxConfigurationSnippet: | 22 | more_set_headers "X-Robots-Tag: noindex"; 23 | more_set_headers "Link: ; rel=preconnect; crossorigin, ; rel=preconnect"; 24 | -------------------------------------------------------------------------------- /templates/first-snap/_layout_fsf.html: -------------------------------------------------------------------------------- 1 | {% extends webapp_config['LAYOUT'] %} 2 | 3 | {% block meta_copydoc %}https://docs.google.com/spreadsheets/d/1gN_xcaAFFP_ckDvWeNDpD9ogbRXFgcXN0w0ITZQB0e4/edit#gid=779498506{% endblock %} 4 | 5 | {% block content %} 6 | {% include "first-snap/_breadcrumb.html" %} 7 |
8 | {% block fsf_content %} 9 | {% endblock %} 10 |
11 | 12 | {% if self.fsf_pagination() %} 13 |
14 | {% block fsf_pagination %}{% endblock %} 15 |
16 | {% endif %} 17 | {% endblock %} 18 | 19 | {% block scripts_includes %} 20 | 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /static/js/brand-store/Members/memberRoles.js: -------------------------------------------------------------------------------- 1 | const ROLES = { 2 | admin: { 3 | name: "Admin", 4 | description: 5 | "Admins manage the store's users and roles, and control the store's settings.", 6 | }, 7 | review: { 8 | name: "Reviewer", 9 | description: 10 | "Reviewers can approve or reject snaps, and edit snap declarations.", 11 | }, 12 | view: { 13 | name: "Viewer", 14 | description: 15 | "Viewers are read-only users and can view snap details, metrics, and the contents of this store.", 16 | }, 17 | access: { 18 | name: "Publisher", 19 | description: 20 | "Publishers can invite collaborators to a snap, publish snaps and update snap details.", 21 | }, 22 | }; 23 | 24 | export default ROLES; 25 | -------------------------------------------------------------------------------- /static/js/publisher/market/initBanner.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import Banner from "../form/banner"; 4 | import { BANNER_RESTRICTIONS } from "./restrictions"; 5 | 6 | function initBanner(holder, banners, nextState) { 7 | const bannerHolderEl = document.querySelector(holder); 8 | 9 | if (!bannerHolderEl) { 10 | throw new Error("No banner holder defined"); 11 | } 12 | 13 | let banner = {}; 14 | 15 | if (banners[0]) { 16 | banner = banners[0]; 17 | } 18 | 19 | ReactDOM.render( 20 | , 25 | bannerHolderEl 26 | ); 27 | } 28 | 29 | export { initBanner }; 30 | -------------------------------------------------------------------------------- /static/js/publisher/release/actions/branches.js: -------------------------------------------------------------------------------- 1 | export const OPEN_BRANCHES = "OPEN_BRANCHES"; 2 | export const CLOSE_BRANCHES = "CLOSE_BRANCHES"; 3 | 4 | export function openBranches(channelName) { 5 | return { 6 | type: OPEN_BRANCHES, 7 | payload: channelName, 8 | }; 9 | } 10 | 11 | export function closeBranches(channelName) { 12 | return { 13 | type: CLOSE_BRANCHES, 14 | payload: channelName, 15 | }; 16 | } 17 | 18 | export function toggleBranches(channelName) { 19 | return (dispatch, getState) => { 20 | const { branches } = getState(); 21 | 22 | if (branches.includes(channelName)) { 23 | dispatch(closeBranches(channelName)); 24 | } else { 25 | dispatch(openBranches(channelName)); 26 | } 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /static/js/publisher/release/reducers/currentTrack.test.js: -------------------------------------------------------------------------------- 1 | import currentTrack from "./currentTrack"; 2 | 3 | import { SET_CURRENT_TRACK } from "../actions/currentTrack"; 4 | 5 | describe("currentTrack", () => { 6 | it("should return the initial state", () => { 7 | expect(currentTrack(undefined, {})).toEqual(""); 8 | }); 9 | 10 | describe("on SET_CURRENT_TRACK action", () => { 11 | const track = "test"; 12 | const setCurrentTrackAction = { 13 | type: SET_CURRENT_TRACK, 14 | payload: { track }, 15 | }; 16 | 17 | it("should set current track to given name", () => { 18 | const result = currentTrack("previousTrack", setCurrentTrackAction); 19 | 20 | expect(result).toEqual(track); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /webapp/store/content/distros/fedora.yaml: -------------------------------------------------------------------------------- 1 | name: Fedora 2 | color-1: "#3C6EB4" 3 | color-2: "#294172" 4 | logo: https://assets.ubuntu.com/v1/c93d842f-fedora.png 5 | logo-mono: https://assets.ubuntu.com/v1/c93d842f-fedora.png 6 | install: 7 | - 8 | action: | 9 | Snap can be installed on Fedora from the command line: 10 | command: | 11 | sudo dnf install snapd 12 | - 13 | action: | 14 | Either log out and back in again, or restart your system, to ensure snap’s paths are updated correctly. 15 | - 16 | action: | 17 | To enable classic snap support, enter the following to create a symbolic link between /var/lib/snapd/snap and /snap: 18 | command: | 19 | sudo ln -s /var/lib/snapd/snap /snap 20 | -------------------------------------------------------------------------------- /static/js/publisher/tour/tourBar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import { tourStartedByUser } from "./metricsEvents"; 5 | 6 | export default function TourBar({ showTour }) { 7 | const onButtonClick = () => { 8 | tourStartedByUser(); 9 | showTour(); 10 | }; 11 | 12 | return ( 13 |
14 |
15 | 22 |
23 |
24 | ); 25 | } 26 | 27 | TourBar.propTypes = { 28 | showTour: PropTypes.func, 29 | }; 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./static/js/dist", 4 | "baseUrl": "src", 5 | "target": "ES2020", 6 | "module": "esnext", 7 | "jsx": "react-jsx", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "lib": ["dom", "dom.iterable", "esnext"], 13 | "allowJs": true, 14 | "allowSyntheticDefaultImports": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": false, 20 | "sourceMap": true 21 | }, 22 | "include": ["./static/js"], 23 | "paths": [ 24 | { 25 | "*": ["types/*.d.ts"] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /konf/sdrsatcom.snapcraft.io.yaml: -------------------------------------------------------------------------------- 1 | domain: sdrsatcom.snapcraft.io 2 | 3 | image: prod-comms.docker-registry.canonical.com/sdrsatcom.snapcraft.io 4 | 5 | env: 6 | - name: WEBAPP 7 | value: sdrsatcom 8 | 9 | useProxy: false 10 | 11 | memoryLimit: 256Mi 12 | 13 | # Overrides for production 14 | production: 15 | replicas: 5 16 | nginxConfigurationSnippet: | 17 | more_set_headers "Link: ; rel=preconnect; crossorigin, ; rel=preconnect"; 18 | 19 | # Overrides for staging 20 | staging: 21 | replicas: 3 22 | nginxConfigurationSnippet: | 23 | more_set_headers "X-Robots-Tag: noindex"; 24 | more_set_headers "Link: ; rel=preconnect; crossorigin, ; rel=preconnect"; 25 | -------------------------------------------------------------------------------- /webapp/decorators.py: -------------------------------------------------------------------------------- 1 | # Core packages 2 | import functools 3 | 4 | # Third party packages 5 | import flask 6 | from webapp import authentication 7 | 8 | 9 | def login_required(func): 10 | """ 11 | Decorator that checks if a user is logged in, and redirects 12 | to login page if not. 13 | """ 14 | 15 | @functools.wraps(func) 16 | def is_user_logged_in(*args, **kwargs): 17 | last_login_method = flask.request.cookies.get("last_login_method") 18 | login_path = "login-beta" if last_login_method == "candid" else "login" 19 | 20 | if not authentication.is_authenticated(flask.session): 21 | return flask.redirect(f"/{login_path}?next={flask.request.path}") 22 | 23 | return func(*args, **kwargs) 24 | 25 | return is_user_logged_in 26 | -------------------------------------------------------------------------------- /static/js/publisher/market/license.js: -------------------------------------------------------------------------------- 1 | function license(form) { 2 | const type = form["license-type"].value; 3 | form["license"].value = form[`license-${type}`].value; 4 | } 5 | 6 | function initLicenses(inputs) { 7 | function licenseTypeChange() { 8 | var type = this.value; 9 | inputs.forEach((item) => { 10 | if (item.id.includes(type)) { 11 | item.style.display = "block"; 12 | } else { 13 | item.style.display = "none"; 14 | } 15 | }); 16 | } 17 | 18 | var licenseRadio = document.querySelectorAll('[name="license-type"]'); 19 | if (licenseRadio) { 20 | for (var i = 0; i < licenseRadio.length; i++) { 21 | licenseRadio[i].addEventListener("change", licenseTypeChange); 22 | } 23 | } 24 | } 25 | 26 | export { license, initLicenses }; 27 | -------------------------------------------------------------------------------- /static/js/publisher/release/actions/modal.test.js: -------------------------------------------------------------------------------- 1 | import { OPEN_MODAL, CLOSE_MODAL, openModal, closeModal } from "./modal"; 2 | 3 | describe("modal actions", () => { 4 | const dummyPayload = { 5 | title: "test", 6 | }; 7 | 8 | describe("openModal", () => { 9 | it("should create an action to open the modal", () => { 10 | const modalAction = openModal(dummyPayload); 11 | expect(modalAction.type).toBe(OPEN_MODAL); 12 | expect(modalAction.payload).toEqual(dummyPayload); 13 | }); 14 | }); 15 | 16 | describe("closeModal", () => { 17 | it("should create an action to close the modal", () => { 18 | const modalAction = closeModal(); 19 | expect(modalAction.type).toBe(CLOSE_MODAL); 20 | expect(modalAction.payload).toBeUndefined(); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /static/js/publisher/builds/components/select.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const Select = ({ options, updateSelection, disabled, selectedOption }) => ( 5 | 20 | ); 21 | 22 | Select.propTypes = { 23 | options: PropTypes.array.isRequired, 24 | selectedOption: PropTypes.string, 25 | updateSelection: PropTypes.func, 26 | disabled: PropTypes.bool, 27 | }; 28 | 29 | export default Select; 30 | -------------------------------------------------------------------------------- /static/js/libs/debounce.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Debounce 3 | * @param {Function} func Function to run. 4 | * @param {Number} wait Time to wait between tries. 5 | * @param {Boolean} immediate Immediately call func. 6 | */ 7 | export default function debounce(func, wait, immediate) { 8 | let timeout; 9 | 10 | const debounced = function () { 11 | const context = this, 12 | args = arguments; 13 | let later = function () { 14 | timeout = null; 15 | if (!immediate) func.apply(context, args); 16 | }; 17 | const callNow = immediate && !timeout; 18 | clearTimeout(timeout); 19 | timeout = setTimeout(later, wait); 20 | if (callNow) func.apply(context, args); 21 | }; 22 | 23 | debounced.clear = function () { 24 | clearTimeout(timeout); 25 | }; 26 | 27 | return debounced; 28 | } 29 | -------------------------------------------------------------------------------- /webapp/store/content/distros/raspbian.yaml: -------------------------------------------------------------------------------- 1 | name: Raspberry Pi 2 | color-1: "#c05672" 3 | color-2: "#a22846" 4 | logo: https://assets.ubuntu.com/v1/193cb6ac-logo-raspberry-pi.svg 5 | logo-mono: https://assets.ubuntu.com/v1/a31b1451-rpi-b-w.svg 6 | install: 7 | - action: | 8 | On a Raspberry Pi running the latest version of Raspbian snap can be installed directly from the command line: 9 | command: | 10 | sudo apt update 11 | sudo apt install snapd 12 | - action: | 13 | You will also need to reboot your device: 14 | command: | 15 | sudo reboot 16 | - action: | 17 | After this, install the core snap in order to get the latest snapd: 18 | command: | 19 | sudo snap install core 20 | -------------------------------------------------------------------------------- /static/js/publisher/form/AccordionHelp.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, fireEvent } from "@testing-library/react"; 3 | import AccordionHelp from "./AccordionHelp"; 4 | 5 | describe("AccordionHelp", () => { 6 | it("renders with a title", () => { 7 | const { container } = render(); 8 | expect(container.querySelector("a").innerHTML).toEqual("Show test"); 9 | }); 10 | 11 | it("opens when toggled", () => { 12 | const { container } = render( 13 | 14 |
15 | 16 | ); 17 | const aEl = container.querySelector("a"); 18 | fireEvent.click(aEl); 19 | expect(aEl.innerHTML).toEqual("Hide test"); 20 | expect(container.querySelectorAll(".todd").length).toEqual(1); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /BRANDSTORES.md: -------------------------------------------------------------------------------- 1 | # Brand stores 2 | 3 | To create a brand store, create a file with the name of the store in the folder `webapp/configs`. Then run the project with the environment variable WEBAPP set with the name of the store. 4 | 5 | ## Example 6 | 7 | Let's create the brand store storePlus. First create the file `webapp/configs/storePlus.py` 8 | 9 | ```python 10 | # webapp/configs/storePlus.py 11 | 12 | WEBAPP_CONFIG = { 13 | 'LAYOUT': '_layout-brandstore.html', # custom layout for brandstores 14 | 'STORE_NAME': 'Store Plus', # Store name displayed in the header 15 | 'STORE_QUERY': 'storePlus', # Store to query to the snap store 16 | } 17 | ``` 18 | 19 | Then run the project with this command, make sure the WEBAPP has the same name as the brand config file: 20 | 21 | ```bash 22 | ./run --env WEBAPP=storePlus 23 | ``` 24 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Sub dependencies pinned to avoid issues 2 | ruamel.yaml.clib==0.2.2 3 | click==8.0.3 4 | 5 | # App dependencies 6 | canonicalwebteam.flask-base==1.0.5 7 | canonicalwebteam.candid==0.9.0 8 | canonicalwebteam.discourse==5.0.1 9 | canonicalwebteam.blog==6.4.0 10 | canonicalwebteam.search==1.2.7 11 | canonicalwebteam.image-template==1.3.1 12 | canonicalwebteam.store-api==3.2.9 13 | canonicalwebteam.launchpad==0.8.3 14 | django-openid-auth==0.16 15 | Flask-OpenID==1.3.0 16 | Flask-WTF==0.15.1 17 | bleach==3.3.1 18 | humanize==3.13.1 19 | mistune==2.0.4 20 | pybadges==3.0.0 21 | pycountry==20.7.3 22 | pymacaroons==0.13.0 23 | python-dateutil==2.8.2 24 | requests==2.27.1 25 | responses==0.17.0 26 | ruamel.yaml==0.16.12 27 | vcrpy-unittest==0.1.7 28 | user-agents==2.2.0 29 | 30 | # Development dependencies 31 | Flask-Testing==0.8.1 32 | freezegun==0.3.4 -------------------------------------------------------------------------------- /static/sass/_search-form.scss: -------------------------------------------------------------------------------- 1 | @mixin search-form { 2 | .p-form--search { 3 | width: 60%; 4 | 5 | @media screen and (max-width: $breakpoint-large - 1) { 6 | width: 100%; 7 | 8 | .p-button--positive { 9 | width: 100%; 10 | } 11 | } 12 | 13 | .p-form__group { 14 | flex-grow: 1; 15 | 16 | &--no-grow { 17 | flex-grow: 0; 18 | } 19 | 20 | @media screen and (max-width: $breakpoint-large - 1) { 21 | margin-bottom: $spv--large; 22 | } 23 | } 24 | 25 | .p-form__control { 26 | width: 100%; 27 | } 28 | } 29 | 30 | .snapcraft-banner-background .p-form--search { 31 | width: 100%; 32 | } 33 | 34 | .no-search-query { 35 | font-weight: 400; 36 | overflow: hidden; 37 | text-overflow: ellipsis; 38 | white-space: nowrap; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /static/sass/_utilities_crop.scss: -------------------------------------------------------------------------------- 1 | // From blog.ubuntu.com 2 | // https://github.com/canonical-web-and-design/blog.ubuntu.com/blob/master/static/sass/styles.scss 3 | 4 | @mixin u-insights-crop { 5 | // Crop to 16:9 6 | .u-crop--16-9 { 7 | position: relative; 8 | 9 | &::before { 10 | content: ""; 11 | display: block; 12 | padding-top: 56.25%; 13 | width: 100%; 14 | } 15 | 16 | // Incase img isn't wrapped in a link 17 | > a, 18 | > img { 19 | bottom: 0; 20 | left: 0; 21 | overflow: hidden; 22 | position: absolute; 23 | right: 0; 24 | top: 0; 25 | } 26 | 27 | > a { 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: center; 31 | text-align: center; 32 | } 33 | 34 | img { 35 | align-self: auto; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /templates/partials/_snap-details-video-layout.html: -------------------------------------------------------------------------------- 1 |
2 | {# TODO: u-align--center should be u-align-text--center 3 | see https://github.com/canonical-web-and-design/vanilla-framework/issues/2448 #} 4 |
5 | {% if video %} 6 |
7 | {% include "partials/_video.html" %} 8 |
9 | {% endif %} 10 |
11 | {% if screenshots %} 12 |
13 | {% for screenshot in screenshots %} 14 | {% include "partials/_snap-screenshot.html" %} 15 | {% endfor %} 16 |
17 | {% endif %} 18 |
19 | -------------------------------------------------------------------------------- /static/js/public/modal.js: -------------------------------------------------------------------------------- 1 | function toggleModal(modal) { 2 | if (modal && modal.classList.contains("p-modal")) { 3 | if (modal.style.display === "none") { 4 | modal.style.display = "flex"; 5 | document.body.style.position = "fixed"; 6 | } else { 7 | modal.style.display = "none"; 8 | document.body.style.position = "relative"; 9 | } 10 | } 11 | } 12 | 13 | function init() { 14 | document.addEventListener("click", (e) => { 15 | const target = e.target; 16 | const modalId = target.getAttribute("aria-controls"); 17 | const modal = document.getElementById(modalId); 18 | 19 | if (Object.keys(target.dataset).includes("jsToggleModal")) { 20 | toggleModal(modal); 21 | } 22 | 23 | if (target.classList.contains("p-modal")) { 24 | toggleModal(target); 25 | } 26 | }); 27 | } 28 | 29 | export { init }; 30 | -------------------------------------------------------------------------------- /webapp/configs/limenet.py: -------------------------------------------------------------------------------- 1 | WEBAPP_CONFIG = { 2 | "LAYOUT": "_layout-brandstore.html", 3 | "STORE_NAME": "LimeNET", 4 | "STORE_BRAND": { 5 | "LOGO": "https://assets.ubuntu.com/v1/cc2b2033-lime-logo.svg", 6 | "COLOUR": "#fff", 7 | }, 8 | "STORE_LINKS": [ 9 | {"title": "Lime Micro", "href": "http://www.limemicro.com/"}, 10 | {"title": "Myriad-RF", "href": "https://myriadrf.org/"}, 11 | ], 12 | "CUSTOM_STYLES": ".p-navigation__link a,.p-navigation__link a:visited,\ 13 | .p-navigation__link a:focus {color: #000;}\ 14 | .p-navigation__link a:hover {color: #049619; background: transparent;}", 15 | "STORE_INSTALL_INSTRUCTIONS": True, 16 | "FOOTER_TEXT": "Copyright 2019 Canonical Ltd.
\ 17 | Ubuntu and Canonical are registered trademarks of Canonical Ltd.", 18 | "STORE_QUERY": "LimeNET", 19 | } 20 | -------------------------------------------------------------------------------- /templates/partials/_hiri-case-study.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |

Case study

8 |
9 |
10 |

Related blog posts

11 |
12 |
13 | 21 |
22 | -------------------------------------------------------------------------------- /templates/publisher/register-name-dispute-success.html: -------------------------------------------------------------------------------- 1 | {% extends "publisher/_publisher_layout.html" %} 2 | 3 | {% block meta_title %} 4 | Snapcraft - Register name dispute 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |

‹ My snaps

11 |

Thank you for requesting the name {{snap_name}}

12 |

We will proccess the details provided with the name dispute

13 |

Each case is reviewed individually and we can't provide an estimate on how long it will take for us to process this information. We will contact you once we confirm the information provided

14 |
15 | Return to my snaps 16 |
17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /static/js/brand-store/types/shared.ts: -------------------------------------------------------------------------------- 1 | export type RouteParams = { 2 | id: string; 3 | }; 4 | 5 | export type Member = { 6 | displayname: string; 7 | email: string; 8 | id: string; 9 | roles: string[]; 10 | username: string; 11 | current_user: boolean; 12 | }; 13 | 14 | export type BrandStores = { 15 | brandStores: { 16 | brandStoresList: Array<{}>; 17 | }; 18 | }; 19 | 20 | export type CurrentStore = { 21 | currentStore: { 22 | currentStore: { 23 | id: string; 24 | "manual-review-policy": string; 25 | private: boolean; 26 | }; 27 | }; 28 | }; 29 | 30 | export type Snaps = { 31 | snaps: { 32 | snaps: Array<{}>; 33 | }; 34 | }; 35 | 36 | export type Invites = { 37 | invites: { 38 | invites: Array<{}>; 39 | }; 40 | }; 41 | 42 | export type Members = { 43 | members: { 44 | members: Array; 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /static/js/publisher/release/actions/gaEventTracking.js: -------------------------------------------------------------------------------- 1 | import { triggerEventReleaseUI } from "../../../base/ga"; 2 | 3 | export function triggerGAEvent() { 4 | const eventLabelItems = [...arguments]; 5 | const eventAction = eventLabelItems.shift(); 6 | let eventLabel = ""; 7 | 8 | return (dispatch, getState) => { 9 | const currentState = getState(); 10 | 11 | if (eventLabelItems.length > 1) { 12 | eventLabel = `from:${currentState.options.snapName}/ 13 | ${eventLabelItems[0]} to:${currentState.options.snapName}/ 14 | ${eventLabelItems[1]}`; 15 | } else if (eventLabelItems.length === 1) { 16 | eventLabel = `${currentState.options.snapName}/${eventLabelItems[0]}`; 17 | } else { 18 | eventLabel = currentState.options.snapName; 19 | } 20 | triggerEventReleaseUI(eventAction, eventLabel); 21 | return { type: "GA_EVENT_SENT" }; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /templates/partials/_publisher-featured-snap.html: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /static/js/publisher/release/components/releasesConfirmDetails/globalRow.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const GlobalRow = ({ useGlobal, toggleGlobal }) => { 5 | return ( 6 |
7 | 8 | 14 | 17 | 18 |
19 | ); 20 | }; 21 | 22 | GlobalRow.propTypes = { 23 | useGlobal: PropTypes.bool, 24 | toggleGlobal: PropTypes.func, 25 | globalPercentage: PropTypes.number, 26 | updatePercentage: PropTypes.func, 27 | }; 28 | 29 | export default GlobalRow; 30 | -------------------------------------------------------------------------------- /webapp/publisher/github/views.py: -------------------------------------------------------------------------------- 1 | import flask 2 | from webapp.api.github import GitHub 3 | from webapp.decorators import login_required 4 | from werkzeug.exceptions import Unauthorized 5 | 6 | publisher_github = flask.Blueprint( 7 | "github", __name__, template_folder="/templates", static_folder="/static" 8 | ) 9 | 10 | 11 | @publisher_github.route("/publisher/github/get-repos", methods=["GET"]) 12 | @login_required 13 | def get_repos(): 14 | github = GitHub(flask.session.get("github_auth_secret")) 15 | org = flask.request.args.get("org") 16 | 17 | try: 18 | if org: 19 | repos = github.get_org_repositories(org) 20 | else: 21 | repos = github.get_user_repositories() 22 | except Unauthorized: 23 | return ( 24 | flask.jsonify({"error": "You need to be authenticated on GitHub"}), 25 | 401, 26 | ) 27 | 28 | return flask.jsonify(repos) 29 | -------------------------------------------------------------------------------- /tests/tests_requests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from requests.exceptions import ConnectionError, Timeout 4 | 5 | import responses 6 | from webapp.api import requests 7 | from webapp.api.exceptions import ApiConnectionError, ApiTimeoutError 8 | 9 | 10 | class RequestsCacheTest(unittest.TestCase): 11 | @responses.activate 12 | def test_connection_api_error(self): 13 | test_url = "https://snapcraft.io" 14 | session = requests.Session() 15 | responses.add(responses.GET, test_url, body=ConnectionError()) 16 | with self.assertRaises(ApiConnectionError): 17 | session.get(test_url) 18 | 19 | @responses.activate 20 | def test_timeout_api_error(self): 21 | test_url = "https://snapcraft.io" 22 | session = requests.Session() 23 | responses.add(responses.GET, test_url, body=Timeout()) 24 | with self.assertRaises(ApiTimeoutError): 25 | session.get(test_url) 26 | -------------------------------------------------------------------------------- /webapp/configs/sdrsatcom.py: -------------------------------------------------------------------------------- 1 | WEBAPP_CONFIG = { 2 | "LAYOUT": "_layout-brandstore.html", 3 | "STORE_NAME": "sdrsatcom", 4 | "STORE_BRAND": { 5 | "LOGO": "https://assets.ubuntu.com/v1/7a1fe153-ESA.png", 6 | "COLOUR": "#fff", 7 | }, 8 | "STORE_LINKS": [ 9 | { 10 | "title": "Documentation", 11 | "href": "https://wiki.myriadrf.org/SDR_Satcom_App_Store", 12 | }, 13 | {"title": "SDR Satcom", "href": "https://sdrsat.com"}, 14 | ], 15 | "CUSTOM_STYLES": ".p-navigation__link a,.p-navigation__link a:visited,\ 16 | .p-navigation__link a:focus {color: #000;}\ 17 | .p-navigation__link a:hover {color: #002568; background: transparent;}", 18 | "STORE_INSTALL_INSTRUCTIONS": True, 19 | "FOOTER_TEXT": "Copyright 2019 Canonical Ltd.
\ 20 | Ubuntu and Canonical are registered trademarks of Canonical Ltd.", 21 | "STORE_QUERY": "SDR_Satcom", 22 | } 23 | -------------------------------------------------------------------------------- /static/js/publisher/release/components/releasesConfirmDetails/cancelProgressiveRow.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const CancelProgressiveRow = ({ release }) => { 5 | const revisionInfo = release.revision; 6 | 7 | return revisionInfo.architectures.map((arch) => { 8 | const previousRevision = release.previousRevisions[0]; 9 | return ( 10 |
14 | Cancel 15 | 16 | {revisionInfo.revision} in {release.channel} on{" "} 17 | {arch}. Revert to {previousRevision.revision}. 18 | 19 |
20 | ); 21 | }); 22 | }; 23 | 24 | CancelProgressiveRow.propTypes = { 25 | release: PropTypes.object, 26 | }; 27 | 28 | export default CancelProgressiveRow; 29 | -------------------------------------------------------------------------------- /templates/store/snap-details/_templates.html: -------------------------------------------------------------------------------- 1 | {# templates #} 2 | {% set article_title = {'rendered': '${title}'} %} 3 | {% set article = {'slug': '${slug}', 'title': article_title, 'image': '${image}'} %} 4 | {% set container_class = "${container_class}" %} 5 | 8 | 9 | {% set video = {'type': 'youtube', 'url': '${url}', 'id': '${id}'} %} 10 | 13 | {% set video = {'type': 'vimeo', 'url': '${url}', 'id': '${id}'} %} 14 | 17 | {% set video = {'type': 'asciinema', 'url': '${url}', 'id': '${id}'} %} 18 | 21 | -------------------------------------------------------------------------------- /static/js/publisher/tour/metricsEvents.js: -------------------------------------------------------------------------------- 1 | import { triggerEvent } from "../../base/ga"; 2 | 3 | export const tourStartedByUser = () => 4 | triggerEvent( 5 | "tour-started-by-user", 6 | window.location.href, 7 | "", 8 | `Tour started manually by user on "${document.title}" page` 9 | ); 10 | 11 | export const tourStartedAutomatically = () => 12 | triggerEvent( 13 | "tour-started-automatically", 14 | window.location.href, 15 | "", 16 | `Tour started automatically on "${document.title}" page` 17 | ); 18 | 19 | export const tourFinished = (stepId) => 20 | triggerEvent( 21 | "tour-finished", 22 | window.location.href, 23 | "", 24 | `Tour finished on "${document.title}" page on step ${stepId}` 25 | ); 26 | 27 | export const tourSkipped = (stepId) => 28 | triggerEvent( 29 | "tour-skipped", 30 | window.location.href, 31 | "", 32 | `Tour skipped on "${document.title}" page on step ${stepId}` 33 | ); 34 | -------------------------------------------------------------------------------- /static/js/publisher/release/actions/architectures.test.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_ARCHITECTURES, updateArchitectures } from "./architectures"; 2 | 3 | describe("architectures actions", () => { 4 | describe("updateArchitectures", () => { 5 | let revisions = [ 6 | { revision: 1, architectures: ["amd64", "armhf"] }, 7 | { revision: 2, architectures: ["test", "test2"] }, 8 | { 9 | revision: 3, 10 | channels: ["stable"], 11 | architectures: ["amd64", "test2"], 12 | }, 13 | ]; 14 | 15 | it("should create an action to update architectures list", () => { 16 | expect(updateArchitectures(revisions).type).toBe(UPDATE_ARCHITECTURES); 17 | }); 18 | 19 | it("should supply a payload with architectures", () => { 20 | expect(updateArchitectures(revisions).payload.architectures).toEqual([ 21 | "amd64", 22 | "armhf", 23 | "test", 24 | "test2", 25 | ]); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /static/js/publisher/release/reducers/availableRevisionsSelect.test.js: -------------------------------------------------------------------------------- 1 | import availableRevisionsSelect from "./availableRevisionsSelect"; 2 | import { SET_AVAILABLE_REVISIONS_SELECT } from "../actions/availableRevisionsSelect"; 3 | import { AVAILABLE_REVISIONS_SELECT_UNRELEASED } from "../constants"; 4 | 5 | describe("releases", () => { 6 | it("should return the initial state", () => { 7 | expect(availableRevisionsSelect(undefined, {})).toEqual( 8 | AVAILABLE_REVISIONS_SELECT_UNRELEASED 9 | ); 10 | }); 11 | 12 | describe("on SET_AVAILABLE_REVISIONS_SELECT action", () => { 13 | let setSelectAction = { 14 | type: SET_AVAILABLE_REVISIONS_SELECT, 15 | payload: { 16 | value: "test", 17 | }, 18 | }; 19 | 20 | it("should set new value in state", () => { 21 | const result = availableRevisionsSelect("", setSelectAction); 22 | 23 | expect(result).toEqual(setSelectAction.payload.value); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /static/js/publisher/release/reducers/pendingCloses.js: -------------------------------------------------------------------------------- 1 | import { CLOSE_CHANNEL } from "../actions/pendingCloses"; 2 | import { 3 | RELEASE_REVISION, 4 | CANCEL_PENDING_RELEASES, 5 | } from "../actions/pendingReleases"; 6 | 7 | // channels to be closed: 8 | // [ "track/risk", ... ] 9 | export default function pendingCloses(state = [], action) { 10 | switch (action.type) { 11 | case CLOSE_CHANNEL: 12 | if (state.includes(action.payload.channel)) { 13 | return state; 14 | } 15 | return [...state, action.payload.channel]; 16 | case RELEASE_REVISION: 17 | if (!state.includes(action.payload.channel)) { 18 | return state; 19 | } 20 | state = [...state]; 21 | // remove channel released to from closing channels 22 | state.splice(state.indexOf(action.payload.channel), 1); 23 | return state; 24 | case CANCEL_PENDING_RELEASES: 25 | return []; 26 | default: 27 | return state; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /static/js/brand-store/ReviewerAndPublisher/ReviewerAndPublisher.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useParams } from "react-router-dom"; 3 | 4 | import type { RouteParams } from "../types/shared"; 5 | 6 | function ReviewerAndPublisher() { 7 | const { id } = useParams(); 8 | 9 | return ( 10 |
11 |

Reviewer and publisher

12 |

13 | As a publisher you can{" "} 14 | register a snap name on the Snap store and{" "} 15 | 16 | manage your snaps on the dashboard 17 | 18 | . 19 |

20 |

21 | As a reviewer you can{" "} 22 | 23 | review the snaps in this store on the dashboard 24 | 25 | . 26 |

27 |
28 | ); 29 | } 30 | 31 | export default ReviewerAndPublisher; 32 | -------------------------------------------------------------------------------- /static/js/publisher/market/publicMetrics.js: -------------------------------------------------------------------------------- 1 | const NAMES = { 2 | public_metrics_territories: "installed_base_by_country_percent", 3 | public_metrics_distros: 4 | "weekly_installed_base_by_operating_system_normalized", 5 | }; 6 | 7 | function publicMetrics(form) { 8 | const publicMetricsEnabled = form["public_metrics_enabled"].checked; 9 | 10 | let blackList = []; 11 | 12 | Object.keys(NAMES).forEach((name) => { 13 | const checked = form[name].checked; 14 | 15 | if (!checked) { 16 | blackList.push(NAMES[name]); 17 | } 18 | 19 | if (!publicMetricsEnabled) { 20 | form[name].setAttribute("disabled", "disabled"); 21 | } else { 22 | form[name].removeAttribute("disabled"); 23 | } 24 | }); 25 | 26 | if (blackList.length > 0) { 27 | form["public_metrics_blacklist"].setAttribute("value", blackList.join(",")); 28 | } else { 29 | form["public_metrics_blacklist"].removeAttribute("value"); 30 | } 31 | } 32 | 33 | export { NAMES, publicMetrics }; 34 | -------------------------------------------------------------------------------- /static/js/publisher/release/actions/globalNotification.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | SHOW_NOTIFICATION, 3 | HIDE_NOTIFICATION, 4 | showNotification, 5 | hideNotification, 6 | } from "./globalNotification"; 7 | 8 | describe("notification actions", () => { 9 | describe("showNotification", () => { 10 | it("should create an action to show notification", () => { 11 | const showNotificationAction = showNotification({ status: "success" }); 12 | 13 | expect(showNotificationAction.type).toBe(SHOW_NOTIFICATION); 14 | expect(showNotificationAction.payload).toEqual({ 15 | status: "success", 16 | }); 17 | }); 18 | }); 19 | 20 | describe("closeNotification", () => { 21 | it("should create an action to hide the notification", () => { 22 | const hideNotificationAction = hideNotification(); 23 | 24 | expect(hideNotificationAction.type).toBe(HIDE_NOTIFICATION); 25 | expect(hideNotificationAction.payload).toBeUndefined(); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /static/sass/_patterns_blog-post.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-p-blog-post { 2 | .p-blog-post { 3 | * { 4 | max-width: 100%; 5 | } 6 | } 7 | 8 | .p-blog-aside { 9 | position: sticky; 10 | top: 1rem; 11 | } 12 | 13 | .p-blog-post--guest-post { 14 | $inner-box-shadow-color: rgba(205, 205, 205, 0.2); 15 | 16 | & > a { 17 | border: 1px solid $color-mid-light; 18 | border-radius: 2px; 19 | box-shadow: inset 0 0 5px 1px $inner-box-shadow-color; 20 | display: block; 21 | overflow: hidden; 22 | 23 | img:first-of-type { 24 | border-radius: 0; 25 | display: block; 26 | height: auto; 27 | position: relative; 28 | width: 100%; 29 | z-index: -1; 30 | } 31 | } 32 | } 33 | 34 | .p-blog-post__source { 35 | border-radius: 0; 36 | display: block; 37 | height: 39px; 38 | margin-bottom: -39px; 39 | position: relative; 40 | top: -39px; 41 | width: 143px; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /static/sass/_utilities_margin-sizing.scss: -------------------------------------------------------------------------------- 1 | @import "vanilla-framework/scss/vanilla/settings"; 2 | 3 | @mixin u-margin-sizing { 4 | .u-margin-medium { 5 | margin: $sp-medium !important; 6 | 7 | &--top { 8 | margin-top: $sp-medium !important; 9 | } 10 | 11 | &--right { 12 | margin-right: $sp-medium !important; 13 | } 14 | 15 | &--bottom { 16 | margin-bottom: $sp-medium !important; 17 | } 18 | 19 | &--left { 20 | margin-left: $sp-medium !important; 21 | } 22 | } 23 | 24 | .u-margin-small { 25 | margin: $sp-small !important; 26 | 27 | &--top { 28 | margin-top: $sp-small !important; 29 | } 30 | 31 | &--right { 32 | margin-right: $sp-small !important; 33 | } 34 | 35 | &--bottom { 36 | margin-bottom: $sp-small !important; 37 | } 38 | 39 | &--left { 40 | margin-left: $sp-small !important; 41 | } 42 | } 43 | 44 | // ... etc for other spacing values (xx-small, x-small, large, x-large...) 45 | } 46 | -------------------------------------------------------------------------------- /static/sass/_patterns_first-snap-flow.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-p-first-snap-flow { 2 | .p-flow-details { 3 | width: 100%; 4 | } 5 | 6 | .p-flow-details__continue { 7 | clear: both; 8 | } 9 | 10 | .p-code-yaml { 11 | font-size: 14px; 12 | line-height: 1.5em; 13 | 14 | b { 15 | color: $color-positive; 16 | } 17 | } 18 | 19 | // workaround for bringing back default p styles in accordion 20 | // before this vanilla bug is fixed: 21 | // https://github.com/canonical-web-and-design/vanilla-framework/issues/2301 22 | .p-accordion p { 23 | margin-bottom: map-get($sp-after, p) - map-get($nudges, nudge--p); 24 | } 25 | 26 | .p-accordion__panel { 27 | $icon-size: map-get($icon-sizes, accordion); 28 | 29 | overflow: visible; 30 | padding-left: $sph--large + $icon-size + $sph--large; // same as accordion button 31 | } 32 | 33 | .p-form-validation__message { 34 | display: none; 35 | 36 | .is-error & { 37 | display: block; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /static/js/publisher/publisher.js: -------------------------------------------------------------------------------- 1 | import * as metrics from "./metrics/metrics"; 2 | import { selector } from "./metrics/filters"; 3 | import * as market from "./form"; 4 | import { initRepoConnect } from "./builds/components/repoConnect"; 5 | import * as publicise from "./publicise"; 6 | import { initCategories } from "./market/categories"; 7 | import markdownToggle from "./market/markdown"; 8 | import stickyListingBar from "./market/stickyListingBar"; 9 | import { preview } from "./preview"; 10 | import submitEnabler from "./submitEnabler"; 11 | import * as tour from "./tour"; 12 | import { initBuilds } from "./builds"; 13 | import { initRepoDisconnect } from "./builds/repoDisconnect"; 14 | import buildStatus from "./build-status"; 15 | 16 | export { 17 | initCategories, 18 | initRepoConnect, 19 | markdownToggle, 20 | metrics, 21 | market, 22 | publicise, 23 | selector, 24 | stickyListingBar, 25 | preview, 26 | submitEnabler, 27 | tour, 28 | initBuilds, 29 | initRepoDisconnect, 30 | buildStatus, 31 | }; 32 | -------------------------------------------------------------------------------- /static/js/publisher/release/components/releasesConfirmDetails/closeChannelsRow.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const CloseChannelsRow = ({ channels }) => { 5 | let group = Array.from(channels); 6 | let last; 7 | if (channels.length > 1) { 8 | last = group.pop(); 9 | } 10 | return ( 11 |
12 | Close 13 | 14 | {group 15 | .map((channel) => {channel}) 16 | .reduce((acc, el) => { 17 | return acc === null ? [el] : [...acc, ", ", el]; 18 | }, null)} 19 | {last ? ( 20 | 21 | {" "} 22 | & {last} 23 | 24 | ) : ( 25 | "" 26 | )} 27 | 28 |
29 | ); 30 | }; 31 | 32 | CloseChannelsRow.propTypes = { 33 | channels: PropTypes.array, 34 | }; 35 | 36 | export default CloseChannelsRow; 37 | -------------------------------------------------------------------------------- /static/js/publisher/tour/tourOverlayMask.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const getClipPathFromMask = ({ top, bottom, left, right }) => { 5 | let mask = [ 6 | `${left}px ${top}px`, 7 | `${left}px ${bottom}px`, 8 | `${right}px ${bottom}px`, 9 | `${right}px ${top}px`, 10 | `${left}px ${top}px`, 11 | ].join(","); 12 | 13 | return `polygon(0 0, 100% 0, 100% 100%, 0 100%, 0 0, ${mask})`; 14 | }; 15 | 16 | export default function TourOverlayMask({ mask }) { 17 | let maskStyle = {}; 18 | 19 | if (mask) { 20 | const clipPath = getClipPathFromMask(mask); 21 | maskStyle = { 22 | clipPath, 23 | WebkitClipPath: clipPath, 24 | }; 25 | } 26 | 27 | return
; 28 | } 29 | 30 | TourOverlayMask.propTypes = { 31 | mask: PropTypes.shape({ 32 | top: PropTypes.number, 33 | bottom: PropTypes.number, 34 | left: PropTypes.number, 35 | right: PropTypes.number, 36 | }), 37 | }; 38 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends webapp_config['LAYOUT'] %} 2 | 3 | {% block meta_title %}404: Page not found{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |

404: Aw, snap! We couldn't find that page

10 |

Looking for a snap? Try searching:

11 | 20 |

Or try the Snapcraft Forum or Documentation

21 |
22 |
23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /templates/publisher/settings.html: -------------------------------------------------------------------------------- 1 | {% extends "publisher/_publisher_layout.html" %} 2 | 3 | {% block meta_title %} 4 | Settings for {% if display_title %}{{ display_title }}{% else %}{{ snap_title }}{% endif %} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | 25 | 26 | {% endblock %} 27 | 28 | 29 | -------------------------------------------------------------------------------- /webapp/first_snap/content/pre-built/test.yaml: -------------------------------------------------------------------------------- 1 | name: test-geekbench4-{name} 2 | linux: 3 | - action: "Install the snap:" 4 | command: sudo snap install --devmode --dangerous *.snap 5 | - action: "List your installed snaps to confirm:" 6 | command: snap list 7 | - action: "Run the snap:" 8 | command: ${name} -h 9 | macos: 10 | - action: "Create a Linux virtual machine for testing snaps:" 11 | command: multipass launch -n testvm 12 | - action: "Return to the directory containing the snapcraft.yaml and map it into the virtual machine:" 13 | command: 'multipass copy-files ${name}*.snap testvm:' 14 | - action: "Connect to the virtual machine using Multipass:" 15 | command: multipass shell testvm 16 | - action: "Install the snap inside the virtual machine:" 17 | command: sudo snap install --devmode --dangerous 18 | - action: "Confirm the snap is installed by listing your installed snaps:" 19 | command: snap list 20 | - action: "Run the snap inside the virtual machine:" 21 | command: ${name} 22 | -------------------------------------------------------------------------------- /.github/workflows/docs-links.yaml: -------------------------------------------------------------------------------- 1 | name: docs links on snapcraft.io/docs 2 | 3 | on: 4 | schedule: 5 | - cron: "0 13 * * 1" 6 | 7 | jobs: 8 | check-links: 9 | if: github.repository == 'canonical-web-and-design/snapcraft.io' 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout master 14 | uses: actions/checkout@v3 15 | 16 | - name: Install linkchecker 17 | run: sudo pip install LinkChecker 18 | 19 | - name: Run linkchecker 20 | run: linkchecker --no-follow-url '!https:\/\/snapcraft.io\/docs\/' --ignore-url "https://munchkin.marketo.net" --ignore-url "https://www.gstatic.com" --ignore-url "https://assets.ubuntu.com$" --ignore-url https://snapcraft.io/static/css/styles.css* --check-extern --no-warnings https://snapcraft.io/docs 21 | 22 | - name: Send message on failure 23 | if: failure() 24 | run: curl -X POST -F "workflow=${GITHUB_WORKFLOW}" -F "repo_name=${GITHUB_REPOSITORY}" -F "action_id=${GITHUB_RUN_ID}" ${{ secrets.BOT_URL }}?room=docs 25 | -------------------------------------------------------------------------------- /webapp/first_snap/content/ros/build.yaml: -------------------------------------------------------------------------------- 1 | name: ros-talker-listener-{name} 2 | linux: 3 | auto: 4 | - action: "Create a directory, move the downloaded snapcraft.yaml file into the new directory and navigate there:" 5 | command: mkdir ~/${name} && mv ~/Downloads/snapcraft.yaml ~/${name} && cd ~/${name} 6 | - action: "Run snapcraft:" 7 | command: snapcraft 8 | - warning: | 9 | Make sure the name matches what has been used previously (i.e ${name}) 10 | macos: 11 | auto: 12 | - action: "Create a directory, move the downloaded snapcraft.yaml file into the new directory and navigate there:" 13 | command: mkdir ~/${name} && mv ~/Downloads/snapcraft.yaml ~/${name} && cd ~/${name} 14 | - action: "Run snapcraft:" 15 | command: snapcraft 16 | windows: 17 | auto: 18 | - action: Run Windows Subsystem for Linux from the Windows Start menu 19 | - action: "Return to the root directory of the project containing your snapcraft.yaml and run snapcraft:" 20 | command: snapcraft 21 | -------------------------------------------------------------------------------- /webapp/first_snap/content/ros2/build.yaml: -------------------------------------------------------------------------------- 1 | name: ros2-talker-listener-{name} 2 | linux: 3 | auto: 4 | - action: "Create a directory, move the downloaded snapcraft.yaml file into the new directory and navigate there:" 5 | command: mkdir ~/${name} && mv ~/Downloads/snapcraft.yaml ~/${name} && cd ~/${name} 6 | - action: "Run snapcraft:" 7 | command: snapcraft 8 | - warning: | 9 | Make sure the name matches what has been used previously (i.e ${name}) 10 | macos: 11 | auto: 12 | - action: "Create a directory, move the downloaded snapcraft.yaml file into the new directory and navigate there:" 13 | command: mkdir ~/${name} && mv ~/Downloads/snapcraft.yaml ~/${name} && cd ~/${name} 14 | - action: "Run snapcraft:" 15 | command: snapcraft 16 | windows: 17 | auto: 18 | - action: Run Windows Subsystem for Linux from the Windows Start menu 19 | - action: "Return to the root directory of the project containing your snapcraft.yaml and run snapcraft:" 20 | command: snapcraft 21 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard-scss", 3 | "plugins": [ 4 | "stylelint-order" 5 | ], 6 | "rules": { 7 | "order/properties-alphabetical-order": true, 8 | "color-function-notation": "legacy", 9 | "alpha-value-notation": "number", 10 | "selector-class-pattern": null, 11 | "selector-max-empty-lines": 1, 12 | "no-invalid-position-at-import-rule": null, 13 | "scss/at-extend-no-missing-placeholder": null, 14 | "scss/percent-placeholder-pattern": null, 15 | "scss/dollar-variable-pattern": null, 16 | "scss/at-mixin-pattern": null, 17 | "scss/no-global-function-names": null, 18 | "declaration-block-no-redundant-longhand-properties": null, 19 | "at-rule-no-unknown": [ 20 | true, 21 | { 22 | "ignoreAtRules": [ 23 | "extend", 24 | "include", 25 | "mixin", 26 | "for", 27 | "function", 28 | "if", 29 | "warn", 30 | "return", 31 | "use" 32 | ] 33 | } 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /webapp/first_snap/content/ruby/test.yaml: -------------------------------------------------------------------------------- 1 | name: test-mdl-{name} 2 | linux: 3 | - action: "Install the snap:" 4 | command: sudo snap install --devmode --dangerous *.snap 5 | - action: "List your installed snaps to confirm:" 6 | command: snap list 7 | - action: "Run the snap:" 8 | command: ${name} -h 9 | macos: 10 | - action: Create a Linux virtual machine for testing snaps 11 | command: multipass launch -n testvm 12 | - action: Return to the directory containing the .snap file and copy it into the virtual machine 13 | command: 'multipass copy-files ${name}*.snap testvm:' 14 | - action: Connect to the virtual machine 15 | command: multipass shell testvm 16 | - action: Install the snap inside the virtual machine 17 | command: sudo snap install --devmode --dangerous ${name}*.snap 18 | - action: Confirm the snap is installed by listing your installed snaps 19 | command: snap list 20 | - action: Run the snap inside the virtual machine 21 | command: ${name} -h 22 | - action: Exit the virtual machine 23 | command: exit 24 | -------------------------------------------------------------------------------- /templates/410.html: -------------------------------------------------------------------------------- 1 | {% extends webapp_config['LAYOUT'] %} 2 | 3 | {% block meta_title %}404: Page not found{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |

410: Aw, snap! The page you are looking for was deleted

10 |

Looking for a snap? Try searching:

11 | 20 |

Or try the Snapcraft Forum or Documentation

21 |
22 |
23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /static/js/publisher/release/reducers/releases.test.js: -------------------------------------------------------------------------------- 1 | import releases from "./releases"; 2 | import { UPDATE_RELEASES } from "../actions/releases"; 3 | 4 | describe("releases", () => { 5 | it("should return the initial state", () => { 6 | expect(releases(undefined, {})).toEqual([]); 7 | }); 8 | 9 | describe("on UPDATE_REVISIONS action", () => { 10 | let updateReleasesAction = { 11 | type: UPDATE_RELEASES, 12 | payload: { 13 | releases: [{ revision: 1 }, { revision: 2 }, { revision: 3 }], 14 | }, 15 | }; 16 | 17 | it("should add new releases to state", () => { 18 | const result = releases({}, updateReleasesAction); 19 | 20 | expect(result).toEqual(updateReleasesAction.payload.releases); 21 | }); 22 | 23 | it("should replace existing releases in state", () => { 24 | const initialState = [{ revision: 5 }, { revision: 6 }]; 25 | 26 | const result = releases(initialState, updateReleasesAction); 27 | 28 | expect(result).toEqual(updateReleasesAction.payload.releases); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /webapp/first_snap/content/flutter/test.yaml: -------------------------------------------------------------------------------- 1 | name: test-super-cool-app-{name} 2 | linux: 3 | - action: "Install the snap:" 4 | command: sudo snap install --dangerous *.snap 5 | - action: "List your installed snaps to confirm:" 6 | command: snap list 7 | - action: "Run the snap:" 8 | command: ${name} -h 9 | macos: 10 | - action: "Create a Linux virtual machine for testing snaps:" 11 | command: multipass launch -n testvm 12 | - action: "Return to the directory containing the .snap file and copy it into the virtual machine:" 13 | command: 'multipass copy-files ${name}*.snap testvm:' 14 | - action: "Connect to the virtual machine:" 15 | command: multipass shell testvm 16 | - action: "Install the snap inside the virtual machine:" 17 | command: sudo snap install --dangerous ${name}*.snap 18 | - action: "Confirm the snap is installed by listing your installed snaps:" 19 | command: snap list 20 | - action: "Run the snap inside the virtual machine:" 21 | command: ${name} -h 22 | - action: "Exit the virtual machine:" 23 | command: exit 24 | -------------------------------------------------------------------------------- /webapp/first_snap/content/c/test.yaml: -------------------------------------------------------------------------------- 1 | name: test-dosbox-{name} 2 | linux: 3 | - action: "Install the snap:" 4 | command: sudo snap install --devmode --dangerous *.snap 5 | - action: "List your installed snaps to confirm:" 6 | command: snap list 7 | - action: "Run the snap:" 8 | command: ${name} -h 9 | macos: 10 | - action: "Create a Linux virtual machine for testing snaps:" 11 | command: multipass launch -n testvm 12 | - action: "Return to the directory containing the .snap file and copy it into the virtual machine:" 13 | command: 'multipass copy-files ${name}*.snap testvm:' 14 | - action: "Connect to the virtual machine:" 15 | command: multipass shell testvm 16 | - action: "Install the snap inside the virtual machine:" 17 | command: sudo snap install --devmode --dangerous ${name}*.snap 18 | - action: "Confirm the snap is installed by listing your installed snaps:" 19 | command: snap list 20 | - action: "Run the snap inside the virtual machine:" 21 | command: ${name} -h 22 | - action: "Exit the virtual machine:" 23 | command: exit 24 | -------------------------------------------------------------------------------- /webapp/first_snap/content/golang/test.yaml: -------------------------------------------------------------------------------- 1 | name: test-httplab-{name} 2 | linux: 3 | - action: "Install the snap:" 4 | command: sudo snap install --devmode --dangerous *.snap 5 | - action: "List your installed snaps to confirm:" 6 | command: snap list 7 | - action: "Run the snap:" 8 | command: ${name} -h 9 | macos: 10 | - action: "Create a Linux virtual machine for testing snaps:" 11 | command: multipass launch -n testvm 12 | - action: "Return to the directory containing the .snap file and copy it into the virtual machine:" 13 | command: 'multipass copy-files ${name}*.snap testvm:' 14 | - action: "Connect to the virtual machine:" 15 | command: multipass shell testvm 16 | - action: "Install the snap inside the virtual machine:" 17 | command: sudo snap install --devmode --dangerous ${name}*.snap 18 | - action: "Confirm the snap is installed by listing your installed snaps:" 19 | command: snap list 20 | - action: "Run the snap inside the virtual machine:" 21 | command: ${name} 22 | - action: "Exit the virtual machine:" 23 | command: exit 24 | -------------------------------------------------------------------------------- /webapp/first_snap/content/node/test.yaml: -------------------------------------------------------------------------------- 1 | name: test-wethr-{name} 2 | linux: 3 | - action: "Install the snap:" 4 | command: sudo snap install --devmode --dangerous *.snap 5 | - action: "List your installed snaps to confirm:" 6 | command: snap list 7 | - action: "Run the snap:" 8 | command: ${name} -h 9 | macos: 10 | - action: "Create a Linux virtual machine for testing snaps:" 11 | command: multipass launch -n testvm 12 | - action: "Return to the directory containing the .snap file and copy it into the virtual machine:" 13 | command: 'multipass copy-files ${name}*.snap testvm:' 14 | - action: "Connect to the virtual machine:" 15 | command: multipass shell testvm 16 | - action: "Install the snap inside the virtual machine:" 17 | command: sudo snap install --devmode --dangerous ${name}*.snap 18 | - action: "Confirm the snap is installed by listing your installed snaps:" 19 | command: snap list 20 | - action: "Run the snap inside the virtual machine:" 21 | command: ${name} -h 22 | - action: "Exit the virtual machine:" 23 | command: exit 24 | -------------------------------------------------------------------------------- /webapp/first_snap/content/rust/test.yaml: -------------------------------------------------------------------------------- 1 | name: test-xsv-{name} 2 | linux: 3 | - action: "Install the snap:" 4 | command: sudo snap install --devmode --dangerous *.snap 5 | - action: "List your installed snaps to confirm:" 6 | command: snap list 7 | - action: "Run the snap:" 8 | command: ${name} -h 9 | macos: 10 | - action: "Create a Linux virtual machine for testing snaps:" 11 | command: multipass launch -n testvm 12 | - action: "Return to the directory containing the .snap file and copy it into the virtual machine:" 13 | command: 'multipass copy-files ${name}*.snap testvm:' 14 | - action: "Connect to the virtual machine:" 15 | command: multipass shell testvm 16 | - action: "Install the snap inside the virtual machine:" 17 | command: sudo snap install --devmode --dangerous ${name}*.snap 18 | - action: "Confirm the snap is installed by listing your installed snaps:" 19 | command: snap list 20 | - action: "Run the snap inside the virtual machine:" 21 | command: ${name} -h 22 | - action: "Exit the virtual machine:" 23 | command: exit 24 | -------------------------------------------------------------------------------- /webapp/first_snap/content/java/test.yaml: -------------------------------------------------------------------------------- 1 | name: test-freeplane-{name} 2 | linux: 3 | - action: "Install the snap:" 4 | command: sudo snap install --devmode --dangerous *.snap 5 | - action: "List your installed snaps to confirm:" 6 | command: snap list 7 | - action: "Run the snap:" 8 | command: ${name} -h 9 | macos: 10 | - action: "Create a Linux virtual machine for testing snaps:" 11 | command: multipass launch -n testvm 12 | - action: "Return to the directory containing the .snap file and copy it into the virtual machine:" 13 | command: 'multipass copy-files ${name}*.snap testvm:' 14 | - action: "Connect to the virtual machine:" 15 | command: multipass shell testvm 16 | - action: "Install the snap inside the virtual machine:" 17 | command: sudo snap install --devmode --dangerous ${name}*.snap 18 | - action: "Confirm the snap is installed by listing your installed snaps:" 19 | command: snap list 20 | - action: "Run the snap inside the virtual machine:" 21 | command: ${name} -h 22 | - action: "Exit the virtual machine:" 23 | command: exit 24 | -------------------------------------------------------------------------------- /templates/store/_generated-featured-snap-partial.html: -------------------------------------------------------------------------------- 1 | 2 | 15 | 28 | 29 | -------------------------------------------------------------------------------- /webapp/first_snap/content/python/test.yaml: -------------------------------------------------------------------------------- 1 | name: test-offlineimap-{name} 2 | linux: 3 | - action: "Install the snap:" 4 | command: sudo snap install --devmode --dangerous *.snap 5 | - action: "List your installed snaps to confirm:" 6 | command: snap list 7 | - action: "Run the snap:" 8 | command: ${name} -h 9 | macos: 10 | - action: "Create a Linux virtual machine for testing snaps:" 11 | command: multipass launch -n testvm 12 | - action: "Return to the directory containing the .snap file and copy it into the virtual machine:" 13 | command: 'multipass copy-files ${name}*.snap testvm:' 14 | - action: "Connect to the virtual machine:" 15 | command: multipass shell testvm 16 | - action: "Install the snap inside the virtual machine:" 17 | command: sudo snap install --devmode --dangerous ${name}*.snap 18 | - action: "Confirm the snap is installed by listing your installed snaps:" 19 | command: snap list 20 | - action: "Run the snap inside the virtual machine:" 21 | command: ${name} -h 22 | - action: "Exit the virtual machine:" 23 | command: exit 24 | -------------------------------------------------------------------------------- /static/js/publisher/release/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | 3 | import architectures from "./architectures"; 4 | import availableRevisionsSelect from "./availableRevisionsSelect"; 5 | import branches from "./branches"; 6 | import channelMap from "./channelMap"; 7 | import currentTrack from "./currentTrack"; 8 | import defaultTrack from "./defaultTrack"; 9 | import history from "./history"; 10 | import modal from "./modal"; 11 | import notification from "./globalNotification"; 12 | import options from "./options"; 13 | import pendingCloses from "./pendingCloses"; 14 | import pendingReleases from "./pendingReleases"; 15 | import releases from "./releases"; 16 | import revisions from "./revisions"; 17 | 18 | const releasesReducers = combineReducers({ 19 | architectures, 20 | availableRevisionsSelect, 21 | branches, 22 | channelMap, 23 | currentTrack, 24 | defaultTrack, 25 | history, 26 | modal, 27 | notification, 28 | options, 29 | pendingCloses, 30 | pendingReleases, 31 | revisions, 32 | releases, 33 | }); 34 | 35 | export default releasesReducers; 36 | -------------------------------------------------------------------------------- /webapp/store/content/distros/kubuntu.yaml: -------------------------------------------------------------------------------- 1 | name: Kubuntu 2 | color-1: "#0079C1" 3 | color-2: "#005A8F" 4 | logo: https://assets.ubuntu.com/v1/7ab50a06-Distro_Logo_Kubuntu.svg 5 | logo-mono: https://assets.ubuntu.com/v1/e9f102e3-Distro_Logo_Kubuntu_White.svg 6 | install: 7 | - 8 | action: | 9 | If you’re running Kubuntu 16.04 LTS (Xenial Xerus) or later, including Kubuntu 18.04 LTS (Bionic Beaver) and Kubuntu 18.10 (Cosmic Cuttlefish), you don’t need to do anything. Snap is already installed and ready to go. 10 | - 11 | action: | 12 | Versions of Kubuntu between 14.04 LTS (Trusty Tahr) and 15.10 (Wily Werewolf) don’t include snap by default, but snap can be installed from the command line as follows: 13 | command: | 14 | sudo apt update 15 | sudo apt install snapd 16 | -------------------------------------------------------------------------------- /templates/tutorials/index.html: -------------------------------------------------------------------------------- 1 | {% set page_slug="tutorials" %} 2 | 3 | {% extends "_layout.html" %} 4 | 5 | {% block meta_title %}Tutorials | Snapcraft{% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 |
11 |
12 |

Tutorials

13 |
14 |
15 |
16 | 17 |
18 |
19 | {% for item in tutorials %} 20 |
21 |
22 |

23 | {{ item.title | safe | truncate(50, true) }} 24 |

25 |

{{ item.summary | safe }}

26 |
27 |

Difficulty: {{ item.difficulty }} out of 5

28 |
29 | {% endfor %} 30 |
31 |
32 | 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /webapp/first_snap/content/moos/test.yaml: -------------------------------------------------------------------------------- 1 | name: test-moos-{name} 2 | linux: 3 | - action: "Install the snap:" 4 | command: sudo snap install --devmode --dangerous *.snap 5 | - action: "List your installed snaps to confirm:" 6 | command: snap list 7 | - action: "Run the snap:" 8 | command: test-moos-{name}.MOOSDB -h 9 | macos: 10 | - action: "Create a Linux virtual machine for testing snaps:" 11 | command: multipass launch -n testvm 12 | - action: "Return to the directory containing the .snap file and copy it into the virtual machine:" 13 | command: 'multipass copy-files ${name}*.snap testvm:' 14 | - action: "Connect to the virtual machine:" 15 | command: multipass shell testvm 16 | - action: "Install the snap inside the virtual machine:" 17 | command: sudo snap install --devmode --dangerous ${name}*.snap 18 | - action: "Confirm the snap is installed by listing your installed snaps:" 19 | command: snap list 20 | - action: "Run the snap inside the virtual machine:" 21 | command: ${name}.MOOSDB -h 22 | - action: "Exit the virtual machine:" 23 | command: exit 24 | -------------------------------------------------------------------------------- /static/js/publisher/release/reducers/branches.test.js: -------------------------------------------------------------------------------- 1 | import branches from "./branches"; 2 | import { OPEN_BRANCHES, CLOSE_BRANCHES } from "../actions/branches"; 3 | 4 | describe("branches", () => { 5 | it("should return an initial empty state", () => { 6 | expect(branches(undefined, {})).toEqual([]); 7 | }); 8 | 9 | describe("on OPEN_BRANCHES action", () => { 10 | it("should add branch to branches list", () => { 11 | const action = { 12 | type: OPEN_BRANCHES, 13 | payload: "test", 14 | }; 15 | 16 | const result = branches(undefined, action); 17 | 18 | expect(result).toEqual(["test"]); 19 | }); 20 | }); 21 | 22 | describe("on CLOSE_BRANCHES action", () => { 23 | it("should remove branch from branches list", () => { 24 | const action = { 25 | type: CLOSE_BRANCHES, 26 | payload: "test", 27 | }; 28 | 29 | const state = ["testing123", "test", "McTestFace", "testtest"]; 30 | 31 | const results = branches(state, action); 32 | 33 | expect(results).toEqual(["testing123", "McTestFace", "testtest"]); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /static/js/publisher/release/reducers/architectures.test.js: -------------------------------------------------------------------------------- 1 | import architectures from "./architectures"; 2 | import { UPDATE_ARCHITECTURES } from "../actions/architectures"; 3 | 4 | describe("architectures", () => { 5 | it("should return the initial state", () => { 6 | expect(architectures(undefined, {})).toEqual([]); 7 | }); 8 | 9 | describe("on UPDATE_ARCHITECTURES action", () => { 10 | let updateArchitecturesAction = { 11 | type: UPDATE_ARCHITECTURES, 12 | payload: { 13 | architectures: ["amd64", "armhf", "test", "test2"], 14 | }, 15 | }; 16 | 17 | it("should add architectures to state", () => { 18 | const result = architectures({}, updateArchitecturesAction); 19 | 20 | expect(result).toEqual(updateArchitecturesAction.payload.architectures); 21 | }); 22 | 23 | it("should replace existing architectures in state", () => { 24 | const initialState = ["testing"]; 25 | 26 | const result = architectures(initialState, updateArchitecturesAction); 27 | 28 | expect(result).toEqual(updateArchitecturesAction.payload.architectures); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /static/js/publisher/release/components/releasesConfirmActions.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const ReleasesConfirmActions = ({ 5 | isCancelEnabled, 6 | cancelPendingReleases, 7 | isApplyEnabled, 8 | applyPendingReleases, 9 | isLoading, 10 | }) => ( 11 |
12 | 19 | 26 |
27 | ); 28 | 29 | ReleasesConfirmActions.propTypes = { 30 | isCancelEnabled: PropTypes.bool, 31 | cancelPendingReleases: PropTypes.func, 32 | isApplyEnabled: PropTypes.bool, 33 | applyPendingReleases: PropTypes.func, 34 | isLoading: PropTypes.bool, 35 | }; 36 | 37 | export default ReleasesConfirmActions; 38 | -------------------------------------------------------------------------------- /static/sass/_snapcraft_p-sticky-admin-footer.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-sticky-admin-footer { 2 | %sticky-admin-footer { 3 | background-color: $color-x-light; 4 | bottom: 0; 5 | box-shadow: 0 1px 7px rgba(0, 0, 0, 0.3); 6 | left: 0; 7 | padding: 0.75rem 1rem; 8 | position: fixed; 9 | right: 0; 10 | 11 | @media (min-width: $breakpoint-small) { 12 | left: 4rem; 13 | } 14 | 15 | @media (min-width: $breakpoint-large) { 16 | left: 15rem; 17 | } 18 | } 19 | 20 | .p-sticky-admin-footer { 21 | @extend %sticky-admin-footer; 22 | 23 | transform: translateY(0); 24 | } 25 | 26 | .p-sticky-admin-footer--hidden { 27 | @extend %sticky-admin-footer; 28 | 29 | transform: translateY(100%); 30 | } 31 | 32 | .p-sticky-admin-footer__inner { 33 | button { 34 | margin-bottom: $sp-small; 35 | 36 | @media (min-width: $breakpoint-x-small) { 37 | margin-bottom: 0; 38 | } 39 | } 40 | 41 | @media (min-width: $breakpoint-x-small) { 42 | align-items: center; 43 | display: flex; 44 | justify-content: space-between; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /static/js/public/scroll-to.js: -------------------------------------------------------------------------------- 1 | export function animateScrollTo(to, offset = 0) { 2 | const element = document.scrollingElement || window; 3 | 4 | if (typeof to === "string") { 5 | to = document.querySelector(to); 6 | if (!to) { 7 | throw Error(`Can't find any element for "${to}" in animateScrollTo.`); 8 | } 9 | } 10 | if (typeof to !== "number") { 11 | to = to.getBoundingClientRect().top + element.scrollTop; 12 | } 13 | to = to - offset; 14 | 15 | if (element.scrollTo) { 16 | element.scrollTo({ top: to, left: 0, behavior: "smooth" }); 17 | } else { 18 | element.scrollTop = to; 19 | } 20 | } 21 | 22 | export function initLinkScroll(link, { offset = 0 }) { 23 | if (link && (link.dataset.scrollTo || link.href)) { 24 | const href = link.dataset.scrollTo || link.getAttribute("href"); 25 | const target = document.querySelector(href); 26 | if (target) { 27 | link.addEventListener("click", (event) => { 28 | event.preventDefault(); 29 | animateScrollTo(target, offset); 30 | setTimeout(() => window.history.pushState({}, null, href), 100); 31 | }); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /static/sass/_button-overrides.scss: -------------------------------------------------------------------------------- 1 | @mixin button-overrides { 2 | .p-button--outline { 3 | @extend %vf-button-base; 4 | 5 | border-color: $color-x-light; 6 | color: $color-x-light; 7 | 8 | &:visited { 9 | color: $color-x-light; 10 | } 11 | 12 | &:hover, 13 | &:active:hover { 14 | background-color: rgba($color-mid-x-light, 0.2); 15 | } 16 | } 17 | 18 | .is-inline--right { 19 | margin-right: $sp-medium; 20 | } 21 | 22 | .p-icon--github { 23 | @extend %icon; 24 | 25 | background-image: url("#{$assets-path}61d83c7e-icon-github.svg"); 26 | } 27 | 28 | // propose to vanilla when two buttons are next to each other 29 | [class^="p-button"] + [class^="p-button"]:not(span) { 30 | margin-right: $sph--large; 31 | } 32 | 33 | // new button style for dark strip 34 | // TODO: propose as new pattern (or amendment?) to vanilla 35 | .p-strip--accent { 36 | .p-button--base { 37 | border-color: $color-x-light; 38 | color: $color-x-light; 39 | margin-top: 0; 40 | 41 | &:hover { 42 | background-color: transparentize($color-x-light, 0.8); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /webapp/api/exceptions.py: -------------------------------------------------------------------------------- 1 | class ApiError(Exception): 2 | """ 3 | Base exception for any errors in the API layer 4 | """ 5 | 6 | pass 7 | 8 | 9 | class ApiConnectionError(ApiError): 10 | """ 11 | Communication with the API failed 12 | """ 13 | 14 | pass 15 | 16 | 17 | class ApiTimeoutError(ApiError): 18 | """ 19 | Communication with the API timed out 20 | """ 21 | 22 | pass 23 | 24 | 25 | class ApiResponseDecodeError(ApiError): 26 | """ 27 | We failed to properly decode the response from the API 28 | """ 29 | 30 | pass 31 | 32 | 33 | class ApiResponseError(ApiError): 34 | """ 35 | The API responded with an error 36 | """ 37 | 38 | def __init__(self, message, status_code): 39 | self.status_code = status_code 40 | return super().__init__(message) 41 | 42 | 43 | class ApiResponseErrorList(ApiResponseError): 44 | """ 45 | The API responded with a list of errors, 46 | which are included in self.errors 47 | """ 48 | 49 | def __init__(self, message, status_code, errors): 50 | self.errors = errors 51 | return super().__init__(message, status_code) 52 | -------------------------------------------------------------------------------- /webapp/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class ConfigurationError(Exception): 5 | pass 6 | 7 | 8 | SECRET_KEY = os.getenv("SECRET_KEY") 9 | LOGIN_URL = os.getenv("LOGIN_URL", "https://login.ubuntu.com") 10 | BSI_URL = os.getenv("BSI_URL", "https://build.snapcraft.io") 11 | ENVIRONMENT = os.getenv("ENVIRONMENT", "devel") 12 | COMMIT_ID = os.getenv("COMMIT_ID", "commit_id") 13 | SENTRY_DSN = os.getenv("SENTRY_DSN", "").strip() 14 | SENTRY_CONFIG = {"release": COMMIT_ID, "environment": ENVIRONMENT} 15 | 16 | if ENVIRONMENT != "devel": 17 | SESSION_COOKIE_SAMESITE = "None" 18 | SESSION_COOKIE_SECURE = True 19 | 20 | WEBAPP = os.getenv("WEBAPP", "snapcraft") 21 | 22 | if not WEBAPP: 23 | raise ConfigurationError("`WEBAPP` is not configured") 24 | 25 | WEBAPP_EXTRA_HEADERS = {} 26 | 27 | # Ten years default cache time on static files 28 | SEND_FILE_MAX_AGE_DEFAULT = 10 * 365 * 24 * 60 * 60 29 | 30 | CONTENT_DIRECTORY = {"PUBLISHER_PAGES": "store/content/publishers/"} 31 | 32 | # Docs search 33 | SEARCH_API_KEY = os.getenv("SEARCH_API_KEY") 34 | SEARCH_API_URL = "https://www.googleapis.com/customsearch/v1" 35 | SEARCH_CUSTOM_ID = "009048213575199080868:i3zoqdwqk8o" 36 | -------------------------------------------------------------------------------- /templates/partials/search-bar.html: -------------------------------------------------------------------------------- 1 |
7 |
8 | {% if not IS_BRAND_STORE %} 9 | {% include 'partials/livestream-notification.html' %} 10 |

Search thousands of snaps used by millions of people across 41 Linux distributions

11 | {% endif %} 12 | {% if webapp_config['STORE_SEARCH_INTRO'] %} 13 | {{ webapp_config['STORE_SEARCH_INTRO']|safe }} 14 | {% endif %} 15 | 24 |
25 |
26 | -------------------------------------------------------------------------------- /static/js/base/navigation.js: -------------------------------------------------------------------------------- 1 | // Login 2 | var navAccountContainer = document.querySelector(".js-nav-account"); 3 | 4 | if (navAccountContainer) { 5 | fetch("/account.json") 6 | .then((response) => response.json()) 7 | .then((data) => { 8 | var notAuthenticatedMenu = navAccountContainer.querySelector( 9 | ".js-nav-account--notauthenticated" 10 | ); 11 | var authenticatedMenu = navAccountContainer.querySelector( 12 | ".js-nav-account--authenticated" 13 | ); 14 | 15 | if (data.publisher) { 16 | var displayName = navAccountContainer.querySelector( 17 | ".js-account--name" 18 | ); 19 | 20 | notAuthenticatedMenu.classList.add("u-hide"); 21 | authenticatedMenu.classList.remove("u-hide"); 22 | displayName.innerHTML = data.publisher["fullname"]; 23 | 24 | if (data.publisher.stores) { 25 | authenticatedMenu 26 | .querySelector(".js-nav-account--stores") 27 | .classList.remove("u-hide"); 28 | } 29 | } else { 30 | notAuthenticatedMenu.classList.remove("u-hide"); 31 | authenticatedMenu.classList.add("u-hide"); 32 | } 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /static/js/publisher/release/components/releasesTable/revisionCell.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import { DND_ITEM_REVISIONS } from "../dnd"; 5 | 6 | import { canBeReleased } from "../../helpers"; 7 | import { ReleasesTableCellView, RevisionInfo, EmptyInfo } from "./cellViews"; 8 | 9 | // releases table cell with data for a specific revision (unrelated to channel map) 10 | const ReleasesTableRevisionCell = (props) => { 11 | const { revision, arch } = props; 12 | 13 | const item = { 14 | revisions: [revision], 15 | architectures: revision ? revision.architectures : [], 16 | type: DND_ITEM_REVISIONS, 17 | }; 18 | 19 | return ( 20 | 26 | {revision ? : } 27 | 28 | ); 29 | }; 30 | 31 | ReleasesTableRevisionCell.propTypes = { 32 | revision: PropTypes.object, 33 | showVersion: PropTypes.bool, 34 | arch: PropTypes.string, 35 | }; 36 | 37 | export default ReleasesTableRevisionCell; 38 | -------------------------------------------------------------------------------- /static/sass/_patterns_distro_banner.scss: -------------------------------------------------------------------------------- 1 | @mixin p-distro-banner { 2 | $color-transparent-white: rgba(255, 255, 255, 0); 3 | 4 | .distro-banner { 5 | position: relative; 6 | } 7 | 8 | .distro-banner > * { 9 | // make sure child elements render on top of absolutely positioned background 10 | position: relative; 11 | } 12 | 13 | .distro-banner .p-card--highlighted { 14 | padding: 3rem; 15 | } 16 | 17 | .distro-banner .is-light { 18 | color: $color-light; 19 | } 20 | 21 | .distro-banner__background { 22 | min-height: 700px; 23 | overflow: hidden; 24 | position: absolute; 25 | top: 0; 26 | width: 100%; 27 | z-index: 0; 28 | } 29 | 30 | .distro-banner__suru { 31 | bottom: 0; 32 | min-width: 1440px; 33 | position: absolute; 34 | width: 100%; 35 | } 36 | 37 | .details-block { 38 | margin-bottom: 2rem; 39 | } 40 | 41 | .distro-banner__logo { 42 | float: right; 43 | width: 7rem; 44 | } 45 | 46 | @media only screen and (min-width: $breakpoint-small) { 47 | .distro-code-snippet { 48 | // to align code snippet with paragraph of text in column next to it 49 | padding-top: 0.5rem; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /static/sass/_snapcraft_distro-chart.scss: -------------------------------------------------------------------------------- 1 | @mixin snapcraft-distro-chart { 2 | $bar-color: #084081; 3 | $color-transparent-white: rgba(255, 255, 255, 0); 4 | 5 | .snapcraft-distro-chart { 6 | &__names, 7 | &__bars { 8 | display: block; 9 | float: left; 10 | min-height: 1px; 11 | position: relative; 12 | } 13 | 14 | &__names { 15 | width: 8rem; 16 | } 17 | 18 | &__bars { 19 | margin-left: 1rem; 20 | margin-top: 0; 21 | width: calc(100% - 9rem); 22 | } 23 | 24 | &__name { 25 | font-size: 0.75rem; 26 | line-height: 1rem; 27 | margin-top: 1px; 28 | overflow: hidden; 29 | text-align: right; 30 | text-overflow: ellipsis; 31 | white-space: nowrap; 32 | } 33 | 34 | &__bar { 35 | background: $bar-color; 36 | clear: left; 37 | float: left; 38 | height: 1px; 39 | margin: 0.5313rem 0 0.4688rem; 40 | } 41 | 42 | &__more { 43 | background: linear-gradient($color-transparent-white, $color-x-light 80%); 44 | bottom: 0; 45 | height: 68px; 46 | padding-top: 3rem; 47 | position: absolute; 48 | width: 100%; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /static/js/libs/iframeSize.js: -------------------------------------------------------------------------------- 1 | import debounce from "./debounce"; 2 | 3 | /** 4 | * 5 | * @param wrapperSelector A query selector for the wrapping element 6 | * This element is used to define how wide the iframe 7 | * should be. 8 | * It's also used to find the iframe element. 9 | * @param maxWidth The maximum width the iframe should go. 10 | */ 11 | function sizeIframe(wrapperSelector) { 12 | const wrapperEl = document.querySelector(wrapperSelector); 13 | if (!wrapperEl) { 14 | return; 15 | } 16 | 17 | const iframe = wrapperEl.querySelector("iframe"); 18 | 19 | // asciinema is a snowflake, so treat it as such 20 | if (!iframe || (iframe.name && iframe.name.indexOf("asciicast") !== -1)) { 21 | return; 22 | } 23 | 24 | const IFRAME_RATIO = iframe.width / iframe.height; 25 | 26 | const width = wrapperEl.clientWidth; 27 | 28 | iframe.width = width; 29 | iframe.height = width / IFRAME_RATIO; 30 | } 31 | 32 | export default (wrapperSelector) => { 33 | window.addEventListener( 34 | "resize", 35 | debounce(sizeIframe.bind(this, wrapperSelector), 100) 36 | ); 37 | 38 | sizeIframe(wrapperSelector); 39 | }; 40 | -------------------------------------------------------------------------------- /static/js/publisher/builds/components/select.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, fireEvent } from "@testing-library/react"; 3 | 4 | import Select from "./select"; 5 | 6 | describe("Select", () => { 7 | const essentialProps = { 8 | options: [{ value: "test-1" }, { value: "test-2" }], 9 | updateSelection: jest.fn(), 10 | selectedOption: "test-1", 11 | }; 12 | 13 | it("should render all the essential components", () => { 14 | const { container } = render(); 21 | const selectEl = container.querySelector("select"); 22 | 23 | expect(selectEl.childNodes.length).toEqual(2); 24 | }); 25 | 26 | it("should call the callback function on change", () => { 27 | const { container } = render(