├── .github ├── CODEOWNERS ├── pull_request_template.md └── workflows │ ├── ci.yml │ └── trigger-deploy.yml ├── .gitignore ├── .yarnrc.yml ├── LICENSE ├── Makefile ├── README.md ├── cmd └── hermes │ └── main.go ├── configs └── config.hcl ├── docker-compose.yml ├── go.mod ├── go.sum ├── internal ├── api │ ├── analytics.go │ ├── approvals.go │ ├── document_types.go │ ├── documents.go │ ├── documents_related_resources.go │ ├── documents_test.go │ ├── drafts.go │ ├── drafts_shareable.go │ ├── helpers.go │ ├── helpers_test.go │ ├── me.go │ ├── me_recently_viewed_docs.go │ ├── me_subscriptions.go │ ├── people.go │ ├── products.go │ ├── reviews.go │ └── v2 │ │ ├── analytics.go │ │ ├── approvals.go │ │ ├── document_types.go │ │ ├── documents.go │ │ ├── documents_related_resources.go │ │ ├── documents_test.go │ │ ├── drafts.go │ │ ├── drafts_shareable.go │ │ ├── groups.go │ │ ├── helpers.go │ │ ├── helpers_test.go │ │ ├── jira_issue.go │ │ ├── jira_issue_picker.go │ │ ├── me.go │ │ ├── me_recently_viewed_docs.go │ │ ├── me_recently_viewed_projects.go │ │ ├── me_subscriptions.go │ │ ├── people.go │ │ ├── products.go │ │ ├── projects.go │ │ ├── projects_related_resources.go │ │ └── reviews.go ├── auth │ ├── auth.go │ ├── google │ │ └── google.go │ └── oktaalb │ │ ├── doc.go │ │ └── oktaalb.go ├── cmd │ ├── base │ │ └── base.go │ ├── commands.go │ ├── commands │ │ ├── indexer │ │ │ └── indexer.go │ │ ├── operator │ │ │ ├── migrate_algolia_to_postgresql.go │ │ │ └── operator.go │ │ ├── server │ │ │ └── server.go │ │ └── version │ │ │ ├── version.go │ │ │ └── version_test.go │ └── main.go ├── config │ ├── config.go │ └── helpers.go ├── datadog │ ├── datadog.go │ └── doc.go ├── db │ └── db.go ├── email │ ├── email.go │ └── templates │ │ ├── document-approved.html │ │ ├── new-owner.html │ │ ├── review-requested.html │ │ └── subscriber-document-published.html ├── helpers │ ├── helpers.go │ └── helpers_test.go ├── indexer │ ├── indexer.go │ ├── refresh_docs_headers.go │ ├── refresh_drafts_headers.go │ └── refresh_headers.go ├── jira │ ├── api_responses.go │ ├── doc.go │ └── service.go ├── pkg │ ├── doctypes │ │ ├── doc.go │ │ └── doc_types.go │ └── featureflags │ │ └── flags.go ├── pub │ ├── assets │ │ ├── document.png │ │ └── hermes-logo.png │ └── pub.go ├── server │ └── server.go ├── structs │ └── product.go ├── test │ └── database.go └── version │ └── version.go ├── pkg ├── algolia │ ├── client.go │ ├── doc.go │ └── proxy.go ├── document │ ├── doc.go │ ├── document.go │ └── replace_header.go ├── googleworkspace │ ├── backoff.go │ ├── doc.go │ ├── docs_helpers.go │ ├── drive_helpers.go │ ├── gmail_helpers.go │ ├── oauth2_helpers.go │ ├── people_helpers.go │ └── service.go ├── hashicorpdocs │ ├── basedoc.go │ ├── common.go │ ├── doc.go │ ├── frd.go │ ├── frd_replace_header.go │ ├── locked.go │ ├── prd.go │ ├── prd_replace_header.go │ ├── rfc.go │ ├── rfc_replace_header.go │ └── rfc_test.go ├── links │ ├── data.go │ └── redirect.go └── models │ ├── document.go │ ├── document_custom_field.go │ ├── document_custom_field_test.go │ ├── document_file_revision.go │ ├── document_file_revision_test.go │ ├── document_group_review.go │ ├── document_group_review_test.go │ ├── document_related_resource.go │ ├── document_related_resource_external_link.go │ ├── document_related_resource_hermes_document.go │ ├── document_related_resource_test.go │ ├── document_review.go │ ├── document_review_test.go │ ├── document_test.go │ ├── document_type.go │ ├── document_type_custom_field.go │ ├── document_type_custom_field_test.go │ ├── document_type_test.go │ ├── gorm.go │ ├── group.go │ ├── group_test.go │ ├── indexer_folder.go │ ├── indexer_folder_test.go │ ├── indexer_metadata.go │ ├── indexer_metadata_test.go │ ├── product.go │ ├── product_latest_document_number.go │ ├── product_latest_document_number_test.go │ ├── product_test.go │ ├── project.go │ ├── project_related_resource.go │ ├── project_related_resource_external_link.go │ ├── project_related_resource_hermes_document.go │ ├── project_related_resource_test.go │ ├── project_test.go │ ├── testing.go │ ├── user.go │ └── user_test.go └── web ├── .eslintrc.js ├── .prettierrc.js ├── app ├── adapters │ ├── application.ts │ ├── group.ts │ ├── jira-issue.ts │ └── person.ts ├── app.ts ├── authenticators │ └── torii.ts ├── components │ ├── .gitkeep │ ├── action.gts │ ├── application-loading │ │ ├── index.hbs │ │ └── index.js │ ├── copy-u-r-l-button.hbs │ ├── copy-u-r-l-button.ts │ ├── custom-editable-field.hbs │ ├── custom-editable-field.ts │ ├── dashboard │ │ ├── docs-awaiting-review.hbs │ │ ├── docs-awaiting-review.ts │ │ ├── docs-awaiting-review │ │ │ ├── doc.hbs │ │ │ └── doc.ts │ │ ├── index.hbs │ │ ├── index.ts │ │ ├── latest-docs.hbs │ │ ├── latest-docs.ts │ │ ├── new-features-banner.hbs │ │ ├── new-features-banner.ts │ │ ├── recently-viewed.hbs │ │ ├── recently-viewed.ts │ │ └── recently-viewed │ │ │ ├── item.hbs │ │ │ └── item.ts │ ├── doc │ │ ├── folder-affordance.hbs │ │ ├── folder-affordance.ts │ │ ├── snippet.hbs │ │ ├── snippet.ts │ │ ├── state-progress-bar.hbs │ │ ├── status.hbs │ │ ├── status.ts │ │ ├── thumbnail.hbs │ │ ├── thumbnail.ts │ │ ├── tile-medium.hbs │ │ └── tile-medium.ts │ ├── document │ │ ├── index.hbs │ │ ├── index.ts │ │ ├── modal.hbs │ │ ├── modal.ts │ │ ├── sidebar.hbs │ │ ├── sidebar.ts │ │ ├── sidebar │ │ │ ├── empty-state-add-button.gts │ │ │ ├── header.hbs │ │ │ ├── header.ts │ │ │ ├── related-resources.hbs │ │ │ ├── related-resources.ts │ │ │ ├── related-resources │ │ │ │ ├── list-item.hbs │ │ │ │ ├── list-item.ts │ │ │ │ ├── list-item │ │ │ │ │ ├── resource.hbs │ │ │ │ │ └── resource.ts │ │ │ │ ├── list.hbs │ │ │ │ └── list.ts │ │ │ ├── section-header.hbs │ │ │ └── section-header.ts │ │ ├── status-icon.hbs │ │ └── status-icon.ts │ ├── documents │ │ ├── table.hbs │ │ └── table.ts │ ├── editable-field.hbs │ ├── editable-field.ts │ ├── editable-field │ │ ├── read-value.hbs │ │ ├── read-value.ts │ │ └── read-value │ │ │ └── person.gts │ ├── empty-state-text.gts │ ├── external-link.hbs │ ├── external-link.ts │ ├── favicon.hbs │ ├── favicon.ts │ ├── floating-u-i │ │ ├── content.hbs │ │ ├── content.ts │ │ ├── index.hbs │ │ └── index.ts │ ├── footer.hbs │ ├── footer.ts │ ├── header.hbs │ ├── header.ts │ ├── header │ │ ├── active-filter-list-item.hbs │ │ ├── active-filter-list-item.ts │ │ ├── active-filter-list.hbs │ │ ├── active-filter-list.ts │ │ ├── facet-dropdown.hbs │ │ ├── facet-dropdown.ts │ │ ├── nav.hbs │ │ ├── nav.ts │ │ ├── search.hbs │ │ ├── search.ts │ │ ├── sort-dropdown.hbs │ │ ├── sort-dropdown.ts │ │ ├── toolbar.hbs │ │ ├── toolbar.ts │ │ ├── user-menu-highlight.hbs │ │ └── user-menu-highlight.ts │ ├── hermes-logo.hbs │ ├── hermes-logo.ts │ ├── inputs │ │ ├── people-select.hbs │ │ ├── people-select.ts │ │ ├── product-select.hbs │ │ ├── product-select.ts │ │ ├── product-select │ │ │ ├── item.hbs │ │ │ └── item.ts │ │ ├── tag-select.hbs │ │ └── tag-select.js │ ├── match-count-headline.gts │ ├── modal-alert-error.hbs │ ├── modal-alert-error.ts │ ├── modals.gts │ ├── modals │ │ ├── doc-transferred.hbs │ │ ├── doc-transferred.ts │ │ ├── draft-created.hbs │ │ └── draft-created.ts │ ├── multiselect │ │ ├── tag-chip.hbs │ │ ├── user-email-image-chip.hbs │ │ └── user-email-image-chip.ts │ ├── my │ │ ├── docs.hbs │ │ ├── docs.ts │ │ └── header.gts │ ├── new │ │ ├── doc-form.hbs │ │ ├── doc-form.ts │ │ ├── document-template-list.hbs │ │ ├── document-template-list.ts │ │ ├── form.hbs │ │ ├── form.ts │ │ ├── project-form.hbs │ │ └── project-form.ts │ ├── notification.hbs │ ├── notification.ts │ ├── overflow-menu.hbs │ ├── overflow-menu.ts │ ├── pagination.hbs │ ├── pagination.ts │ ├── pagination │ │ ├── link.hbs │ │ └── link.ts │ ├── person │ │ ├── avatar.hbs │ │ ├── avatar.ts │ │ ├── index.hbs │ │ └── index.ts │ ├── product-area │ │ ├── index.hbs │ │ └── index.ts │ ├── product-link.hbs │ ├── product-link.ts │ ├── product │ │ ├── avatar.gts │ │ └── subscription-toggle.gts │ ├── project │ │ ├── index.hbs │ │ ├── index.ts │ │ ├── jira-widget.hbs │ │ ├── jira-widget.ts │ │ ├── resource-empty-state.gts │ │ ├── resource-list-item.hbs │ │ ├── resource-list-item.ts │ │ ├── resource-list.hbs │ │ ├── resource-list.ts │ │ ├── resource.hbs │ │ ├── resource.ts │ │ ├── status-icon.gts │ │ ├── tile.hbs │ │ └── tile.ts │ ├── projects │ │ ├── add-to-or-create.hbs │ │ ├── add-to-or-create.ts │ │ ├── index.hbs │ │ └── index.ts │ ├── related-resource │ │ ├── external-link.gts │ │ └── hermes-document.gts │ ├── related-resources.hbs │ ├── related-resources.ts │ ├── related-resources │ │ ├── add-or-edit-external-resource-modal.hbs │ │ ├── add-or-edit-external-resource-modal.ts │ │ ├── add.hbs │ │ ├── add.ts │ │ └── add │ │ │ ├── document.hbs │ │ │ ├── document.ts │ │ │ ├── fallback-external-resource.hbs │ │ │ └── fallback-external-resource.ts │ ├── results │ │ ├── index.hbs │ │ ├── index.ts │ │ ├── nav.hbs │ │ ├── nav.ts │ │ ├── section-header.gts │ │ └── section-header.hbs │ ├── settings │ │ ├── subscription-list-item.hbs │ │ ├── subscription-list-item.ts │ │ ├── subscription-list.hbs │ │ └── subscription-list.ts │ ├── table │ │ ├── row.hbs │ │ ├── row.ts │ │ ├── sortable-header.hbs │ │ └── sortable-header.ts │ ├── tooltip-icon.hbs │ ├── tooltip-icon.ts │ ├── truncated-text.hbs │ ├── truncated-text.ts │ ├── type-to-confirm.hbs │ ├── type-to-confirm.ts │ ├── type-to-confirm │ │ ├── input.hbs │ │ └── input.ts │ ├── whats-a-project.hbs │ ├── whats-a-project.ts │ └── x │ │ └── dropdown-list │ │ ├── _shared.ts │ │ ├── action.hbs │ │ ├── action.ts │ │ ├── checkable-item.hbs │ │ ├── checkable-item.ts │ │ ├── external-link.hbs │ │ ├── external-link.ts │ │ ├── index.hbs │ │ ├── index.ts │ │ ├── item.hbs │ │ ├── item.ts │ │ ├── items.hbs │ │ ├── items.ts │ │ ├── link-to.hbs │ │ ├── link-to.ts │ │ ├── toggle-action.hbs │ │ ├── toggle-action.ts │ │ ├── toggle-button.hbs │ │ ├── toggle-button.ts │ │ ├── toggle-select.hbs │ │ └── toggle-select.ts ├── config │ └── environment.d.ts ├── controllers │ ├── .gitkeep │ ├── 404.ts │ ├── application.ts │ ├── authenticate.ts │ ├── authenticated.ts │ └── authenticated │ │ ├── all.ts │ │ ├── dashboard.ts │ │ ├── document.ts │ │ ├── documents.ts │ │ ├── my │ │ └── documents.ts │ │ ├── new │ │ └── doc.ts │ │ ├── product-areas │ │ └── product-area.ts │ │ ├── projects │ │ ├── index.ts │ │ └── project.ts │ │ └── results.ts ├── helpers │ ├── .gitkeep │ ├── _dasherize.ts │ ├── _lowercase.ts │ ├── add.ts │ ├── dasherize.js │ ├── get-facet-label.ts │ ├── get-facet-query-hash.ts │ ├── get-model-attr.ts │ ├── get-owner-query.ts │ ├── get-product-id.ts │ ├── get-product-label.ts │ ├── highlight-text.ts │ ├── html-element.ts │ ├── is-active-filter.ts │ ├── lowercase.js │ ├── maybe-query.ts │ ├── model-or-models.ts │ ├── parse-date.ts │ ├── time-ago.ts │ └── uid.js ├── index.html ├── initializers │ ├── custom-inflector-rules.ts │ └── initialize-torii.js ├── metrics-adapters │ ├── _google-analytics-four.ts │ └── google-analytics-four.js ├── models │ ├── .gitkeep │ ├── group.ts │ ├── jira-issue.ts │ ├── me.ts │ └── person.ts ├── modifiers │ ├── auto-height-textarea.ts │ ├── autofocus.ts │ ├── dismissible.ts │ ├── select-on-focus.ts │ └── tooltip.ts ├── router.js ├── routes │ ├── .gitkeep │ ├── 404.ts │ ├── application.ts │ ├── authenticate.ts │ ├── authenticated.ts │ └── authenticated │ │ ├── all.ts │ │ ├── dashboard.ts │ │ ├── document.ts │ │ ├── documents.ts │ │ ├── drafts.ts │ │ ├── index.js │ │ ├── my.ts │ │ ├── my │ │ └── documents.ts │ │ ├── new.ts │ │ ├── new │ │ ├── doc.ts │ │ ├── index.ts │ │ └── project.ts │ │ ├── product-areas.ts │ │ ├── product-areas │ │ └── product-area.ts │ │ ├── projects.ts │ │ ├── projects │ │ ├── index.ts │ │ └── project.ts │ │ ├── results.ts │ │ └── settings.ts ├── serializers │ ├── application.ts │ ├── group.ts │ ├── jira-issue.ts │ ├── me.ts │ └── person.ts ├── services │ ├── _metrics.ts │ ├── _session.ts │ ├── _store.ts │ ├── active-filters.ts │ ├── algolia.ts │ ├── authenticated-user.ts │ ├── config.ts │ ├── document-types.ts │ ├── fetch.ts │ ├── flags.ts │ ├── flash-messages.d.ts │ ├── latest-docs.ts │ ├── metrics.js │ ├── modal-alerts.ts │ ├── product-areas.ts │ ├── recently-viewed.ts │ ├── session.d.ts │ ├── session.js │ ├── store.d.ts │ ├── store.js │ └── viewport.ts ├── styles │ ├── animations.scss │ ├── app.scss │ ├── body.scss │ ├── buttons.scss │ ├── components │ │ ├── action.scss │ │ ├── dashboard.scss │ │ ├── doc │ │ │ ├── folder-affordance.scss │ │ │ ├── status.scss │ │ │ ├── thumbnail.scss │ │ │ └── tile.scss │ │ ├── document │ │ │ └── related-resources.scss │ │ ├── editable-field.scss │ │ ├── floating-u-i │ │ │ └── content.scss │ │ ├── footer.scss │ │ ├── hds-badge.scss │ │ ├── hds-table.scss │ │ ├── header │ │ │ ├── active-filter-list-item.scss │ │ │ ├── active-filter-list.scss │ │ │ ├── facet-dropdown.scss │ │ │ └── search.scss │ │ ├── interactive-card.scss │ │ ├── jira-widget.scss │ │ ├── modal-dialog.scss │ │ ├── multiselect.scss │ │ ├── nav.scss │ │ ├── new.scss │ │ ├── notification.scss │ │ ├── overflow-menu.scss │ │ ├── page.scss │ │ ├── person.scss │ │ ├── popover.scss │ │ ├── preview-card.scss │ │ ├── product-link.scss │ │ ├── project.scss │ │ ├── projects │ │ │ └── tile.scss │ │ ├── segmented-control.scss │ │ ├── sidebar.scss │ │ ├── table │ │ │ └── sortable-header.scss │ │ ├── toolbar.scss │ │ ├── x-tooltip.scss │ │ └── x │ │ │ └── dropdown │ │ │ ├── list-item.scss │ │ │ ├── list.scss │ │ │ └── toggle-select.scss │ ├── ember-power-select-theme.scss │ ├── error-404.scss │ ├── hashicorp │ │ ├── hermes-logo.scss │ │ └── product-badge.scss │ ├── hds-overrides.scss │ ├── hermes │ │ ├── avatar.scss │ │ └── variables │ ├── routes │ │ ├── my.scss │ │ └── results.scss │ ├── typography.scss │ └── variables.scss ├── templates │ ├── 404.hbs │ ├── application-loading.hbs │ ├── application.hbs │ ├── authenticate.hbs │ ├── authenticated.hbs │ └── authenticated │ │ ├── dashboard.hbs │ │ ├── document.hbs │ │ ├── documents.hbs │ │ ├── my.hbs │ │ ├── my │ │ └── documents.hbs │ │ ├── new.hbs │ │ ├── new │ │ ├── doc.hbs │ │ ├── index.hbs │ │ └── project.hbs │ │ ├── product-areas.hbs │ │ ├── product-areas │ │ └── product-area.hbs │ │ ├── projects.hbs │ │ ├── projects │ │ ├── index.hbs │ │ └── project.hbs │ │ ├── results.hbs │ │ └── settings.hbs ├── torii-providers │ └── google-oauth2-bearer.js ├── types │ ├── document-routes.ts │ ├── document-type.d.ts │ ├── document.d.ts │ ├── facets.d.ts │ ├── project-status.ts │ ├── project.d.ts │ ├── route-models.ts │ └── sizes.ts └── utils │ ├── blink-element.ts │ ├── clean-string.ts │ ├── create-draft-url-search-params.ts │ ├── ember-animated │ ├── animate-transform.ts │ ├── easings.ts │ ├── empty-transition.ts │ └── highlight-element.ts │ ├── ember-cli-flash │ └── timeouts.ts │ ├── facets.js │ ├── get-model-attr.ts │ ├── get-product-id.ts │ ├── get-product-label.ts │ ├── hermes-urls.ts │ ├── html-element.ts │ ├── is-valid-u-r-l.ts │ ├── mockdate │ └── dates.ts │ ├── parse-date.ts │ ├── scroll-into-view-if-needed.ts │ ├── simple-timeout.ts │ ├── time-ago.ts │ ├── tooltip-text.ts │ └── update-related-resources-sort-order.ts ├── config ├── deprecation-workflow.js ├── ember-cli-update.json ├── environment.js ├── optional-features.json └── targets.js ├── ember-cli-build.js ├── mirage ├── algolia │ ├── hosts.ts │ └── utils.ts ├── config.ts ├── factories │ ├── document-type.ts │ ├── document.ts │ ├── google │ │ └── person.ts │ ├── group.ts │ ├── jira-issue.ts │ ├── jira-picker-result.ts │ ├── me.ts │ ├── person.ts │ ├── product.ts │ ├── project.ts │ ├── recently-viewed-doc.ts │ ├── recently-viewed-docs-database.ts │ ├── recently-viewed-project.ts │ ├── related-external-link.ts │ └── related-hermes-document.ts ├── helpers.ts ├── models │ ├── document-type.ts │ ├── document.ts │ ├── google │ │ └── person.ts │ ├── group.ts │ ├── jira-issue.ts │ ├── jira-picker-result.ts │ ├── me.ts │ ├── person.ts │ ├── product.ts │ ├── project.ts │ ├── recently-viewed-doc.ts │ ├── recently-viewed-docs-database.ts │ ├── recently-viewed-project.ts │ ├── related-external-link.ts │ └── related-hermes-document.ts └── utils.ts ├── package.json ├── public ├── images │ ├── document.png │ ├── favicon.png │ └── hermes-logo.png └── robots.txt ├── tailwind.config.js ├── testem.js ├── tests ├── acceptance │ ├── 404-test.ts │ ├── application-test.ts │ ├── authenticate-test.ts │ ├── authenticated-test.ts │ └── authenticated │ │ ├── all-test.ts │ │ ├── dashboard-test.ts │ │ ├── document-test.ts │ │ ├── documents-test.ts │ │ ├── drafts-test.ts │ │ ├── my-test.ts │ │ ├── my │ │ └── documents-test.ts │ │ ├── new-test.ts │ │ ├── new │ │ ├── doc-test.ts │ │ └── project-test.ts │ │ ├── product-areas-test.ts │ │ ├── product-areas │ │ └── product-area-test.ts │ │ ├── projects-test.ts │ │ ├── projects │ │ └── project-test.ts │ │ ├── results-test.ts │ │ └── settings-test.ts ├── helpers │ ├── .gitkeep │ └── flash-message.js ├── index.html ├── integration │ ├── .gitkeep │ ├── components │ │ ├── action-test.js │ │ ├── copy-u-r-l-button-test.ts │ │ ├── custom-editable-field-test.ts │ │ ├── dashboard │ │ │ ├── docs-awaiting-review-test.ts │ │ │ ├── docs-awaiting-review │ │ │ │ └── doc-test.ts │ │ │ ├── latest-docs-test.ts │ │ │ ├── new-features-banner-test.ts │ │ │ ├── recently-viewed-test.ts │ │ │ └── recently-viewed │ │ │ │ └── item-test.ts │ │ ├── doc │ │ │ ├── folder-affordance-test.ts │ │ │ ├── status-test.ts │ │ │ ├── thumbnail-test.ts │ │ │ └── tile-medium-test.ts │ │ ├── document │ │ │ ├── modal-test.ts │ │ │ ├── sidebar-test.ts │ │ │ ├── sidebar │ │ │ │ ├── empty-state-add-button-test.ts │ │ │ │ ├── header-test.ts │ │ │ │ ├── related-resources-test.ts │ │ │ │ ├── related-resources │ │ │ │ │ ├── list-item-test.ts │ │ │ │ │ └── list-test.ts │ │ │ │ └── section-header-test.ts │ │ │ └── status-icon-test.ts │ │ ├── editable-field-test.ts │ │ ├── editable-field │ │ │ ├── read-value-test.ts │ │ │ └── read-value │ │ │ │ └── person-test.ts │ │ ├── empty-state-text-test.ts │ │ ├── external-link-test.ts │ │ ├── favicon-test.ts │ │ ├── floating-u-i │ │ │ ├── content-test.ts │ │ │ └── index-test.ts │ │ ├── footer-test.ts │ │ ├── header │ │ │ ├── active-filter-list-item-test.ts │ │ │ ├── nav-test.ts │ │ │ ├── search-test.ts │ │ │ └── toolbar-test.ts │ │ ├── inputs │ │ │ ├── people-select-test.ts │ │ │ ├── product-select-test.ts │ │ │ └── product-select │ │ │ │ └── item-test.ts │ │ ├── match-count-headline-test.ts │ │ ├── modals-test.ts │ │ ├── modals │ │ │ └── draft-created-test.ts │ │ ├── my │ │ │ ├── docs-test.ts │ │ │ └── header-test.ts │ │ ├── new │ │ │ └── form-test.ts │ │ ├── person-test.ts │ │ ├── person │ │ │ └── avatar-test.ts │ │ ├── product-link-test.ts │ │ ├── product │ │ │ ├── avatar-test.ts │ │ │ └── subscription-toggle-test.ts │ │ ├── project │ │ │ ├── jira-widget-test.ts │ │ │ ├── resource-test.ts │ │ │ ├── status-icon-test.ts │ │ │ └── tile-test.ts │ │ ├── projects │ │ │ └── add-to-or-create-test.ts │ │ ├── related-resources-test.ts │ │ ├── related-resources │ │ │ ├── add-test.ts │ │ │ ├── add │ │ │ │ └── external-resource-test.ts │ │ │ └── overflow-menu-test.ts │ │ ├── settings │ │ │ ├── subscription-list-item-test.ts │ │ │ └── subscription-list-test.ts │ │ ├── table │ │ │ └── row-test.ts │ │ ├── tooltip-icon-test.ts │ │ ├── truncated-text-test.ts │ │ ├── type-to-confirm-test.ts │ │ └── x │ │ │ └── dropdown-list │ │ │ ├── checkable-item-test.ts │ │ │ └── index-test.ts │ ├── helpers │ │ ├── dasherize-test.ts │ │ ├── get-facet-label-test.ts │ │ ├── get-facet-query-hash-test.ts │ │ ├── get-model-attrs-test.ts │ │ ├── get-owner-query-test.ts │ │ ├── get-product-id-test.js │ │ ├── get-product-label-test.ts │ │ ├── highlight-text-test.ts │ │ ├── html-element-test.ts │ │ ├── is-active-filter-test.ts │ │ ├── lowercase-test.js │ │ ├── maybe-query-test.ts │ │ ├── model-or-models-test.ts │ │ ├── parse-date-test.ts │ │ └── time-ago-test.ts │ └── modifiers │ │ ├── auto-height-textarea-test.ts │ │ ├── autofocus-test.ts │ │ ├── dismissible-test.ts │ │ └── tooltip-test.ts ├── mirage-helpers │ └── utils.ts ├── test-helper.ts └── unit │ ├── .gitkeep │ ├── services │ ├── active-filters-test.ts │ ├── fetch-test.ts │ ├── latest-docs-test.ts │ ├── modal-alerts-test.ts │ ├── product-areas-test.ts │ ├── recently-viewed-test.ts │ ├── session-test.ts │ └── viewport-test.ts │ └── utils │ ├── blink-element-test.ts │ ├── clean-string-test.ts │ ├── get-product-id-test.js │ ├── get-product-label-test.ts │ ├── html-element-test.ts │ ├── is-valid-u-r-l-test.ts │ ├── parse-date-test.ts │ ├── time-ago-test.ts │ └── update-related-resources-sort-order-test.ts ├── tsconfig.json ├── types ├── ember-animated-tools │ └── animated-tools.d.ts ├── ember-animated │ ├── transition-rules.d.ts │ └── transition.ts ├── ember-cli-mirage │ └── test-support.d.ts ├── ember-data │ └── types │ │ └── registries │ │ └── model.d.ts ├── ember-metrics │ ├── metrics-adapters │ │ └── google-analytics-four.d.ts │ └── services │ │ └── metrics.d.ts ├── ember-page-title │ ├── index.ts │ └── test-support │ │ └── index.d.ts ├── ember-set-body-class │ └── index.ts ├── ember-simple-auth │ └── services │ │ ├── index.d.ts │ │ └── session.d.ts ├── glint │ └── index.d.ts ├── global.d.ts ├── hds │ ├── _shared.ts │ ├── alert.d.ts │ ├── badge-count.d.ts │ ├── badge.d.ts │ ├── button-set.d.ts │ ├── button.d.ts │ ├── card │ │ └── container.d.ts │ ├── dropdown.d.ts │ ├── flight-icon.d.ts │ ├── form │ │ ├── checkbox │ │ │ └── fields.d.ts │ │ ├── error │ │ │ └── index.d.ts │ │ ├── field.d.ts │ │ ├── label.d.ts │ │ ├── text-input │ │ │ ├── base.d.ts │ │ │ └── field.d.ts │ │ ├── textarea │ │ │ └── field.d.ts │ │ └── toggle │ │ │ └── base.d.ts │ ├── icon-tile.d.ts │ ├── link │ │ ├── inline.d.ts │ │ └── standalone.d.ts │ ├── modal.d.ts │ ├── table │ │ ├── index.d.ts │ │ ├── td.d.ts │ │ └── tr.d.ts │ └── toast.d.ts └── hermes │ └── index.d.ts ├── vendor └── .gitkeep ├── web.go └── yarn.lock /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hashicorp-forge/labs 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **DELETE THIS TEMPLATE BEFORE SUBMITTING** 2 | 3 | In the short term, there are several large changes planned for the Hermes project. 4 | Before submitting a pull request we request that you submit a 5 | [GitHub issue](https://github.com/hashicorp-forge/hermes/issues/new) first 6 | so maintainers can validate the proposed change. This is to make sure 7 | there aren’t any conflicts with the upcoming plans for the project. As the 8 | project becomes more stable over the next several releases, we think it 9 | will become much easier to contribute. 10 | 11 | If your PR resolves any open issue(s), please indicate them like this so they will be closed when your PR is merged: 12 | 13 | Closes #xxx 14 | Closes #xxx 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /config.hcl 2 | /hermes 3 | 4 | # Google OAuth 2.0 5 | /credentials.json 6 | /token.json 7 | 8 | # macOS & local 9 | .DS_Store 10 | .env 11 | 12 | # Web application 13 | node_modules 14 | /web/.pnp.* 15 | /web/.yarn/* 16 | /web/.eslintcache 17 | /web/dist 18 | 19 | # Terraform related 20 | .terraform 21 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /cmd/hermes/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/hashicorp-forge/hermes/internal/cmd" 8 | ) 9 | 10 | func main() { 11 | // Name of the executable 12 | os.Args[0] = filepath.Base(os.Args[0]) 13 | 14 | os.Exit(cmd.Main(os.Args)) 15 | } 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | postgres: 5 | image: postgres:14.5-alpine 6 | restart: always 7 | environment: 8 | POSTGRES_USER: postgres 9 | POSTGRES_PASSWORD: postgres 10 | POSTGRES_DB: db 11 | ports: 12 | - "5432:5432" 13 | volumes: 14 | - postgres:/var/lib/postgresql/data 15 | 16 | volumes: 17 | postgres: 18 | -------------------------------------------------------------------------------- /internal/api/document_types.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/hashicorp-forge/hermes/internal/config" 8 | "github.com/hashicorp/go-hclog" 9 | ) 10 | 11 | func DocumentTypesHandler(cfg config.Config, log hclog.Logger) http.Handler { 12 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 | switch r.Method { 14 | case "GET": 15 | w.Header().Set("Content-Type", "application/json") 16 | 17 | enc := json.NewEncoder(w) 18 | err := enc.Encode(cfg.DocumentTypes.DocumentType) 19 | if err != nil { 20 | log.Error("error encoding document types", 21 | "error", err, 22 | "method", r.Method, 23 | "path", r.URL.Path) 24 | http.Error(w, "{\"error\": \"Error getting document types\"}", 25 | http.StatusInternalServerError) 26 | return 27 | } 28 | 29 | default: 30 | w.WriteHeader(http.StatusMethodNotAllowed) 31 | return 32 | } 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /internal/api/v2/document_types.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/hashicorp-forge/hermes/internal/server" 8 | ) 9 | 10 | func DocumentTypesHandler(srv server.Server) http.Handler { 11 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 12 | switch r.Method { 13 | case "GET": 14 | w.Header().Set("Content-Type", "application/json") 15 | 16 | enc := json.NewEncoder(w) 17 | err := enc.Encode(srv.Config.DocumentTypes.DocumentType) 18 | if err != nil { 19 | srv.Logger.Error("error encoding document types", 20 | "error", err, 21 | "method", r.Method, 22 | "path", r.URL.Path) 23 | http.Error(w, "{\"error\": \"Error getting document types\"}", 24 | http.StatusInternalServerError) 25 | return 26 | } 27 | 28 | default: 29 | w.WriteHeader(http.StatusMethodNotAllowed) 30 | return 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /internal/auth/oktaalb/doc.go: -------------------------------------------------------------------------------- 1 | // Package oktaalb implements authorization using Okta and an Amazon Application 2 | // Load Balancer. 3 | package oktaalb 4 | -------------------------------------------------------------------------------- /internal/cmd/commands/operator/operator.go: -------------------------------------------------------------------------------- 1 | package operator 2 | 3 | import ( 4 | "github.com/hashicorp-forge/hermes/internal/cmd/base" 5 | "github.com/mitchellh/cli" 6 | ) 7 | 8 | type Command struct { 9 | *base.Command 10 | } 11 | 12 | func (c *Command) Synopsis() string { 13 | return "Perform operator-specific tasks" 14 | } 15 | 16 | func (c *Command) Help() string { 17 | return `Usage: hermes operator [options] [args] 18 | 19 | This command groups subcommands for operators interacting with Hermes.` 20 | } 21 | 22 | func (c *Command) Run(args []string) int { 23 | return cli.RunResultHelp 24 | } 25 | -------------------------------------------------------------------------------- /internal/cmd/commands/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "github.com/hashicorp-forge/hermes/internal/cmd/base" 5 | "github.com/hashicorp-forge/hermes/internal/version" 6 | ) 7 | 8 | type Command struct { 9 | *base.Command 10 | } 11 | 12 | func (c *Command) Synopsis() string { 13 | return "Print the version of the binary" 14 | } 15 | 16 | func (c *Command) Help() string { 17 | return `Usage: hermes version 18 | 19 | This command prints the version of the binary.` 20 | } 21 | 22 | func (c *Command) Run(args []string) int { 23 | c.UI.Output(version.Version) 24 | 25 | return 0 26 | } 27 | -------------------------------------------------------------------------------- /internal/cmd/commands/version/version_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/hashicorp/go-hclog" 8 | "github.com/mitchellh/cli" 9 | 10 | "github.com/hashicorp-forge/hermes/internal/cmd/base" 11 | ) 12 | 13 | func TestVersion(t *testing.T) { 14 | log := hclog.NewNullLogger() 15 | ui := cli.NewMockUi() 16 | c := &Command{ 17 | Command: base.NewCommand(log, ui), 18 | } 19 | 20 | args := []string{} 21 | if code := c.Run(args); code != 0 { 22 | t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 23 | } 24 | 25 | output := ui.OutputWriter.String() 26 | if matched, _ := regexp.MatchString(`^\d\.\d\.\d\n$`, output); !matched { 27 | t.Fatalf("output is not a valid version: %s", output) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/config/helpers.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // ValidateFeatureFlags validates the feature flags defined in the config. 8 | func ValidateFeatureFlags(flags []*FeatureFlag) error { 9 | for _, f := range flags { 10 | if f.Name == "" { 11 | return fmt.Errorf("feature flag 'name' cannot be empty") 12 | } 13 | if f.Enabled != nil && f.Percentage > 0 { 14 | return fmt.Errorf("invalid definition of feature flag %q: only one of 'enabled' or 'percentage' parameter can be set", f.Name) 15 | } 16 | if f.Enabled == nil && f.Percentage == 0 { 17 | return fmt.Errorf("invalid definition of feature flag %q: at least one of 'enabled' or a non-zero value for 'percentage' parameter should be set", f.Name) 18 | } 19 | } 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /internal/datadog/doc.go: -------------------------------------------------------------------------------- 1 | // Package datadog contains logic for working with Datadog. 2 | package datadog 3 | -------------------------------------------------------------------------------- /internal/helpers/helpers.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | // RemoveStringSliceDuplicates removes duplicate strings from a string slice. 4 | func RemoveStringSliceDuplicates(in []string) []string { 5 | keys := make(map[string]bool) 6 | out := []string{} 7 | for _, s := range in { 8 | if _, seen := keys[s]; !seen { 9 | keys[s] = true 10 | out = append(out, s) 11 | } 12 | } 13 | return out 14 | } 15 | 16 | // StringSliceContains returns true if a string is present in a slice of 17 | // strings. 18 | func StringSliceContains(values []string, s string) bool { 19 | for _, v := range values { 20 | if s == v { 21 | return true 22 | } 23 | } 24 | return false 25 | } 26 | -------------------------------------------------------------------------------- /internal/jira/doc.go: -------------------------------------------------------------------------------- 1 | // Package jira contains logic for working with Jira. 2 | package jira 3 | -------------------------------------------------------------------------------- /internal/pkg/doctypes/doc.go: -------------------------------------------------------------------------------- 1 | // Package doctypes manages document types. 2 | package doctypes 3 | -------------------------------------------------------------------------------- /internal/pub/assets/document.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-forge/hermes/caa1b990d237b39a99643f84ec5cbdbc6e086f2b/internal/pub/assets/document.png -------------------------------------------------------------------------------- /internal/pub/assets/hermes-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-forge/hermes/caa1b990d237b39a99643f84ec5cbdbc6e086f2b/internal/pub/assets/hermes-logo.png -------------------------------------------------------------------------------- /internal/pub/pub.go: -------------------------------------------------------------------------------- 1 | package pub 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | "net/http" 7 | ) 8 | 9 | //go:embed assets/* 10 | var assetsFS embed.FS 11 | 12 | func Handler() http.Handler { 13 | return http.FileServer(httpFileSystem()) 14 | } 15 | 16 | func httpFileSystem() http.FileSystem { 17 | return http.FS(fileSystem()) 18 | } 19 | 20 | func fileSystem() fs.FS { 21 | f, err := fs.Sub(assetsFS, "assets") 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | return f 27 | } 28 | -------------------------------------------------------------------------------- /internal/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/hashicorp-forge/hermes/internal/config" 5 | "github.com/hashicorp-forge/hermes/internal/jira" 6 | "github.com/hashicorp-forge/hermes/pkg/algolia" 7 | gw "github.com/hashicorp-forge/hermes/pkg/googleworkspace" 8 | "github.com/hashicorp/go-hclog" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | // Server contains the server configuration. 13 | type Server struct { 14 | // AlgoSearch is the Algolia search client for the server. 15 | AlgoSearch *algolia.Client 16 | 17 | // AlgoWrite is the Algolia write client for the server. 18 | AlgoWrite *algolia.Client 19 | 20 | // Config is the config for the server. 21 | Config *config.Config 22 | 23 | // DB is the database for the server. 24 | DB *gorm.DB 25 | 26 | // GWService is the Google Workspace service for the server. 27 | GWService *gw.Service 28 | 29 | // Jira is the Jira service for the server. 30 | Jira *jira.Service 31 | 32 | // Logger is the logger for the server. 33 | Logger hclog.Logger 34 | } 35 | -------------------------------------------------------------------------------- /internal/structs/product.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | // ProductDocTypeData contains data for each document type. 4 | type ProductDocTypeData struct { 5 | FolderID string `json:"folderID"` 6 | LatestDocNumber int `json:"latestDocNumber"` 7 | } 8 | 9 | // ProductData is the data associated with a product or area. 10 | // This may include product abbreviation, etc. 11 | type ProductData struct { 12 | Abbreviation string `json:"abbreviation"` 13 | // PerDocTypeData is a map of each document type (RFC, PRD, etc) 14 | // to the associated data 15 | PerDocTypeData map[string]ProductDocTypeData `json:"perDocTypeData"` 16 | } 17 | 18 | // Products is the slice of product data. 19 | type Products struct { 20 | // ObjectID should be "products" 21 | ObjectID string `json:"objectID,omitempty"` 22 | Data map[string]ProductData `json:"data"` 23 | } 24 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "runtime/debug" 5 | ) 6 | 7 | const Version = "0.5.0" 8 | 9 | // GetVersion returns 10 | // the version number 11 | func GetVersion() string { 12 | return Version 13 | } 14 | 15 | // GetShortRevision returns a short 16 | // commit or checkout revision 17 | // from build information 18 | func GetShortRevision() string { 19 | if info, ok := debug.ReadBuildInfo(); ok { 20 | for _, setting := range info.Settings { 21 | if setting.Key == "vcs.revision" { 22 | shortRevision := setting.Value 23 | if len(shortRevision) > 7 { 24 | shortRevision = setting.Value[:7] 25 | } 26 | return shortRevision 27 | } 28 | } 29 | } 30 | return "" 31 | } 32 | -------------------------------------------------------------------------------- /pkg/algolia/doc.go: -------------------------------------------------------------------------------- 1 | // Package algolia contains logic for working with Algolia. 2 | package algolia 3 | -------------------------------------------------------------------------------- /pkg/document/doc.go: -------------------------------------------------------------------------------- 1 | // Package document defines a document struct and contains logic for working 2 | // with these documents. 3 | package document 4 | -------------------------------------------------------------------------------- /pkg/googleworkspace/backoff.go: -------------------------------------------------------------------------------- 1 | package googleworkspace 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/cenkalti/backoff/v4" 7 | "github.com/hashicorp/go-hclog" 8 | ) 9 | 10 | // defaultBackoff returns a default exponential backoff configuration for Google 11 | // Workspace APIs. 12 | func defaultBackoff() *backoff.ExponentialBackOff { 13 | bo := backoff.NewExponentialBackOff() 14 | bo.MaxElapsedTime = 2 * time.Minute 15 | 16 | return bo 17 | } 18 | 19 | // backoffNotify is an exponential backoff notify function that logs the error 20 | // and wait duration as a warning. 21 | func backoffNotify(err error, d time.Duration) { 22 | // TODO: enable passing in a logger. 23 | l := hclog.Default() 24 | l.Warn("backoff error (retrying)", 25 | "error", err, 26 | "delay", d, 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/googleworkspace/doc.go: -------------------------------------------------------------------------------- 1 | // Package googleworkspace contains logic for working with Google Workspace. 2 | package googleworkspace 3 | -------------------------------------------------------------------------------- /pkg/googleworkspace/gmail_helpers.go: -------------------------------------------------------------------------------- 1 | package googleworkspace 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "strings" 7 | 8 | "google.golang.org/api/gmail/v1" 9 | ) 10 | 11 | // SendEmail sends an email. 12 | func (s *Service) SendEmail(to []string, from, subject, body string) (*gmail.Message, error) { 13 | email := fmt.Sprintf("To: %s\r\nFrom: %s\r\nContent-Type: text/html; charset=UTF-8\r\nSubject: %s\r\n\r\n%s\r\n", 14 | strings.Join(to, ","), from, subject, body) 15 | 16 | msg := &gmail.Message{ 17 | Raw: base64.URLEncoding.EncodeToString([]byte(email)), 18 | } 19 | 20 | resp, err := s.Gmail.Users.Messages.Send("me", msg).Do() 21 | if err != nil { 22 | return nil, fmt.Errorf("error sending email: %w", err) 23 | } 24 | return resp, nil 25 | } 26 | -------------------------------------------------------------------------------- /pkg/googleworkspace/oauth2_helpers.go: -------------------------------------------------------------------------------- 1 | package googleworkspace 2 | 3 | import ( 4 | "google.golang.org/api/oauth2/v2" 5 | ) 6 | 7 | // ValidateAccessToken validates a Google access token and returns the token 8 | // info. 9 | func (s *Service) ValidateAccessToken( 10 | accessToken string) (*oauth2.Tokeninfo, error) { 11 | 12 | resp, err := s.OAuth2.Tokeninfo(). 13 | AccessToken(accessToken). 14 | Fields("*"). 15 | Do() 16 | if err != nil { 17 | return nil, err 18 | } 19 | return resp, nil 20 | } 21 | -------------------------------------------------------------------------------- /pkg/hashicorpdocs/doc.go: -------------------------------------------------------------------------------- 1 | // Package hashicorpdocs contains helpers for working with HashiCorp's document 2 | // templates. 3 | package hashicorpdocs 4 | -------------------------------------------------------------------------------- /pkg/models/document_related_resource.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | // DocumentRelatedResource is a model for a document related resource. 8 | type DocumentRelatedResource struct { 9 | gorm.Model 10 | 11 | // Document is the document that the related resource is attached to. 12 | Document Document 13 | DocumentID uint `gorm:"uniqueIndex:document_id_sort_order_unique"` 14 | 15 | // RelatedResourceID is the foreign key of the related resource, set by a Gorm 16 | // polymorphic relationship. 17 | RelatedResourceID uint `gorm:"default:null;not null"` 18 | 19 | // RelatedResourceType is the table for the related resource, set by a Gorm 20 | // polymorphic relationship. 21 | RelatedResourceType string `gorm:"default:null;not null"` 22 | 23 | // SortOrder is the relative order of the related resource in comparison to 24 | // all of the document's other related resources. 25 | SortOrder int `gorm:"default:null;not null;uniqueIndex:document_id_sort_order_unique"` 26 | } 27 | -------------------------------------------------------------------------------- /pkg/models/gorm.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | func ModelsToAutoMigrate() []interface{} { 4 | return []interface{}{ 5 | &DocumentType{}, 6 | &Document{}, 7 | &DocumentCustomField{}, 8 | &DocumentFileRevision{}, 9 | DocumentGroupReview{}, 10 | &DocumentRelatedResource{}, 11 | &DocumentRelatedResourceExternalLink{}, 12 | &DocumentRelatedResourceHermesDocument{}, 13 | &DocumentReview{}, 14 | &DocumentTypeCustomField{}, 15 | &Group{}, 16 | &IndexerFolder{}, 17 | &IndexerMetadata{}, 18 | &Product{}, 19 | &ProductLatestDocumentNumber{}, 20 | &Project{}, 21 | &ProjectRelatedResource{}, 22 | &ProjectRelatedResourceExternalLink{}, 23 | &ProjectRelatedResourceHermesDocument{}, 24 | &User{}, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pkg/models/project_related_resource.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | // ProjectRelatedResource is a model for a project related resource. 8 | type ProjectRelatedResource struct { 9 | gorm.Model 10 | 11 | // Project is the project that the related resource is attached to. 12 | Project Project 13 | ProjectID uint `gorm:"uniqueIndex:project_id_sort_order_unique"` 14 | 15 | // RelatedResourceID is the foreign key of the related resource, set by a Gorm 16 | // polymorphic relationship. 17 | RelatedResourceID uint `gorm:"default:null;not null"` 18 | 19 | // RelatedResourceType is the table for the related resource, set by a Gorm 20 | // polymorphic relationship. 21 | RelatedResourceType string `gorm:"default:null;not null"` 22 | 23 | // SortOrder is the relative order of the related resource in comparison to 24 | // all of the project's other related resources. 25 | SortOrder int `gorm:"default:null;not null;uniqueIndex:project_id_sort_order_unique"` 26 | } 27 | -------------------------------------------------------------------------------- /pkg/models/testing.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/hashicorp-forge/hermes/internal/test" 8 | "github.com/stretchr/testify/require" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | func setupTest(t *testing.T, dsn string) ( 13 | db *gorm.DB, tearDownFunc func(t *testing.T), 14 | ) { 15 | // Create test database. 16 | db, dbName, err := test.CreateTestDatabase(t, dsn) 17 | require.NoError(t, err) 18 | 19 | // Enable citext extension. 20 | sqlDB, err := db.DB() 21 | require.NoError(t, err) 22 | _, err = sqlDB.Exec("CREATE EXTENSION IF NOT EXISTS citext;") 23 | require.NoError(t, err) 24 | 25 | // Migrate test database. 26 | err = db.AutoMigrate( 27 | ModelsToAutoMigrate()..., 28 | ) 29 | require.NoError(t, err) 30 | 31 | return db, func(t *testing.T) { 32 | // TODO: add back and make configurable. 33 | // err := test.DropTestDatabase(dsn, dbName) 34 | // require.NoError(t, err) 35 | log.Printf("would have dropped test database %q here", dbName) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /web/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | "prettier-plugin-ember-template-tag", 4 | "prettier-plugin-tailwindcss", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /web/app/adapters/application.ts: -------------------------------------------------------------------------------- 1 | import JSONAdapter from "@ember-data/adapter/json-api"; 2 | import { inject as service } from "@ember/service"; 3 | import ConfigService from "hermes/services/config"; 4 | import FetchService from "hermes/services/fetch"; 5 | import SessionService from "hermes/services/session"; 6 | 7 | export default class ApplicationAdapter extends JSONAdapter { 8 | @service("config") declare configSvc: ConfigService; 9 | @service("fetch") declare fetchSvc: FetchService; 10 | @service declare session: SessionService; 11 | 12 | get namespace() { 13 | return `api/${this.configSvc.config.api_version}`; 14 | } 15 | 16 | get headers() { 17 | return { 18 | "Hermes-Google-Access-Token": 19 | this.session.data.authenticated.access_token, 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /web/app/adapters/group.ts: -------------------------------------------------------------------------------- 1 | import DS from "ember-data"; 2 | import ApplicationAdapter from "./application"; 3 | import RSVP from "rsvp"; 4 | 5 | export default class GroupAdapter extends ApplicationAdapter { 6 | /** 7 | * The Query method for the group model. 8 | * Returns an array of groups that match the query. 9 | * Also used by the `queryRecord` method. 10 | */ 11 | query(_store: DS.Store, _type: DS.Model, query: { query: string }) { 12 | const results = this.fetchSvc 13 | .fetch(`/api/${this.configSvc.config.api_version}/groups`, { 14 | method: "POST", 15 | body: JSON.stringify({ 16 | // Spaces throw an error, so we replace them with dashes 17 | query: query.query.replace(" ", "-"), 18 | }), 19 | }) 20 | .then((r) => r?.json()); 21 | 22 | return RSVP.hash({ results }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /web/app/adapters/jira-issue.ts: -------------------------------------------------------------------------------- 1 | import DS from "ember-data"; 2 | import ApplicationAdapter from "./application"; 3 | import RSVP from "rsvp"; 4 | import ModelRegistry from "ember-data/types/registries/model"; 5 | import JiraIssueModel from "hermes/models/jira-issue"; 6 | 7 | export default class JiraIssueAdapter extends ApplicationAdapter { 8 | findRecord( 9 | _store: DS.Store, 10 | _type: ModelRegistry[K], 11 | id: string, 12 | _snapshot: DS.Snapshot, 13 | ): RSVP.Promise { 14 | const issue = this.fetchSvc 15 | .fetch(`/api/${this.configSvc.config.api_version}/jira/issues/${id}`) 16 | .then((response) => response?.json()); 17 | 18 | return RSVP.resolve(issue); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /web/app/adapters/person.ts: -------------------------------------------------------------------------------- 1 | import DS from "ember-data"; 2 | import ApplicationAdapter from "./application"; 3 | import RSVP from "rsvp"; 4 | 5 | export default class PersonAdapter extends ApplicationAdapter { 6 | /** 7 | * Queries using the `body` parameter instead of a queryParam. 8 | * Default query: `/people?query=foo` 9 | * Our custom query: `/people` with `{ query: "foo" }` in the request body. 10 | */ 11 | query(_store: DS.Store, _type: DS.Model, query: { query: string }) { 12 | const results = this.fetchSvc 13 | .fetch(`/api/${this.configSvc.config.api_version}/people`, { 14 | method: "POST", 15 | body: JSON.stringify({ 16 | query: query.query, 17 | }), 18 | }) 19 | .then((r) => r?.json()); 20 | 21 | return RSVP.hash({ results }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/app/app.ts: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from 'hermes/config/environment'; 5 | 6 | export default class App extends Application { 7 | modulePrefix = config.modulePrefix; 8 | podModulePrefix = config.podModulePrefix; 9 | Resolver = Resolver; 10 | } 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | -------------------------------------------------------------------------------- /web/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-forge/hermes/caa1b990d237b39a99643f84ec5cbdbc6e086f2b/web/app/components/.gitkeep -------------------------------------------------------------------------------- /web/app/components/action.gts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | interface ActionComponentSignature { 4 | Element: HTMLButtonElement; 5 | Blocks: { 6 | default: []; 7 | }; 8 | } 9 | 10 | export default class ActionComponent extends Component { 11 | 16 | } 17 | 18 | declare module "@glint/environment-ember-loose/registry" { 19 | export default interface Registry { 20 | Action: typeof ActionComponent; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /web/app/components/application-loading/index.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /web/app/components/application-loading/index.js: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | /** 4 | * Renders the initial loading screen before the app is loaded. 5 | */ 6 | export default class ApplicationLoadingComponent extends Component {} 7 | -------------------------------------------------------------------------------- /web/app/components/copy-u-r-l-button.hbs: -------------------------------------------------------------------------------- 1 | 20 | 21 | {{#unless @isIconOnly}} 22 | {{this.defaultText}} 23 | {{/unless}} 24 | 25 | -------------------------------------------------------------------------------- /web/app/components/custom-editable-field.hbs: -------------------------------------------------------------------------------- 1 |
2 | 11 |
12 | -------------------------------------------------------------------------------- /web/app/components/dashboard/docs-awaiting-review/doc.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { HermesDocument } from "hermes/types/document"; 3 | 4 | interface DashboardDocsAwaitingReviewDocComponentSignature { 5 | Element: null; 6 | Args: { 7 | doc: HermesDocument; 8 | }; 9 | Blocks: { 10 | default: []; 11 | }; 12 | } 13 | 14 | export default class DashboardDocsAwaitingReviewDocComponent extends Component {} 15 | 16 | declare module "@glint/environment-ember-loose/registry" { 17 | export default interface Registry { 18 | "Dashboard::DocsAwaitingReview::Doc": typeof DashboardDocsAwaitingReviewDocComponent; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /web/app/components/dashboard/index.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 |
9 | {{! Main }} 10 |
11 |
12 | {{#if @docsAwaitingReview}} 13 | 14 | {{/if}} 15 | 16 |
17 |
18 | {{! Secondary }} 19 | 20 |
21 | -------------------------------------------------------------------------------- /web/app/components/dashboard/index.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import AuthenticatedUserService from "hermes/services/authenticated-user"; 3 | import { HermesDocument } from "hermes/types/document"; 4 | import { inject as service } from "@ember/service"; 5 | 6 | interface DashboardIndexComponentSignature { 7 | Element: null; 8 | Args: { 9 | docsAwaitingReview?: HermesDocument[]; 10 | }; 11 | Blocks: { 12 | default: []; 13 | }; 14 | } 15 | 16 | export default class DashboardIndexComponent extends Component { 17 | @service declare authenticatedUser: AuthenticatedUserService; 18 | 19 | protected get firstName(): string { 20 | return this.authenticatedUser.info.firstName; 21 | } 22 | } 23 | 24 | declare module "@glint/environment-ember-loose/registry" { 25 | export default interface Registry { 26 | Dashboard: typeof DashboardIndexComponent; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /web/app/components/doc/folder-affordance.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { DocThumbnailSize } from "hermes/components/doc/thumbnail"; 3 | import { HermesSize } from "hermes/types/sizes"; 4 | 5 | interface DocFolderAffordanceSignature { 6 | Args: { 7 | size?: `${DocThumbnailSize}`; 8 | }; 9 | } 10 | 11 | export default class DocFolderAffordance extends Component { 12 | protected get sizeIsSmall(): boolean { 13 | return this.args.size === HermesSize.Small || !this.args.size; 14 | } 15 | } 16 | 17 | declare module "@glint/environment-ember-loose/registry" { 18 | export default interface Registry { 19 | "Doc::FolderAffordance": typeof DocFolderAffordance; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /web/app/components/doc/snippet.hbs: -------------------------------------------------------------------------------- 1 |

2 | {{! Triple-stashed to escape the HTML }} 3 | {{{@snippet}}} 4 |

5 | -------------------------------------------------------------------------------- /web/app/components/doc/snippet.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | interface DocSnippetComponentSignature { 4 | Element: HTMLParagraphElement; 5 | Args: { 6 | snippet?: string; 7 | }; 8 | } 9 | 10 | export default class DocSnippetComponent extends Component {} 11 | 12 | declare module "@glint/environment-ember-loose/registry" { 13 | export default interface Registry { 14 | "Doc::Snippet": typeof DocSnippetComponent; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /web/app/components/doc/state-progress-bar.hbs: -------------------------------------------------------------------------------- 1 | {{! @glint-nocheck: not typesafe yet }} 2 |
  • 6 |
  • 7 | -------------------------------------------------------------------------------- /web/app/components/doc/status.hbs: -------------------------------------------------------------------------------- 1 | {{#unless @hideProgress}} 2 |
      6 |
    1. 7 |
    2. 8 |
    3. 9 |
    10 | {{/unless}} 11 | 12 | 19 | -------------------------------------------------------------------------------- /web/app/components/documents/table.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { HermesDocument } from "hermes/types/document"; 3 | import { 4 | SortAttribute, 5 | SortDirection, 6 | } from "hermes/components/table/sortable-header"; 7 | 8 | interface DocumentsTableComponentSignature { 9 | Args: { 10 | docs: HermesDocument[]; 11 | isDraft?: boolean; 12 | nbPages?: number; 13 | currentPage?: number; 14 | currentSort: `${SortAttribute}`; 15 | sortDirection: SortDirection; 16 | }; 17 | } 18 | export default class DocumentsTableComponent extends Component { 19 | protected get paginationIsShown() { 20 | return this.args.nbPages && this.args.currentPage !== undefined; 21 | } 22 | } 23 | 24 | declare module "@glint/environment-ember-loose/registry" { 25 | export default interface Registry { 26 | "Documents::Table": typeof DocumentsTableComponent; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /web/app/components/editable-field/read-value.hbs: -------------------------------------------------------------------------------- 1 | {{#let (element (or @tag "div")) as |Field|}} 2 | 3 | {{#if this.valueIsEmpty}} 4 | 5 | {{else}} 6 | {{#if this.typeIsPeople}} 7 |
      8 | {{#each this.emails as |email|}} 9 |
    1. 10 | 14 | 15 |
    2. 16 | {{/each}} 17 |
    18 | {{else}} 19 | 20 | {{this.stringValue}} 21 | 22 | {{/if}} 23 | {{/if}} 24 |
    25 | {{/let}} 26 | -------------------------------------------------------------------------------- /web/app/components/empty-state-text.gts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import or from "ember-truth-helpers/helpers/or"; 3 | 4 | interface EmptyStateTextComponentSignature { 5 | Element: HTMLSpanElement; 6 | Args: { 7 | value?: string; 8 | }; 9 | Blocks: {}; 10 | } 11 | 12 | export default class EmptyStateTextComponent extends Component { 13 | 18 | } 19 | 20 | declare module "@glint/environment-ember-loose/registry" { 21 | export default interface Registry { 22 | EmptyStateText: typeof EmptyStateTextComponent; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /web/app/components/external-link.hbs: -------------------------------------------------------------------------------- 1 | 9 | {{yield}} 10 | {{#if @iconIsShown}} 11 | 12 | {{/if}} 13 | 14 | -------------------------------------------------------------------------------- /web/app/components/external-link.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | interface ExternalLinkComponentSignature { 4 | Element: HTMLAnchorElement; 5 | Args: { 6 | iconIsShown?: boolean; 7 | }; 8 | Blocks: { 9 | default: []; 10 | }; 11 | } 12 | 13 | export default class ExternalLinkComponent extends Component {} 14 | 15 | declare module "@glint/environment-ember-loose/registry" { 16 | export default interface Registry { 17 | ExternalLink: typeof ExternalLinkComponent; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /web/app/components/favicon.hbs: -------------------------------------------------------------------------------- 1 | {{#if this.fallbackIconIsShown}} 2 | 8 | {{else}} 9 | 17 | {{/if}} 18 | -------------------------------------------------------------------------------- /web/app/components/favicon.ts: -------------------------------------------------------------------------------- 1 | import { action } from "@ember/object"; 2 | import Component from "@glimmer/component"; 3 | import { tracked } from "@glimmer/tracking"; 4 | 5 | interface FaviconComponentSignature { 6 | Element: SVGElement | HTMLImageElement; 7 | Args: { 8 | url: string; 9 | }; 10 | } 11 | 12 | export default class FaviconComponent extends Component { 13 | @tracked protected fallbackIconIsShown = false; 14 | 15 | protected readonly faviconURL = `https://www.google.com/s2/favicons?sz=64&domain=${this.args.url}`; 16 | 17 | @action protected showFallbackIcon() { 18 | this.fallbackIconIsShown = true; 19 | } 20 | } 21 | 22 | declare module "@glint/environment-ember-loose/registry" { 23 | export default interface Registry { 24 | Favicon: typeof FaviconComponent; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /web/app/components/floating-u-i/content.hbs: -------------------------------------------------------------------------------- 1 | {{! @glint-nocheck: not typesafe yet }} 2 | {{#maybe-in-element 3 | (html-element this.inElementTarget) (not @renderOut) insertBefore=null 4 | }} 5 |
    14 | {{yield}} 15 |
    16 | {{/maybe-in-element}} 17 | -------------------------------------------------------------------------------- /web/app/components/floating-u-i/index.hbs: -------------------------------------------------------------------------------- 1 | {{yield 2 | (hash 3 | contentIsShown=this.contentIsShown 4 | registerAnchor=this.registerAnchor 5 | toggleContent=this.toggleContent 6 | showContent=this.showContent 7 | hideContent=this.hideContent 8 | contentID=this.contentID 9 | ) 10 | to="anchor" 11 | }} 12 | 13 | {{#if this.contentIsShown}} 14 | 24 | {{yield 25 | (hash 26 | contentID=this.contentID hideContent=this.hideContent anchor=this.anchor 27 | ) 28 | to="content" 29 | }} 30 | 31 | {{/if}} 32 | -------------------------------------------------------------------------------- /web/app/components/footer.hbs: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /web/app/components/header.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 | -------------------------------------------------------------------------------- /web/app/components/header.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | interface HeaderComponentSignature { 4 | Args: { 5 | query?: string; 6 | }; 7 | } 8 | 9 | export default class HeaderComponent extends Component {} 10 | 11 | declare module "@glint/environment-ember-loose/registry" { 12 | export default interface Registry { 13 | Header: typeof HeaderComponent; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /web/app/components/header/active-filter-list-item.hbs: -------------------------------------------------------------------------------- 1 | 6 | 7 | {{this.filter}} 8 | 9 | -------------------------------------------------------------------------------- /web/app/components/header/active-filter-list.hbs: -------------------------------------------------------------------------------- 1 | {{#if this.shownFilters.length}} 2 |
    3 |

    Showing

    4 |
    5 | {{#each this.shownFilters as |filter|}} 6 | 7 | {{/each}} 8 |
    9 | 14 | 15 | Clear all 16 | 17 |
    18 | {{/if}} 19 | -------------------------------------------------------------------------------- /web/app/components/header/facet-dropdown.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { FacetDropdownObjects } from "hermes/types/facets"; 3 | import { FacetName } from "./toolbar"; 4 | 5 | interface HeaderFacetDropdownComponentSignature { 6 | Element: HTMLDivElement; 7 | Args: { 8 | name: FacetName; 9 | facets?: FacetDropdownObjects | null; 10 | }; 11 | } 12 | 13 | export default class HeaderFacetDropdownComponent extends Component { 14 | protected get isDisabled() { 15 | return !this.args.facets || Object.keys(this.args.facets).length === 0; 16 | } 17 | } 18 | 19 | declare module "@glint/environment-ember-loose/registry" { 20 | export default interface Registry { 21 | "Header::FacetDropdown": typeof HeaderFacetDropdownComponent; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/app/components/header/sort-dropdown.hbs: -------------------------------------------------------------------------------- 1 | 7 | <:anchor as |dd|> 8 | 13 | 14 | <:item as |dd|> 15 | 20 | 25 | {{dd.value}} 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /web/app/components/header/sort-dropdown.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { SortByFacets, SortByValue } from "./toolbar"; 3 | import { Placement } from "@floating-ui/dom"; 4 | 5 | interface HeaderSortDropdownComponentSignature { 6 | Args: { 7 | label: string; 8 | facets: SortByFacets; 9 | disabled: boolean; 10 | currentSortByValue: SortByValue; 11 | dropdownPlacement: Placement; 12 | }; 13 | } 14 | 15 | export default class HeaderSortDropdownComponent extends Component { 16 | get dateDesc() { 17 | return SortByValue.DateDesc; 18 | } 19 | 20 | get dateAsc() { 21 | return SortByValue.DateAsc; 22 | } 23 | } 24 | 25 | declare module "@glint/environment-ember-loose/registry" { 26 | export default interface Registry { 27 | "Header::SortDropdown": typeof HeaderSortDropdownComponent; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /web/app/components/header/user-menu-highlight.hbs: -------------------------------------------------------------------------------- 1 |
    5 |
    8 | 9 |
    12 |
    13 | -------------------------------------------------------------------------------- /web/app/components/header/user-menu-highlight.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | interface HeaderUserMenuHighlightSignature { 4 | Args: {}; 5 | } 6 | 7 | export default class HeaderUserMenuHighlight extends Component {} 8 | 9 | declare module "@glint/environment-ember-loose/registry" { 10 | export default interface Registry { 11 | "Header::UserMenuHighlight": typeof HeaderUserMenuHighlight; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /web/app/components/hermes-logo.hbs: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /web/app/components/hermes-logo.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | interface HermesLogoComponentSignature { 4 | Element: HTMLDivElement; 5 | } 6 | 7 | export default class HermesLogoComponent extends Component {} 8 | 9 | declare module "@glint/environment-ember-loose/registry" { 10 | export default interface Registry { 11 | HermesLogo: typeof HermesLogoComponent; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /web/app/components/inputs/product-select/item.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | interface InputsProductSelectItemComponentSignature { 4 | Element: HTMLDivElement; 5 | Args: { 6 | product?: string; 7 | isSelected?: boolean; 8 | abbreviation?: string; 9 | }; 10 | } 11 | 12 | export default class InputsProductSelectItemComponent extends Component { 13 | protected get abbreviationIsShown(): boolean { 14 | const { abbreviation, product } = this.args; 15 | if (abbreviation && abbreviation !== product) { 16 | return true; 17 | } 18 | return false; 19 | } 20 | } 21 | 22 | declare module "@glint/environment-ember-loose/registry" { 23 | export default interface Registry { 24 | "Inputs::ProductSelect::Item": typeof InputsProductSelectItemComponent; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /web/app/components/inputs/tag-select.hbs: -------------------------------------------------------------------------------- 1 | 14 | {{value}} 15 | 16 | -------------------------------------------------------------------------------- /web/app/components/match-count-headline.gts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import eq from "ember-truth-helpers/helpers/eq"; 3 | 4 | interface MatchCountHeadlineSignature { 5 | Element: HTMLHeadingElement; 6 | Args: { 7 | count: number; 8 | }; 9 | Blocks: { 10 | default: []; 11 | }; 12 | } 13 | 14 | export default class MatchCountHeadline extends Component { 15 | 21 | } 22 | 23 | declare module "@glint/environment-ember-loose/registry" { 24 | export default interface Registry { 25 | MatchCountHeadline: typeof MatchCountHeadline; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /web/app/components/modal-alert-error.hbs: -------------------------------------------------------------------------------- 1 | 10 | {{@title}} 11 | {{@description}} 12 | 13 | -------------------------------------------------------------------------------- /web/app/components/modal-alert-error.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | interface ModalAlertErrorComponentSignature { 4 | Element: HTMLDivElement; 5 | Args: { 6 | onDismiss: () => void; 7 | title: string; 8 | description: string; 9 | }; 10 | Blocks: { 11 | default: []; 12 | }; 13 | } 14 | 15 | export default class ModalAlertErrorComponent extends Component {} 16 | 17 | declare module "@glint/environment-ember-loose/registry" { 18 | export default interface Registry { 19 | ModalAlertError: typeof ModalAlertErrorComponent; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /web/app/components/modals/doc-transferred.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | 8 | Done 9 |
    10 |
    11 | 12 |
    13 |

    Ownership transferred.

    14 |

    15 | {{get-model-attr "person.name" this.newOwner}} 16 | has been notified of the change. 17 |

    18 |
    19 |
    20 | 21 | 26 | 27 |
    28 | -------------------------------------------------------------------------------- /web/app/components/modals/doc-transferred.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "@ember/debug"; 2 | import { inject as service } from "@ember/service"; 3 | import Component from "@glimmer/component"; 4 | import ModalAlertsService from "hermes/services/modal-alerts"; 5 | 6 | interface ModalsDocTransferredComponentSignature { 7 | Args: { 8 | close: () => void; 9 | }; 10 | } 11 | 12 | export default class ModalsDocTransferredComponent extends Component { 13 | @service declare modalAlerts: ModalAlertsService; 14 | 15 | protected get newOwner() { 16 | const { newOwner } = this.modalAlerts.data; 17 | assert("newOwner must be a string", typeof newOwner === "string"); 18 | return newOwner; 19 | } 20 | } 21 | 22 | declare module "@glint/environment-ember-loose/registry" { 23 | export default interface Registry { 24 | "Modals::DocTransferred": typeof ModalsDocTransferredComponent; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /web/app/components/multiselect/tag-chip.hbs: -------------------------------------------------------------------------------- 1 | {{! @glint-nocheck: not typesafe yet }} 2 | 3 | {{@option}} 4 | -------------------------------------------------------------------------------- /web/app/components/multiselect/user-email-image-chip.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/app/components/multiselect/user-email-image-chip.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | interface MultiselectUserEmailImageChipComponentSignature { 4 | Element: HTMLDivElement; 5 | Args: { 6 | option: string; 7 | }; 8 | Blocks: { 9 | default: []; 10 | }; 11 | } 12 | 13 | export default class MultiselectUserEmailImageChipComponent extends Component {} 14 | 15 | declare module "@glint/environment-ember-loose/registry" { 16 | export default interface Registry { 17 | "multiselect/user-email-image-chip": typeof MultiselectUserEmailImageChipComponent; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /web/app/components/new/document-template-list.ts: -------------------------------------------------------------------------------- 1 | import { inject as service } from "@ember/service"; 2 | import Component from "@glimmer/component"; 3 | import DocumentTypesService from "hermes/services/document-types"; 4 | 5 | interface NewDocumentTemplateListComponentSignature {} 6 | 7 | export default class NewDocumentTemplateListComponent extends Component { 8 | @service declare documentTypes: DocumentTypesService; 9 | 10 | protected get moreInfoLinksAreShown(): boolean { 11 | return !!this.documentTypes.index?.some((docType) => docType.moreInfoLink); 12 | } 13 | 14 | protected get docTypes() { 15 | return this.documentTypes.index; 16 | } 17 | } 18 | 19 | declare module "@glint/environment-ember-loose/registry" { 20 | export default interface Registry { 21 | "New::DocumentTemplateList": typeof NewDocumentTemplateListComponent; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/app/components/notification.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { inject as service } from "@ember/service"; 3 | import HermesFlashMessagesService from "hermes/services/flash-messages"; 4 | 5 | export default class Notification extends Component { 6 | @service declare flashMessages: HermesFlashMessagesService; 7 | } 8 | 9 | declare module "@glint/environment-ember-loose/registry" { 10 | export default interface Registry { 11 | Notification: typeof Notification; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /web/app/components/overflow-menu.ts: -------------------------------------------------------------------------------- 1 | import { OffsetOptions } from "@floating-ui/dom"; 2 | import Component from "@glimmer/component"; 3 | 4 | export interface OverflowItem { 5 | label: string; 6 | icon: string; 7 | action: any; 8 | } 9 | 10 | interface OverflowMenuComponentSignature { 11 | Element: HTMLDivElement; 12 | Args: { 13 | items: Record; 14 | offset?: OffsetOptions; 15 | isShown?: boolean; 16 | renderOut?: boolean; 17 | }; 18 | } 19 | 20 | export default class OverflowMenuComponent extends Component {} 21 | 22 | declare module "@glint/environment-ember-loose/registry" { 23 | export default interface Registry { 24 | OverflowMenu: typeof OverflowMenuComponent; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /web/app/components/pagination/link.hbs: -------------------------------------------------------------------------------- 1 | {{#if @disabled}} 2 | 6 | {{#if @icon}} 7 | 8 | {{else}} 9 | {{@page}} 10 |
    13 | {{/if}} 14 |
    15 | {{else}} 16 | 21 | {{#if @icon}} 22 | 23 | {{else}} 24 | {{@page}} 25 | {{/if}} 26 | 27 | {{/if}} 28 | -------------------------------------------------------------------------------- /web/app/components/pagination/link.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | interface PaginationLinkComponentSignature { 4 | Element: HTMLAnchorElement; 5 | Args: { 6 | disabled?: boolean; 7 | icon?: string; 8 | page?: number; 9 | }; 10 | } 11 | 12 | export default class PaginationLinkComponent extends Component {} 13 | 14 | declare module "@glint/environment-ember-loose/registry" { 15 | export default interface Registry { 16 | "Pagination::Link": typeof PaginationLinkComponent; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /web/app/components/person/index.hbs: -------------------------------------------------------------------------------- 1 | {{#unless this.isHidden}} 2 |
    3 |
    4 | 5 | {{#if this.badgeIsShown}} 6 |
    11 | 16 |
    17 | {{/if}} 18 |
    19 |
    24 | {{this.label}} 25 |
    26 |
    27 | {{/unless}} 28 | -------------------------------------------------------------------------------- /web/app/components/product-area/index.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { HITS_PER_PAGE } from "hermes/services/algolia"; 3 | import { HermesDocument } from "hermes/types/document"; 4 | 5 | interface ProductAreaIndexComponentSignature { 6 | Args: { 7 | productArea: string; 8 | docs: HermesDocument[]; 9 | nbHits: number; 10 | }; 11 | } 12 | 13 | export default class ProductAreaIndexComponent extends Component { 14 | protected get seeMoreButtonIsShown() { 15 | return this.args.nbHits > HITS_PER_PAGE; 16 | } 17 | } 18 | 19 | declare module "@glint/environment-ember-loose/registry" { 20 | export default interface Registry { 21 | ProductArea: typeof ProductAreaIndexComponent; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/app/components/product-link.hbs: -------------------------------------------------------------------------------- 1 | 7 | {{#if (has-block "default")}} 8 | {{yield}} 9 | {{else}} 10 | 15 | {{/if}} 16 | 17 | -------------------------------------------------------------------------------- /web/app/components/project/resource-empty-state.gts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | interface ProjectResourceEmptyStateComponentSignature {} 4 | 5 | export default class ProjectResourceEmptyStateComponent extends Component { 6 | 16 | } 17 | 18 | declare module "@glint/environment-ember-loose/registry" { 19 | export default interface Registry { 20 | "Project::ResourceEmptyState": typeof ProjectResourceEmptyStateComponent; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /web/app/components/related-resource/external-link.gts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { RelatedExternalLink, RelatedResource } from "../related-resources"; 3 | import { assert } from "@ember/debug"; 4 | 5 | interface RelatedResourceExternalLinkComponentSignature { 6 | Args: { 7 | resource: RelatedResource; 8 | }; 9 | Blocks: { 10 | default: [RelatedExternalLink]; 11 | }; 12 | } 13 | 14 | export default class RelatedResourceExternalLinkComponent extends Component { 15 | protected get link(): RelatedExternalLink { 16 | assert("url must exist on the resource", "url" in this.args.resource); 17 | return this.args.resource as RelatedExternalLink; 18 | } 19 | 20 | 23 | } 24 | 25 | declare module "@glint/environment-ember-loose/registry" { 26 | export default interface Registry { 27 | "RelatedResource::ExternalLink": typeof RelatedResourceExternalLinkComponent; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /web/app/components/related-resources/add/document.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { HermesDocument } from "hermes/types/document"; 3 | 4 | interface RelatedResourcesAddDocumentComponentSignature { 5 | Args: { 6 | document: HermesDocument; 7 | }; 8 | Blocks: { 9 | default: []; 10 | }; 11 | } 12 | 13 | export default class RelatedResourcesAddDocumentComponent extends Component {} 14 | 15 | declare module "@glint/environment-ember-loose/registry" { 16 | export default interface Registry { 17 | "RelatedResources::Add::Document": typeof RelatedResourcesAddDocumentComponent; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /web/app/components/results/nav.hbs: -------------------------------------------------------------------------------- 1 |
    6 | 12 | Projects 13 | 14 | 15 | 21 | Docs 22 | 23 | 24 | 30 | Top 31 | 32 |
    33 | -------------------------------------------------------------------------------- /web/app/components/results/section-header.hbs: -------------------------------------------------------------------------------- 1 | {{#if @query}} 2 | 7 |

    8 | {{@text}} 9 |

    10 | 11 | 15 |
    16 | {{else}} 17 |
    18 |

    19 | {{@text}} 20 |

    21 | 22 |
    23 | {{/if}} 24 | -------------------------------------------------------------------------------- /web/app/components/settings/subscription-list-item.hbs: -------------------------------------------------------------------------------- 1 |
  • 5 | 11 | 12 |
    16 | {{@productArea}} 17 |
    18 |
    19 | 20 |
  • 21 | -------------------------------------------------------------------------------- /web/app/components/settings/subscription-list-item.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | interface SettingsSubscriptionListItemComponentSignature { 4 | Args: { 5 | productArea: string; 6 | }; 7 | } 8 | 9 | export default class SettingsSubscriptionListItemComponent extends Component {} 10 | 11 | declare module "@glint/environment-ember-loose/registry" { 12 | export default interface Registry { 13 | "Settings::SubscriptionListItem": typeof SettingsSubscriptionListItemComponent; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /web/app/components/settings/subscription-list.hbs: -------------------------------------------------------------------------------- 1 | 9 |
      15 | {{#each this.shownItems as |listItem|}} 16 | 17 | {{/each}} 18 |
    19 | -------------------------------------------------------------------------------- /web/app/components/table/sortable-header.hbs: -------------------------------------------------------------------------------- 1 | 10 |
    11 | 12 |
    13 | {{yield}} 14 |
    15 | -------------------------------------------------------------------------------- /web/app/components/tooltip-icon.hbs: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /web/app/components/tooltip-icon.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | interface TooltipIconComponentSignature { 4 | Element: HTMLSpanElement; 5 | Args: { 6 | text: string; 7 | icon?: string; 8 | }; 9 | } 10 | 11 | export default class TooltipIconComponent extends Component { 12 | protected get icon(): string { 13 | return this.args.icon ?? "help"; 14 | } 15 | } 16 | 17 | declare module "@glint/environment-ember-loose/registry" { 18 | export default interface Registry { 19 | TooltipIcon: typeof TooltipIconComponent; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /web/app/components/truncated-text.hbs: -------------------------------------------------------------------------------- 1 | {{! 2 | Truncates a single line of text with a small, gray, regularly weighted ellipsis. 3 | Reduces the visual emphasis of ellipses when working with medium-sized, dark, bold text. 4 | Not recommended for display type in its current configuration. 5 | }} 6 | 7 | {{! @glint-ignore - element helper not yet typed }} 8 | 9 | {{#let (element (or @tagName "p")) as |Container|}} 10 | 11 | 12 | {{yield}} 13 | 14 | 15 | {{/let}} 16 | -------------------------------------------------------------------------------- /web/app/components/truncated-text.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | interface TruncatedTextComponentSignature { 4 | Element: HTMLSpanElement; 5 | Args: { 6 | tagName?: string; 7 | startingBreakpoint?: "md"; 8 | }; 9 | Blocks: { 10 | default: []; 11 | }; 12 | } 13 | 14 | export default class TruncatedTextComponent extends Component { 15 | protected get class(): string { 16 | if (this.args.startingBreakpoint === "md") { 17 | return "starting-breakpoint-md"; 18 | } 19 | return "default"; 20 | } 21 | } 22 | 23 | declare module "@glint/environment-ember-loose/registry" { 24 | export default interface Registry { 25 | TruncatedText: typeof TruncatedTextComponent; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /web/app/components/type-to-confirm.hbs: -------------------------------------------------------------------------------- 1 | {{yield 2 | (hash 3 | Input=(component 4 | "type-to-confirm/input" 5 | id=this.id 6 | value=@value 7 | inputValue=this.inputValue 8 | onInput=this.onInput 9 | onKeydown=this.onKeydown 10 | ) 11 | hasConfirmed=this.hasConfirmed 12 | ) 13 | }} 14 | -------------------------------------------------------------------------------- /web/app/components/type-to-confirm/input.hbs: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /web/app/components/type-to-confirm/input.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | interface TypeToConfirmInputSignature { 4 | Element: HTMLInputElement; 5 | Args: { 6 | onInput: (event: Event) => void; 7 | onKeydown: (event: KeyboardEvent) => void; 8 | inputValue: string; 9 | value: string; 10 | id: string; 11 | }; 12 | Blocks: { 13 | default: []; 14 | }; 15 | } 16 | 17 | export default class TypeToConfirmInput extends Component {} 18 | 19 | declare module "@glint/environment-ember-loose/registry" { 20 | export default interface Registry { 21 | // Only validate invocation via the `component` helper 22 | "type-to-confirm/input": typeof TypeToConfirmInput; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /web/app/components/whats-a-project.hbs: -------------------------------------------------------------------------------- 1 |
    11 |
    12 | Whatʼs a project? 13 |
    14 |
    15 | -------------------------------------------------------------------------------- /web/app/components/whats-a-project.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | 3 | interface WhatsAProjectComponentSignature { 4 | Element: HTMLDivElement; 5 | } 6 | 7 | export default class WhatsAProjectComponent extends Component {} 8 | 9 | declare module "@glint/environment-ember-loose/registry" { 10 | export default interface Registry { 11 | WhatsAProject: typeof WhatsAProjectComponent; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /web/app/components/x/dropdown-list/_shared.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Used by Action and LinkTo 3 | */ 4 | export type XDropdownListInteractiveComponentArgs = { 5 | registerElement: (e: HTMLElement) => void; 6 | focusMouseTarget: (e: MouseEvent) => void; 7 | onClick: () => void; 8 | disabled?: boolean; 9 | role: string; 10 | isAriaSelected: boolean; 11 | isAriaChecked: boolean; 12 | }; 13 | 14 | /** 15 | * Used by Index and Items 16 | */ 17 | export interface XDropdownListSharedArgs { 18 | items?: any; 19 | selected?: any; 20 | listIsOrdered?: boolean; 21 | } 22 | 23 | /** 24 | * Used by ToggleAction, ToggleSelect and ToggleButton 25 | */ 26 | export interface XDropdownListToggleComponentArgs { 27 | registerAnchor: (e: HTMLElement) => void; 28 | onTriggerKeydown: (e: KeyboardEvent) => void; 29 | toggleContent: () => void; 30 | contentIsShown: boolean; 31 | ariaControls: string; 32 | disabled?: boolean; 33 | } 34 | -------------------------------------------------------------------------------- /web/app/components/x/dropdown-list/action.hbs: -------------------------------------------------------------------------------- 1 | 12 | {{yield}} 13 | 14 | -------------------------------------------------------------------------------- /web/app/components/x/dropdown-list/action.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { XDropdownListInteractiveComponentArgs } from "./_shared"; 3 | 4 | interface XDropdownListActionComponentSignature { 5 | Element: HTMLButtonElement; 6 | Args: XDropdownListInteractiveComponentArgs; 7 | Blocks: { 8 | default: []; 9 | }; 10 | } 11 | 12 | export default class XDropdownListActionComponent extends Component {} 13 | 14 | declare module "@glint/environment-ember-loose/registry" { 15 | export default interface Registry { 16 | "x/dropdown-list/action": typeof XDropdownListActionComponent; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /web/app/components/x/dropdown-list/external-link.hbs: -------------------------------------------------------------------------------- 1 | 14 | {{yield}} 15 | 16 | -------------------------------------------------------------------------------- /web/app/components/x/dropdown-list/external-link.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { XDropdownListInteractiveComponentArgs } from "./_shared"; 3 | 4 | interface XDropdownListExternalLinkComponentSignature { 5 | Element: HTMLAnchorElement; 6 | Args: XDropdownListInteractiveComponentArgs & { 7 | iconIsShown?: boolean; 8 | }; 9 | Blocks: { 10 | default: []; 11 | }; 12 | } 13 | 14 | export default class XDropdownListExternalLinkComponent extends Component {} 15 | 16 | declare module "@glint/environment-ember-loose/registry" { 17 | export default interface Registry { 18 | "x/dropdown-list/external-link": typeof XDropdownListExternalLinkComponent; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /web/app/components/x/dropdown-list/link-to.hbs: -------------------------------------------------------------------------------- 1 | 16 | {{yield}} 17 | 18 | -------------------------------------------------------------------------------- /web/app/components/x/dropdown-list/toggle-action.hbs: -------------------------------------------------------------------------------- 1 | 15 | {{yield}} 16 | {{#if @hasChevron}} 17 | 21 | {{/if}} 22 | 23 | -------------------------------------------------------------------------------- /web/app/components/x/dropdown-list/toggle-action.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { XDropdownListToggleComponentArgs } from "./_shared"; 3 | 4 | interface XDropdownListToggleActionComponentSignature { 5 | Element: HTMLButtonElement; 6 | Args: XDropdownListToggleComponentArgs & { 7 | hasChevron?: boolean; 8 | }; 9 | Blocks: { 10 | default: []; 11 | }; 12 | } 13 | 14 | export default class XDropdownListToggleActionComponent extends Component {} 15 | 16 | declare module "@glint/environment-ember-loose/registry" { 17 | export default interface Registry { 18 | "x/dropdown-list/toggle-action": typeof XDropdownListToggleActionComponent; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /web/app/components/x/dropdown-list/toggle-button.hbs: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /web/app/components/x/dropdown-list/toggle-button.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { HdsButtonColor } from "hds/_shared"; 3 | import { XDropdownListToggleComponentArgs } from "./_shared"; 4 | 5 | interface XDropdownListToggleButtonComponentSignature { 6 | Element: HTMLButtonElement; 7 | Args: XDropdownListToggleComponentArgs & { 8 | color: HdsButtonColor; 9 | text: string; 10 | }; 11 | Blocks: { 12 | default: []; 13 | }; 14 | } 15 | 16 | export default class XDropdownListToggleButtonComponent extends Component {} 17 | 18 | declare module "@glint/environment-ember-loose/registry" { 19 | export default interface Registry { 20 | "x/dropdown-list/toggle-button": typeof XDropdownListToggleButtonComponent; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /web/app/components/x/dropdown-list/toggle-select.hbs: -------------------------------------------------------------------------------- 1 | 13 | {{yield}} 14 | 19 | 20 | -------------------------------------------------------------------------------- /web/app/components/x/dropdown-list/toggle-select.ts: -------------------------------------------------------------------------------- 1 | import Component from "@glimmer/component"; 2 | import { XDropdownListToggleComponentArgs } from "./_shared"; 3 | 4 | interface XDropdownListToggleSelectComponentSignature { 5 | Element: HTMLButtonElement; 6 | Args: XDropdownListToggleComponentArgs; 7 | Blocks: { 8 | default: []; 9 | }; 10 | } 11 | 12 | export default class XDropdownListToggleSelectComponent extends Component {} 13 | 14 | declare module "@glint/environment-ember-loose/registry" { 15 | export default interface Registry { 16 | "x/dropdown-list/toggle-select": typeof XDropdownListToggleSelectComponent; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /web/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-forge/hermes/caa1b990d237b39a99643f84ec5cbdbc6e086f2b/web/app/controllers/.gitkeep -------------------------------------------------------------------------------- /web/app/controllers/404.ts: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import parseDate from "hermes/utils/parse-date"; 3 | 4 | export default class Error404Controller extends Controller { 5 | get currentDate() { 6 | return parseDate(new Date(), "long"); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /web/app/controllers/application.ts: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import config from "hermes/config/environment"; 3 | 4 | export default class ApplicationController extends Controller { 5 | protected get animatedToolsAreShown() { 6 | if (config.environment === "development") { 7 | return config.showEmberAnimatedTools; 8 | } else { 9 | return false; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /web/app/controllers/authenticate.ts: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import { inject as service } from "@ember/service"; 3 | import SessionService from "hermes/services/session"; 4 | import { dropTask } from "ember-concurrency"; 5 | 6 | export default class AuthenticateController extends Controller { 7 | @service declare session: SessionService; 8 | 9 | protected get currentYear(): number { 10 | return new Date().getFullYear(); 11 | } 12 | 13 | protected authenticate = dropTask(async () => { 14 | await this.session.authenticate( 15 | "authenticator:torii", 16 | "google-oauth2-bearer" 17 | ); 18 | }); 19 | } 20 | declare module "@ember/controller" { 21 | interface Registry { 22 | authenticate: AuthenticateController; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /web/app/controllers/authenticated.ts: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import RouterService from "@ember/routing/router-service"; 3 | import { inject as service } from "@ember/service"; 4 | import { tracked } from "@glimmer/tracking"; 5 | 6 | export default class AuthenticatedController extends Controller { 7 | @service declare router: RouterService; 8 | 9 | /** 10 | * The current search query. Set in the route's `beforeModel` hook 11 | * when transitioning to the `results` route with a query. 12 | * Used to populate the search input when dry-loading the results route. 13 | */ 14 | @tracked query: string | undefined; 15 | 16 | protected get standardTemplateIsShown() { 17 | const routeName = this.router.currentRouteName; 18 | return routeName !== "authenticated.document" && routeName !== "404"; 19 | } 20 | } 21 | 22 | declare module "@ember/controller" { 23 | interface Registry { 24 | authenticated: AuthenticatedController; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /web/app/controllers/authenticated/all.ts: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | 3 | /** 4 | * This allows queryParams to be captured and passed 5 | * to the `/documents` route on redirect. 6 | */ 7 | 8 | export default class AuthenticatedAllController extends Controller { 9 | queryParams = ["docType", "owners", "page", "product", "sortBy", "status"]; 10 | docType = []; 11 | owners = []; 12 | page = 1; 13 | product = []; 14 | sortBy = "dateDesc"; 15 | status = []; 16 | } 17 | -------------------------------------------------------------------------------- /web/app/controllers/authenticated/dashboard.ts: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import { HermesDocument } from "hermes/types/document"; 3 | 4 | export default class AuthenticatedDashboardController extends Controller { 5 | declare model: HermesDocument[]; 6 | } 7 | -------------------------------------------------------------------------------- /web/app/controllers/authenticated/document.ts: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import { tracked } from "@glimmer/tracking"; 3 | import AuthenticatedDocumentRoute from "hermes/routes/authenticated/document"; 4 | import { ModelFrom } from "hermes/types/route-models"; 5 | 6 | export default class AuthenticatedDocumentController extends Controller { 7 | declare model: ModelFrom; 8 | 9 | queryParams = ["draft"]; 10 | draft = false; 11 | 12 | /** 13 | * Whether the model is loading a new document from another one, 14 | * as is when loading a related Hermes document. 15 | * Used conditionally by the document `afterModel` to toggle 16 | * sidebar visibility, resetting its local state to reflect 17 | * the new model data. 18 | */ 19 | @tracked modelIsChanging = false; 20 | } 21 | 22 | declare module "@ember/controller" { 23 | interface Registry { 24 | "authenticated.document": AuthenticatedDocumentController; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /web/app/controllers/authenticated/my/documents.ts: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import { SortByValue } from "hermes/components/header/toolbar"; 3 | import { SortDirection } from "hermes/components/table/sortable-header"; 4 | import AuthenticatedMyDocumentsRoute from "hermes/routes/authenticated/my/documents"; 5 | import { ModelFrom } from "hermes/types/route-models"; 6 | 7 | export default class AuthenticatedMyDocumentsController extends Controller { 8 | queryParams = ["includeSharedDrafts", "page", "sortBy"]; 9 | includeSharedDrafts = true; 10 | page = 1; 11 | sortBy = SortByValue.DateDesc; 12 | 13 | declare model: ModelFrom; 14 | 15 | get sortDirection() { 16 | switch (this.model.sortedBy) { 17 | case SortByValue.DateAsc: 18 | return SortDirection.Asc; 19 | default: 20 | return SortDirection.Desc; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/app/controllers/authenticated/new/doc.ts: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import AuthenticatedNewDocRoute from "hermes/routes/authenticated/new/doc"; 3 | import { ModelFrom } from "hermes/types/route-models"; 4 | 5 | export default class AuthenticatedNewDocController extends Controller { 6 | queryParams = ["docType"]; 7 | 8 | declare model: ModelFrom; 9 | } 10 | -------------------------------------------------------------------------------- /web/app/controllers/authenticated/product-areas/product-area.ts: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import AuthenticatedProductAreasProductAreaRoute from "hermes/routes/authenticated/product-areas/product-area"; 3 | import { ModelFrom } from "hermes/types/route-models"; 4 | 5 | export default class AuthenticatedProductAreasProductAreaController extends Controller { 6 | queryParams = ["product"]; 7 | product = []; 8 | 9 | declare model: ModelFrom; 10 | } 11 | -------------------------------------------------------------------------------- /web/app/controllers/authenticated/projects/index.ts: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import AuthenticatedProjectsIndexRoute from "hermes/routes/authenticated/projects/index"; 3 | import { ProjectStatus } from "hermes/types/project-status"; 4 | import { ModelFrom } from "hermes/types/route-models"; 5 | 6 | export default class AuthenticatedProjectsController extends Controller { 7 | queryParams = ["status", "page"]; 8 | 9 | /** 10 | * Because every Projects view is filtered, we set a param 11 | * so that the default tab's URL is "/projects" and not 12 | * "/projects?status=active" 13 | */ 14 | status = ProjectStatus.Active; 15 | 16 | /** 17 | * The current page. 1-indexed. 18 | */ 19 | page = 1; 20 | 21 | declare model: ModelFrom; 22 | } 23 | -------------------------------------------------------------------------------- /web/app/controllers/authenticated/projects/project.ts: -------------------------------------------------------------------------------- 1 | import Controller from "@ember/controller"; 2 | import { tracked } from "@glimmer/tracking"; 3 | import AuthenticatedProjectsProjectRoute from "hermes/routes/authenticated/projects/project"; 4 | import { ModelFrom } from "hermes/types/route-models"; 5 | 6 | export default class AuthenticatedProjectsProjectController extends Controller { 7 | declare model: ModelFrom; 8 | 9 | /** 10 | * Whether a new project has loaded from the project route, 11 | * as is the case when clicking a project from the search popover 12 | * while already viewing a project. In these cases, the model 13 | * will set `newModelHasLoaded` true to trigger a rerender. 14 | * Always set false in the `afterModel` hook. 15 | */ 16 | @tracked newModelHasLoaded = false; 17 | } 18 | 19 | declare module "@ember/controller" { 20 | interface Registry { 21 | "authenticated.projects.project": AuthenticatedProjectsProjectController; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-forge/hermes/caa1b990d237b39a99643f84ec5cbdbc6e086f2b/web/app/helpers/.gitkeep -------------------------------------------------------------------------------- /web/app/helpers/_dasherize.ts: -------------------------------------------------------------------------------- 1 | import { helper } from "@ember/component/helper"; 2 | import { dasherize } from "@ember/string"; 3 | 4 | export interface DasherizeHelperSignature { 5 | Args: { 6 | Positional: [value: string | undefined]; 7 | }; 8 | Return: string; 9 | } 10 | 11 | const dasherizeHelper = helper( 12 | ([value]: [string | undefined]) => { 13 | if (typeof value === "string") { 14 | return dasherize(value); 15 | } 16 | return ""; 17 | } 18 | ); 19 | 20 | export default dasherizeHelper; 21 | 22 | declare module "@glint/environment-ember-loose/registry" { 23 | export default interface Registry { 24 | dasherize: typeof dasherizeHelper; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /web/app/helpers/_lowercase.ts: -------------------------------------------------------------------------------- 1 | import { helper } from "@ember/component/helper"; 2 | 3 | export interface LowercaseHelperSignature { 4 | Args: { 5 | Positional: [value: string | number | boolean | undefined]; 6 | }; 7 | Return: string; 8 | } 9 | 10 | const lowercaseHelper = helper( 11 | ([value]: [string | number | boolean | undefined]) => { 12 | return value?.toString().toLowerCase() ?? ""; 13 | } 14 | ); 15 | 16 | export default lowercaseHelper; 17 | 18 | declare module "@glint/environment-ember-loose/registry" { 19 | export default interface Registry { 20 | lowercase: typeof lowercaseHelper; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /web/app/helpers/add.ts: -------------------------------------------------------------------------------- 1 | import { helper } from "@ember/component/helper"; 2 | 3 | interface AddHelperSignature { 4 | Args: { 5 | Positional: [first: string | number, second: string | number]; 6 | }; 7 | Return: number; 8 | } 9 | 10 | const addHelper = helper( 11 | ([first, second]: [string | number, string | number]) => { 12 | let firstInteger = 0; 13 | let secondInteger = 0; 14 | 15 | if (typeof first === "number") { 16 | firstInteger = first; 17 | } else { 18 | firstInteger = parseInt(first); 19 | } 20 | 21 | if (typeof second === "number") { 22 | secondInteger = second; 23 | } else { 24 | secondInteger = parseInt(second); 25 | } 26 | return firstInteger + secondInteger; 27 | } 28 | ); 29 | 30 | export default addHelper; 31 | 32 | declare module "@glint/environment-ember-loose/registry" { 33 | export default interface Registry { 34 | add: typeof addHelper; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /web/app/helpers/dasherize.js: -------------------------------------------------------------------------------- 1 | export { default } from "./_dasherize"; 2 | export * from "./_dasherize"; 3 | -------------------------------------------------------------------------------- /web/app/helpers/get-model-attr.ts: -------------------------------------------------------------------------------- 1 | import Helper from "@ember/component/helper"; 2 | import { inject as service } from "@ember/service"; 3 | import StoreService from "hermes/services/store"; 4 | import getModelAttr, { GetModelAttrArgs } from "hermes/utils/get-model-attr"; 5 | 6 | export interface GetModelAttrSignature { 7 | Args: { 8 | Positional: GetModelAttrArgs; 9 | Named: { fallback?: string }; 10 | }; 11 | Return: any; 12 | } 13 | 14 | export default class GetModelAttrHelper extends Helper { 15 | @service declare store: StoreService; 16 | 17 | compute(positional: GetModelAttrArgs, named: { fallback?: string }) { 18 | return getModelAttr(this.store, positional, named); 19 | } 20 | } 21 | 22 | declare module "@glint/environment-ember-loose/registry" { 23 | export default interface Registry { 24 | "get-model-attr": typeof GetModelAttrHelper; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /web/app/helpers/get-owner-query.ts: -------------------------------------------------------------------------------- 1 | import Helper from "@ember/component/helper"; 2 | import { DEFAULT_FILTERS } from "hermes/services/active-filters"; 3 | 4 | interface GetOwnerQueryHelperSignature { 5 | Args: { 6 | Positional: [email: string | undefined]; 7 | }; 8 | Return: Record; 9 | } 10 | /** 11 | * Generates a query hash filtering to a specific owner. 12 | * Used in templates to make owners clickable. 13 | */ 14 | export default class GetOwnerQueryHelper extends Helper { 15 | compute(positional: GetOwnerQueryHelperSignature["Args"]["Positional"]) { 16 | const owner = positional[0]; 17 | if (owner) { 18 | return { 19 | ...DEFAULT_FILTERS, 20 | owners: [owner], 21 | page: 1, 22 | }; 23 | } else { 24 | return {}; 25 | } 26 | } 27 | } 28 | 29 | declare module "@glint/environment-ember-loose/registry" { 30 | export default interface Registry { 31 | "get-owner-query": typeof GetOwnerQueryHelper; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /web/app/helpers/get-product-id.ts: -------------------------------------------------------------------------------- 1 | import { helper } from "@ember/component/helper"; 2 | import getProductId from "hermes/utils/get-product-id"; 3 | 4 | export interface GetProductIDSignature { 5 | Args: { 6 | Positional: [string | undefined]; 7 | }; 8 | Return: string | undefined; 9 | } 10 | 11 | const getProductIDHelper = helper(([productName]) => { 12 | if (!productName) { 13 | return; 14 | } 15 | return getProductId(productName); 16 | }); 17 | 18 | export default getProductIDHelper; 19 | 20 | declare module "@glint/environment-ember-loose/registry" { 21 | export default interface Registry { 22 | "get-product-id": typeof getProductIDHelper; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /web/app/helpers/get-product-label.ts: -------------------------------------------------------------------------------- 1 | import { helper } from "@ember/component/helper"; 2 | import getProductLabel from "hermes/utils/get-product-label"; 3 | 4 | export interface GetProductLabelSignature { 5 | Args: { 6 | Positional: [string | undefined]; 7 | }; 8 | Return: string; 9 | } 10 | 11 | const getProductLabelHelper = helper(([product]) => { 12 | return getProductLabel(product); 13 | }); 14 | 15 | export default getProductLabelHelper; 16 | 17 | declare module "@glint/environment-ember-loose/registry" { 18 | export default interface Registry { 19 | "get-product-label": typeof getProductLabelHelper; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /web/app/helpers/html-element.ts: -------------------------------------------------------------------------------- 1 | import { helper } from "@ember/component/helper"; 2 | import htmlElement from "hermes/utils/html-element"; 3 | 4 | interface HtmlElementHelperSignature { 5 | Args: { 6 | Positional: [selector: string]; 7 | }; 8 | Return: HTMLElement; 9 | } 10 | 11 | const htmlElementHelper = helper( 12 | ([selector]: [string]) => { 13 | return htmlElement(selector); 14 | } 15 | ); 16 | 17 | export default htmlElementHelper; 18 | 19 | declare module "@glint/environment-ember-loose/registry" { 20 | export default interface Registry { 21 | "html-element": typeof htmlElementHelper; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/app/helpers/is-active-filter.ts: -------------------------------------------------------------------------------- 1 | import Helper from "@ember/component/helper"; 2 | import { inject as service } from "@ember/service"; 3 | import ActiveFiltersService from "hermes/services/active-filters"; 4 | 5 | export interface IsActiveFilterSignature { 6 | Args: { 7 | Positional: [string]; 8 | }; 9 | Return: boolean; 10 | } 11 | 12 | export default class IsActiveFilterHelper extends Helper { 13 | @service declare activeFilters: ActiveFiltersService; 14 | 15 | compute([positional]: IsActiveFilterSignature["Args"]["Positional"]) { 16 | const activeFilters = Object.values(this.activeFilters.index).flat(); 17 | return activeFilters.some((values) => values.includes(positional)); 18 | } 19 | } 20 | 21 | declare module "@glint/environment-ember-loose/registry" { 22 | export default interface Registry { 23 | "is-active-filter": typeof IsActiveFilterHelper; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /web/app/helpers/lowercase.js: -------------------------------------------------------------------------------- 1 | export { default } from "./_lowercase"; 2 | export * from "./_lowercase"; 3 | -------------------------------------------------------------------------------- /web/app/helpers/maybe-query.ts: -------------------------------------------------------------------------------- 1 | import { helper } from "@ember/component/helper"; 2 | 3 | export interface MaybeQuerySignature { 4 | Args: { 5 | Positional: [Record | undefined]; 6 | }; 7 | Return: Record | {}; 8 | } 9 | 10 | /** 11 | * Supplies an empty object if no is query provided. 12 | * Avoids errors when passing empty `@query` values to LinkTos. 13 | * Workaround for https://github.com/emberjs/ember.js/issues/19693 14 | * Can be removed when we upgrade to Ember 4.0. 15 | */ 16 | const maybeQueryHelper = helper( 17 | ([query]: [unknown | undefined]) => { 18 | return query ? query : {}; 19 | } 20 | ); 21 | 22 | export default maybeQueryHelper; 23 | 24 | declare module "@glint/environment-ember-loose/registry" { 25 | export default interface Registry { 26 | "maybe-query": typeof maybeQueryHelper; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /web/app/helpers/parse-date.ts: -------------------------------------------------------------------------------- 1 | import { helper } from "@ember/component/helper"; 2 | import parseDate from "hermes/utils/parse-date"; 3 | export interface ParseDateHelperSignature { 4 | Args: { 5 | Positional: [ 6 | time: string | number | Date | undefined, 7 | monthFormat?: "short" | "long" 8 | ]; 9 | Return: Date | undefined; 10 | }; 11 | } 12 | 13 | const parseDateHelper = helper( 14 | ([time, monthFormat = "short"]) => { 15 | return parseDate(time, monthFormat); 16 | } 17 | ); 18 | 19 | export default parseDateHelper; 20 | 21 | declare module "@glint/environment-ember-loose/registry" { 22 | export default interface Registry { 23 | "parse-date": typeof parseDateHelper; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /web/app/helpers/time-ago.ts: -------------------------------------------------------------------------------- 1 | import { helper } from "@ember/component/helper"; 2 | import timeAgo from "hermes/utils/time-ago"; 3 | 4 | export interface TimeAgoHelperSignature { 5 | Args: { 6 | Positional: [time?: number]; 7 | Named: { 8 | limitTo24Hours?: boolean; 9 | }; 10 | }; 11 | Return: string | null; 12 | } 13 | 14 | const timeAgoHelper = helper( 15 | ([time], { limitTo24Hours }) => { 16 | return timeAgo(time, { limitTo24Hours }); 17 | }, 18 | ); 19 | 20 | export default timeAgoHelper; 21 | 22 | declare module "@glint/environment-ember-loose/registry" { 23 | export default interface Registry { 24 | "time-ago": typeof timeAgoHelper; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /web/app/helpers/uid.js: -------------------------------------------------------------------------------- 1 | import { helper } from "@ember/component/helper"; 2 | import { guidFor } from "@ember/object/internals"; 3 | 4 | /* 5 | * Returns a unique id that contains the provided label and guid from the salt 6 | * 7 | * @salt: An object to generate a salt value from (using guidFor) 8 | * @label: A human-readable label 9 | * 10 | * @example 11 | * {{uid this "title"}} 12 | * 13 | * "title-ember123912" 14 | */ 15 | export default helper(([salt, label]) => { 16 | return `${label}-${guidFor(salt)}`; 17 | }); 18 | -------------------------------------------------------------------------------- /web/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hermes 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {{content-for "head-footer"}} 21 | 22 | 23 | 24 | {{content-for "body"}} 25 | 26 | 27 | 28 | 29 | {{content-for "body-footer"}} 30 | 31 | 32 | -------------------------------------------------------------------------------- /web/app/initializers/custom-inflector-rules.ts: -------------------------------------------------------------------------------- 1 | import Inflector from "ember-inflector"; 2 | 3 | export function initialize() { 4 | const inflector = Inflector.inflector; 5 | 6 | // Turn off pluralization. 7 | inflector.uncountable("document"); 8 | inflector.uncountable("me"); 9 | } 10 | 11 | export default { 12 | name: "custom-inflector-rules", 13 | initialize, 14 | }; 15 | -------------------------------------------------------------------------------- /web/app/metrics-adapters/google-analytics-four.js: -------------------------------------------------------------------------------- 1 | export { default } from "./_google-analytics-four"; 2 | export * from "./_google-analytics-four"; 3 | -------------------------------------------------------------------------------- /web/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-forge/hermes/caa1b990d237b39a99643f84ec5cbdbc6e086f2b/web/app/models/.gitkeep -------------------------------------------------------------------------------- /web/app/models/group.ts: -------------------------------------------------------------------------------- 1 | import Model, { attr } from "@ember-data/model"; 2 | 3 | export default class GroupModel extends Model { 4 | /** 5 | * The name of the group, e.g., "Team Hermes" 6 | */ 7 | @attr declare name: string; 8 | 9 | /** 10 | * The group's email address, e.g., "team-hermes@hashicorp.com" 11 | */ 12 | @attr declare email: string; 13 | } 14 | -------------------------------------------------------------------------------- /web/app/models/me.ts: -------------------------------------------------------------------------------- 1 | import Model from "@ember-data/model"; 2 | 3 | export default class MeModel extends Model { 4 | /** 5 | * This will soon include a `hasMany` relationship to the Subscription model. 6 | */ 7 | } 8 | -------------------------------------------------------------------------------- /web/app/models/person.ts: -------------------------------------------------------------------------------- 1 | import Model, { attr } from "@ember-data/model"; 2 | 3 | export default class PersonModel extends Model { 4 | /** 5 | * The person's full name, e.g., "Jane Doe". 6 | */ 7 | @attr declare name: string; 8 | 9 | /** 10 | * The person's first name, e.g., "Jane". 11 | */ 12 | @attr declare firstName: string; 13 | 14 | /** 15 | * The person's email address, e.g., "jane.doe@hashicorp.com" 16 | */ 17 | @attr declare email: string; 18 | 19 | /** 20 | * The person's profile picture URL. 21 | */ 22 | @attr declare picture: string; 23 | } 24 | -------------------------------------------------------------------------------- /web/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-forge/hermes/caa1b990d237b39a99643f84ec5cbdbc6e086f2b/web/app/routes/.gitkeep -------------------------------------------------------------------------------- /web/app/routes/404.ts: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class Error404Route extends Route {} 4 | -------------------------------------------------------------------------------- /web/app/routes/authenticate.ts: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | import { inject as service } from "@ember/service"; 3 | import ConfigService from "hermes/services/config"; 4 | import RouterService from "@ember/routing/router-service"; 5 | import SessionService from "hermes/services/session"; 6 | 7 | export default class AuthenticateRoute extends Route { 8 | @service("config") declare configSvc: ConfigService; 9 | @service declare router: RouterService; 10 | @service declare session: SessionService; 11 | 12 | beforeModel() { 13 | /** 14 | * If we're skipping Google auth, redirect right away because this route 15 | * isn't useful. 16 | */ 17 | if (this.configSvc.config.skip_google_auth) { 18 | this.router.replaceWith("/"); 19 | } 20 | 21 | /** 22 | * Checks if the session is authenticated, 23 | * and if it is, transitions to the specified route 24 | */ 25 | this.session.prohibitAuthentication("/"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /web/app/routes/authenticated/all.ts: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | import { inject as service } from "@ember/service"; 3 | import RouterService from "@ember/routing/router-service"; 4 | 5 | export default class AuthenticatedDocumentsRoute extends Route { 6 | @service declare router: RouterService; 7 | 8 | beforeModel() { 9 | this.router.transitionTo("authenticated.documents"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /web/app/routes/authenticated/drafts.ts: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | import RouterService from "@ember/routing/router-service"; 3 | import Transition from "@ember/routing/transition"; 4 | import { inject as service } from "@ember/service"; 5 | 6 | export default class AuthenticatedDraftsRoute extends Route { 7 | @service declare router: RouterService; 8 | 9 | beforeModel(_transition: Transition) { 10 | void this.router.transitionTo("authenticated.my"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /web/app/routes/authenticated/index.js: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | import { inject as service } from "@ember/service"; 3 | 4 | export default class AuthenticatedIndexRoute extends Route { 5 | @service router; 6 | 7 | redirect() { 8 | this.router.replaceWith("authenticated.dashboard"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /web/app/routes/authenticated/my.ts: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | import RouterService from "@ember/routing/router-service"; 3 | import Transition from "@ember/routing/transition"; 4 | import { inject as service } from "@ember/service"; 5 | 6 | export default class AuthenticatedMyDocumentsRoute extends Route { 7 | @service declare router: RouterService; 8 | 9 | beforeModel(transition: Transition) { 10 | if (transition.to.name === "authenticated.my.index") { 11 | this.router.transitionTo("authenticated.my.documents"); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /web/app/routes/authenticated/new.ts: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | import { inject as service } from "@ember/service"; 3 | import DocumentTypesService from "hermes/services/document-types"; 4 | 5 | export default class AuthenticatedNewRoute extends Route { 6 | @service declare documentTypes: DocumentTypesService; 7 | 8 | async model() { 9 | if (!this.documentTypes.index) { 10 | await this.documentTypes.fetch.perform(); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /web/app/routes/authenticated/new/doc.ts: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | import { inject as service } from "@ember/service"; 3 | import ProductAreasService from "hermes/services/product-areas"; 4 | 5 | interface AuthenticatedNewDocRouteParams { 6 | docType: string; 7 | } 8 | 9 | export default class AuthenticatedNewDocRoute extends Route { 10 | @service declare productAreas: ProductAreasService; 11 | 12 | queryParams = { 13 | docType: { 14 | refreshModel: true, 15 | }, 16 | }; 17 | 18 | model(params: AuthenticatedNewDocRouteParams) { 19 | return params.docType; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /web/app/routes/authenticated/new/index.ts: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class AuthenticatedNewIndexRoute extends Route {} 4 | -------------------------------------------------------------------------------- /web/app/routes/authenticated/new/project.ts: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class AuthenticatedNewProjectRoute extends Route {} 4 | -------------------------------------------------------------------------------- /web/app/routes/authenticated/projects.ts: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | 3 | export default class AuthenticatedProjectsRoute extends Route {} 4 | -------------------------------------------------------------------------------- /web/app/routes/authenticated/settings.ts: -------------------------------------------------------------------------------- 1 | import Route from "@ember/routing/route"; 2 | import { inject as service } from "@ember/service"; 3 | import AuthenticatedUserService from "hermes/services/authenticated-user"; 4 | import ProductAreasService from "hermes/services/product-areas"; 5 | 6 | export default class SettingsRoute extends Route { 7 | @service declare authenticatedUser: AuthenticatedUserService; 8 | @service declare productAreas: ProductAreasService; 9 | 10 | async model(): Promise { 11 | /** 12 | * Make sure the user's subscriptions are loaded before rendering the page. 13 | */ 14 | await this.authenticatedUser.fetchSubscriptions.perform(); 15 | 16 | return Object.keys(this.productAreas.index).sort(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /web/app/serializers/application.ts: -------------------------------------------------------------------------------- 1 | import JSONSerializer from "@ember-data/serializer/json"; 2 | import DS from "ember-data"; 3 | 4 | export default class ApplicationSerializer extends JSONSerializer { 5 | /** 6 | * The default serializer for all models. 7 | * Formats the response to match the JSON spec. 8 | * Model-specific serializers should extend this class. 9 | */ 10 | normalizeResponse( 11 | _store: DS.Store, 12 | primaryModelClass: any, 13 | payload: any, 14 | _id: string | number, 15 | _requestType: string, 16 | ) { 17 | payload = { 18 | data: [ 19 | { 20 | id: payload.id, 21 | type: primaryModelClass.modelName, 22 | attributes: payload, 23 | }, 24 | ], 25 | }; 26 | 27 | return payload; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /web/app/serializers/jira-issue.ts: -------------------------------------------------------------------------------- 1 | import JSONSerializer from "@ember-data/serializer/json"; 2 | import DS from "ember-data"; 3 | import JiraIssueModel from "hermes/models/jira-issue"; 4 | 5 | export default class JiraIssueSerializer extends JSONSerializer { 6 | /** 7 | * The serializer for the JiraIssue model. 8 | * Formats responses to the JSON spec. 9 | */ 10 | normalizeResponse( 11 | _store: DS.Store, 12 | _primaryModelClass: any, 13 | payload: JiraIssueModel, 14 | _id: string | number, 15 | _requestType: string, 16 | ) { 17 | return { 18 | data: { 19 | id: payload.key, 20 | type: "jira-issue", 21 | attributes: payload, 22 | }, 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /web/app/services/flags.ts: -------------------------------------------------------------------------------- 1 | import Service, { inject as service } from "@ember/service"; 2 | import ConfigService from "./config"; 3 | 4 | export default class FlagsService extends Service { 5 | @service("config") declare configSvc: ConfigService; 6 | 7 | // get exampleFlag() { 8 | // return this.configSvc.config.feature_flags?.["example_feature"]; 9 | // } 10 | } 11 | -------------------------------------------------------------------------------- /web/app/services/flash-messages.d.ts: -------------------------------------------------------------------------------- 1 | import FlashMessageService, { 2 | FlashFunction, 3 | MessageOptions, 4 | } from "ember-cli-flash/services/flash-messages"; 5 | 6 | export default class HermesFlashMessagesService extends FlashMessageService { 7 | critical: FlashFunction; 8 | success: FlashFunction; 9 | } 10 | -------------------------------------------------------------------------------- /web/app/services/metrics.js: -------------------------------------------------------------------------------- 1 | export { default } from "./_metrics"; 2 | export * from "./_metrics"; 3 | -------------------------------------------------------------------------------- /web/app/services/session.d.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./_session"; 2 | export { REDIRECT_STORAGE_KEY, isJSON } from "./_session"; 3 | export * from "./_session"; 4 | -------------------------------------------------------------------------------- /web/app/services/session.js: -------------------------------------------------------------------------------- 1 | export { default } from "./_session"; 2 | export * from "./_session"; 3 | -------------------------------------------------------------------------------- /web/app/services/store.d.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./_store"; 2 | export * from "./_store"; 3 | -------------------------------------------------------------------------------- /web/app/services/store.js: -------------------------------------------------------------------------------- 1 | export { default } from "./_store"; 2 | export * from "./_store"; 3 | -------------------------------------------------------------------------------- /web/app/styles/body.scss: -------------------------------------------------------------------------------- 1 | .ember-application { 2 | @apply bg-color-page-primary; 3 | } 4 | 5 | .secondary-screen { 6 | @apply bg-color-page-faint; 7 | 8 | .ember-application { 9 | @apply bg-color-page-faint; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /web/app/styles/buttons.scss: -------------------------------------------------------------------------------- 1 | .quarternary-button { 2 | @apply flex items-center justify-center gap-1.5 rounded-button-md border border-transparent; 3 | 4 | &:hover:not(.disabled):not(:focus), 5 | &:focus, 6 | &.open { 7 | @apply border-color-border-strong bg-color-foreground-high-contrast outline-none; 8 | } 9 | 10 | &:focus, 11 | &:focus-visible { 12 | @apply border-color-focus-action-internal; 13 | 14 | &.disabled { 15 | @apply border-color-border-strong; 16 | 17 | &::before { 18 | @apply border-color-border-primary; 19 | } 20 | } 21 | 22 | &::before { 23 | content: ""; 24 | @apply absolute -top-1 -right-1 -bottom-1 -left-1 -z-10 rounded-[8px] border-[3px] border-color-focus-action-external; 25 | } 26 | } 27 | 28 | &[class=".rounded"]::before { 29 | @apply rounded-[6px]; 30 | } 31 | } 32 | 33 | .pill-button, 34 | .pill-button:focus::before { 35 | @apply rounded-l-full rounded-r-full; 36 | } 37 | -------------------------------------------------------------------------------- /web/app/styles/components/action.scss: -------------------------------------------------------------------------------- 1 | .action { 2 | @apply appearance-none font-sans p-0 border-0 bg-transparent text-left cursor-pointer; 3 | font-weight: inherit; 4 | } 5 | -------------------------------------------------------------------------------- /web/app/styles/components/doc/folder-affordance.scss: -------------------------------------------------------------------------------- 1 | .doc-thumbnail { 2 | .folder-affordance { 3 | @apply w-auto absolute scale-x-[-1] -left-1; 4 | 5 | // set border via currentColor 6 | @apply text-color-border-primary; 7 | 8 | // Allow strokes to show 9 | @apply overflow-visible; 10 | 11 | // Make it 100% tall plus 1px for each horizontal stroke 12 | @apply h-[calc(100%+2px)]; 13 | 14 | // Offset it by 1px so the horizontal strokes aren't visible 15 | @apply -top-[1px]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /web/app/styles/components/doc/status.scss: -------------------------------------------------------------------------------- 1 | .doc-status { 2 | .progress-bar { 3 | @apply h-0.5 rounded-full bg-color-palette-neutral-200; 4 | } 5 | 6 | &--wip { 7 | li:nth-child(1) { 8 | @apply bg-color-palette-blue-200; 9 | } 10 | } 11 | 12 | &--in-review { 13 | li:nth-child(1) { 14 | @apply bg-color-palette-purple-200 opacity-75; 15 | } 16 | li:nth-child(2) { 17 | @apply h-1 bg-color-palette-purple-200; 18 | } 19 | } 20 | 21 | &--approved { 22 | .progress-bar { 23 | @apply bg-color-palette-green-200 opacity-75; 24 | } 25 | li:nth-child(3) { 26 | @apply h-1 opacity-100; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /web/app/styles/components/floating-u-i/content.scss: -------------------------------------------------------------------------------- 1 | .hermes-floating-ui-content { 2 | &:not(.non-floating-content) { 3 | @apply absolute; 4 | 5 | /* These positioning styles are overwritten by FloatingUI, 6 | * but they ensure that the tooltip isn't added to the bottom of the page, 7 | * where it could cause a reflow. This is especially important because 8 | * the Google Docs iframe responds to layout changes and might 9 | * otherwise jitter when a tooltip opened. 10 | */ 11 | @apply top-0 left-0; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /web/app/styles/components/footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | @apply mt-12 mb-16 w-full text-body-200 text-color-foreground-faint; 3 | 4 | .footer-inner { 5 | @apply flex w-full items-center justify-between border-t border-t-color-border-primary pt-6; 6 | } 7 | 8 | &.compact { 9 | @apply mt-0; 10 | 11 | .footer-inner { 12 | @apply border-t-0; 13 | } 14 | } 15 | } 16 | 17 | .hds-table, 18 | .pagination { 19 | & + .footer { 20 | @apply mt-24; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /web/app/styles/components/hds-badge.scss: -------------------------------------------------------------------------------- 1 | .hds-badge { 2 | @apply mix-blend-multiply; 3 | } 4 | 5 | .hds-badge__text { 6 | @apply truncate; 7 | } 8 | -------------------------------------------------------------------------------- /web/app/styles/components/header/active-filter-list-item.scss: -------------------------------------------------------------------------------- 1 | .active-filter-list-item { 2 | @apply flex h-[30px] items-center rounded-full border border-color-border-strong bg-color-surface-interactive px-3.5 no-underline; 3 | 4 | &:hover { 5 | @apply bg-color-surface-interactive-hover; 6 | } 7 | 8 | .flight-icon { 9 | @apply -ml-1.5 mr-1 scale-75; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /web/app/styles/components/header/active-filter-list.scss: -------------------------------------------------------------------------------- 1 | .active-filter-list { 2 | @apply flex items-start gap-1; 3 | 4 | h3 { 5 | @apply mr-4 flex h-[30px] shrink-0 items-center font-semibold text-color-foreground-strong; 6 | } 7 | 8 | .clear-all-link { 9 | @apply ml-2 flex h-[30px] shrink-0 items-center text-color-foreground-faint no-underline; 10 | 11 | &:hover { 12 | @apply text-color-foreground-strong; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /web/app/styles/components/header/facet-dropdown.scss: -------------------------------------------------------------------------------- 1 | .facet-dropdown-popover { 2 | @apply min-w-[175px]; 3 | 4 | &.medium:not(.has-input) { 5 | @apply w-[240px]; 6 | } 7 | 8 | &.has-input { 9 | @apply w-[320px]; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /web/app/styles/components/interactive-card.scss: -------------------------------------------------------------------------------- 1 | a { 2 | &:hover, 3 | &:focus { 4 | .interactive-card { 5 | @apply bg-gradient-to-t from-color-page-faint to-color-page-primary; 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /web/app/styles/components/jira-widget.scss: -------------------------------------------------------------------------------- 1 | .jira-widget { 2 | .jira-input { 3 | // we use a custom search icon for this 4 | @apply bg-none; 5 | } 6 | 7 | .animated-icon { 8 | --scaleInFrom: 0.5; 9 | animation: scaleIn 450ms cubic-bezier(0, 1, 0.5, 1) forwards; 10 | } 11 | 12 | a:not(.disabled) { 13 | &:hover, 14 | &:focus { 15 | .primary-text { 16 | @apply text-color-foreground-primary underline decoration-color-palette-neutral-300 underline-offset-[3px]; 17 | } 18 | } 19 | } 20 | 21 | .overflow-button-container { 22 | @apply relative top-auto right-auto mr-1 ml-2 w-auto bg-none; 23 | } 24 | } 25 | 26 | .add-jira-issue-button { 27 | @apply py-0 pl-[8px] pr-[14px]; 28 | 29 | &:hover, 30 | &:focus { 31 | @apply rounded-button-md shadow-surface-low; 32 | } 33 | } 34 | 35 | .attached-jira-issue { 36 | @apply relative grid items-center gap-2.5; 37 | grid-template-columns: 16px 1fr min-content; 38 | } 39 | -------------------------------------------------------------------------------- /web/app/styles/components/new.scss: -------------------------------------------------------------------------------- 1 | .new { 2 | h1 { 3 | @apply text-center text-display-400; 4 | } 5 | 6 | .create-new-form { 7 | @apply mx-auto mt-6 max-w-xl; 8 | } 9 | 10 | .footer { 11 | @apply mt-24; 12 | } 13 | 14 | .feature-icon { 15 | --slideUpFrom: 4px; 16 | animation: slideUp 1500ms cubic-bezier(0, 1, 0.5, 1); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /web/app/styles/components/notification.scss: -------------------------------------------------------------------------------- 1 | .notifications-container { 2 | @apply fixed z-20 bottom-6 right-6; 3 | } 4 | 5 | @keyframes notificationIn { 6 | from { 7 | opacity: 0; 8 | transform: translateX(8px); 9 | } 10 | to { 11 | opacity: 1; 12 | transform: translateX(0); 13 | } 14 | } 15 | 16 | @keyframes notificationOut { 17 | from { 18 | opacity: 1; 19 | transform: translateX(0); 20 | } 21 | to { 22 | opacity: 0; 23 | transform: translateX(8px); 24 | } 25 | } 26 | 27 | .notification { 28 | animation: notificationIn 700ms cubic-bezier(0.68, -0.55, 0.265, 1.55) 29 | forwards; 30 | 31 | &.exiting { 32 | animation: notificationOut 300ms cubic-bezier(0.68, -0.55, 0.265, 1.55) 33 | forwards; 34 | } 35 | } 36 | 37 | .flash-message { 38 | .hds-toast { 39 | @apply w-[400px]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /web/app/styles/components/overflow-menu.scss: -------------------------------------------------------------------------------- 1 | .overflow-button { 2 | @apply pointer-events-auto relative z-10 h-[26px] w-[26px] rounded; 3 | 4 | &:hover:not(.disabled):not(:focus), 5 | &:focus { 6 | @apply bg-color-surface-faint; 7 | } 8 | } 9 | 10 | .overflow-menu-item-button { 11 | @apply flex w-full space-x-2 py-2 px-4 leading-none; 12 | 13 | > .flight-icon { 14 | @apply text-inherit; 15 | } 16 | } 17 | 18 | .overflow-button-container { 19 | @apply pointer-events-none absolute top-[3px] -right-px; 20 | @apply flex w-16 justify-end rounded-r; 21 | @apply bg-gradient-to-l from-color-page-primary via-color-page-primary to-transparent; 22 | } 23 | -------------------------------------------------------------------------------- /web/app/styles/components/page.scss: -------------------------------------------------------------------------------- 1 | .page { 2 | @apply flex flex-col items-center flex-1 min-h-full px-8 py-10; 3 | } 4 | 5 | .page--fixed-width { 6 | @apply flex flex-col space-y-4 w-full max-w-screen-lg pb-12 container; 7 | } 8 | -------------------------------------------------------------------------------- /web/app/styles/components/person.scss: -------------------------------------------------------------------------------- 1 | .person { 2 | @apply grid gap-2; 3 | grid-template-columns: 20px 1fr; 4 | } 5 | -------------------------------------------------------------------------------- /web/app/styles/components/popover.scss: -------------------------------------------------------------------------------- 1 | .hermes-popover { 2 | @apply z-50 rounded-md bg-color-foreground-high-contrast min-w-[200px] max-w-[400px] flex flex-col overflow-auto hds-surface-high; 3 | 4 | .text { 5 | @apply relative; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /web/app/styles/components/preview-card.scss: -------------------------------------------------------------------------------- 1 | .preview-card { 2 | @apply p-4 sticky top-4 space-y-4; 3 | background: var(--token-color-surface-faint); 4 | } 5 | -------------------------------------------------------------------------------- /web/app/styles/components/product-link.scss: -------------------------------------------------------------------------------- 1 | .product-link { 2 | &:hover .hds-badge--color-neutral { 3 | @apply bg-color-palette-neutral-200; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /web/app/styles/components/projects/tile.scss: -------------------------------------------------------------------------------- 1 | .project-tile-grid { 2 | grid-template-columns: 1fr 120px 160px; 3 | 4 | .title-and-description { 5 | grid-column: 1; 6 | 7 | .inner { 8 | grid-template-columns: 20px 1fr; 9 | } 10 | } 11 | 12 | .jira { 13 | grid-column: 2; 14 | } 15 | 16 | .products { 17 | grid-column: 3; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /web/app/styles/components/segmented-control.scss: -------------------------------------------------------------------------------- 1 | .segmented-control { 2 | @apply flex; 3 | 4 | a { 5 | @apply relative flex h-9 w-1/3 items-center justify-center gap-1.5 rounded-none border border-color-palette-neutral-300; 6 | @apply bg-color-page-faint; 7 | 8 | &:first-child { 9 | @apply rounded-l-button-md; 10 | } 11 | 12 | &:last-child { 13 | @apply rounded-r-button-md; 14 | } 15 | 16 | &:hover:not(.active) { 17 | @apply bg-color-page-primary; 18 | } 19 | 20 | + a { 21 | @apply -ml-px; 22 | } 23 | 24 | &:focus { 25 | @apply z-20; 26 | } 27 | } 28 | 29 | .active { 30 | @apply z-10 border-color-foreground-action-hover bg-color-foreground-action text-white; 31 | 32 | path { 33 | @apply fill-current; 34 | } 35 | 36 | .status-icon-fill { 37 | fill: none; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /web/app/styles/components/table/sortable-header.scss: -------------------------------------------------------------------------------- 1 | .sortable-table-header { 2 | @apply relative; 3 | 4 | .sort-icon { 5 | @apply absolute -left-1.5 top-1/2 flex -translate-y-1/2 -translate-x-full; 6 | @apply text-color-foreground-faint; 7 | } 8 | 9 | &:not(.active) { 10 | &:hover, 11 | &:focus-within { 12 | @keyframes sortIconIn { 13 | from { 14 | transform: translateX(2px); 15 | } 16 | } 17 | 18 | .sort-icon { 19 | @apply visible; 20 | > .flight-icon { 21 | animation: sortIconIn 85ms ease-in; 22 | } 23 | } 24 | } 25 | 26 | .sort-icon { 27 | @apply invisible text-color-foreground-disabled; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /web/app/styles/components/toolbar.scss: -------------------------------------------------------------------------------- 1 | .toolbar { 2 | .hds-dropdown-list-item { 3 | &__interactive-text { 4 | font-weight: normal; 5 | } 6 | 7 | .checked { 8 | svg { 9 | fill: var(--token-color-foreground-action); 10 | } 11 | } 12 | } 13 | } 14 | 15 | .facets { 16 | li { 17 | &:not(:first-child) { 18 | button, 19 | input { 20 | @apply rounded-none; 21 | } 22 | 23 | button, 24 | input { 25 | @apply border-l-0; 26 | } 27 | } 28 | 29 | &:first-child { 30 | button { 31 | @apply rounded-r-none; 32 | } 33 | } 34 | 35 | &:last-child { 36 | button, 37 | input { 38 | @apply rounded-r-button-md; 39 | } 40 | 41 | input { 42 | &:focus, 43 | &.mock-focus { 44 | @apply -ml-px rounded-l-button-md border-l; 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /web/app/styles/components/x-tooltip.scss: -------------------------------------------------------------------------------- 1 | .hermes-tooltip { 2 | @apply z-50 w-max bg-color-foreground-strong rounded py-2 px-2.5 text-color-foreground-high-contrast; 3 | 4 | .arrow { 5 | @apply absolute bg-color-foreground-strong h-2 w-2 -z-10 pointer-events-none; 6 | } 7 | 8 | .text { 9 | @apply relative; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /web/app/styles/components/x/dropdown/toggle-select.scss: -------------------------------------------------------------------------------- 1 | .x-dropdown-list-toggle-select { 2 | @apply relative flex min-h-[36px] w-full items-center justify-start gap-0 rounded-button-md border border-color-border-strong px-2.5 leading-none; 3 | 4 | .flight-icon { 5 | @apply shrink-0; 6 | } 7 | 8 | &.hds-button { 9 | @apply justify-start; 10 | } 11 | 12 | &.hds-button--size-medium { 13 | @apply px-2.5; 14 | } 15 | 16 | .hds-button__icon + .hds-button__text { 17 | @apply ml-2.5; 18 | } 19 | 20 | &.hds-button--color-secondary:not(:hover) { 21 | @apply bg-color-surface-primary; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/app/styles/error-404.scss: -------------------------------------------------------------------------------- 1 | .error-404 { 2 | @apply bg-color-page-faint; 3 | 4 | .title { 5 | @apply mb-5 mr-20 text-[3.5rem]; 6 | } 7 | 8 | .doc-page { 9 | @apply mt-16 rounded-t-xl border border-color-border-primary bg-white shadow-2xl; 10 | } 11 | 12 | .meta-information { 13 | @apply grid grid-cols-2 gap-8; 14 | 15 | > div { 16 | @apply space-y-1; 17 | } 18 | } 19 | 20 | .divider { 21 | @apply h-[4px] w-full bg-color-foreground-strong; 22 | } 23 | 24 | p { 25 | @apply text-display-300 text-color-foreground-strong; 26 | } 27 | 28 | .summary { 29 | @apply mb-6 pr-20 text-display-400; 30 | } 31 | 32 | .gradient-overlay { 33 | @apply pointer-events-none fixed left-0 bottom-0 h-56 w-full bg-gradient-to-t from-color-surface-strong to-transparent; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /web/app/styles/hashicorp/hermes-logo.scss: -------------------------------------------------------------------------------- 1 | .hermes-logo { 2 | @apply shrink-0 flex items-center space-x-2.5 text-color-foreground-primary; 3 | } 4 | 5 | .hermes-logo-text { 6 | @apply text-display-300 font-semibold; 7 | } 8 | 9 | .hermes-logo-divider { 10 | @apply h-6 relative w-px bg-color-border-strong content-none; 11 | } 12 | -------------------------------------------------------------------------------- /web/app/styles/hermes/variables: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-forge/hermes/caa1b990d237b39a99643f84ec5cbdbc6e086f2b/web/app/styles/hermes/variables -------------------------------------------------------------------------------- /web/app/styles/routes/my.scss: -------------------------------------------------------------------------------- 1 | .my { 2 | .hds-table { 3 | td { 4 | @apply py-0 pr-10; 5 | 6 | > .inner { 7 | @apply overflow-hidden; 8 | } 9 | } 10 | } 11 | 12 | .footer { 13 | @apply mt-24; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /web/app/styles/routes/results.scss: -------------------------------------------------------------------------------- 1 | body.results { 2 | .section-header { 3 | @apply mb-3 inline-flex items-center gap-2; 4 | } 5 | 6 | .product-area-search-result { 7 | @apply flex h-10 items-center gap-3 rounded-button-md border border-transparent pl-2.5 pr-3 text-display-300 font-semibold text-color-foreground-strong; 8 | 9 | &:hover, 10 | &:focus { 11 | @apply border-color-border-primary bg-color-page-faint; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /web/app/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // Breakpoints 2 | $screen-sm: 640px; 3 | $screen-md: 768px; 4 | $screen-lg: 1024px; 5 | $screen-xl: 1280px; 6 | $screen-2xl: 1536px; 7 | -------------------------------------------------------------------------------- /web/app/templates/application-loading.hbs: -------------------------------------------------------------------------------- 1 | {{! @glint-nocheck: not typesafe yet }} 2 |
    3 | 4 |
    5 | -------------------------------------------------------------------------------- /web/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{page-title "Hermes"}} 2 |
    3 | 4 | 5 | 6 |
    7 | {{outlet}} 8 |
    9 | 10 | 11 | 12 |
    13 | 14 |
    15 | 16 | {{#if this.animatedToolsAreShown}} 17 | 18 | {{/if}} 19 | -------------------------------------------------------------------------------- /web/app/templates/authenticated.hbs: -------------------------------------------------------------------------------- 1 | {{#if this.standardTemplateIsShown}} 2 |
    3 |
    4 | {{outlet}} 5 |
    6 |
    7 | {{else}} 8 | {{outlet}} 9 | {{/if}} 10 | -------------------------------------------------------------------------------- /web/app/templates/authenticated/dashboard.hbs: -------------------------------------------------------------------------------- 1 | {{page-title "Dashboard"}} 2 | {{set-body-class "dashboard"}} 3 | 4 | 5 | -------------------------------------------------------------------------------- /web/app/templates/authenticated/document.hbs: -------------------------------------------------------------------------------- 1 | {{page-title (or this.model.doc.title "Document")}} 2 | {{set-body-class "secondary-screen"}} 3 | 4 | 10 | -------------------------------------------------------------------------------- /web/app/templates/authenticated/documents.hbs: -------------------------------------------------------------------------------- 1 | {{page-title "All Docs"}} 2 | 3 | 4 | 5 | {{#unless this.activeFilters.isEmpty}} 6 | 10 | {{/unless}} 11 | 12 | 19 | -------------------------------------------------------------------------------- /web/app/templates/authenticated/my.hbs: -------------------------------------------------------------------------------- 1 | {{set-body-class "my"}} 2 | 3 | {{outlet}} 4 | -------------------------------------------------------------------------------- /web/app/templates/authenticated/my/documents.hbs: -------------------------------------------------------------------------------- 1 | {{page-title "My Docs"}} 2 | 3 | 10 | -------------------------------------------------------------------------------- /web/app/templates/authenticated/new.hbs: -------------------------------------------------------------------------------- 1 | {{set-body-class "new"}} 2 | 3 | {{outlet}} 4 | -------------------------------------------------------------------------------- /web/app/templates/authenticated/new/doc.hbs: -------------------------------------------------------------------------------- 1 | {{page-title (concat "Create Your " @model)}} 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/app/templates/authenticated/new/index.hbs: -------------------------------------------------------------------------------- 1 | {{page-title "Choose a template"}} 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/app/templates/authenticated/new/project.hbs: -------------------------------------------------------------------------------- 1 | {{page-title "Start a project"}} 2 | 3 | 4 | -------------------------------------------------------------------------------- /web/app/templates/authenticated/product-areas.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /web/app/templates/authenticated/product-areas/product-area.hbs: -------------------------------------------------------------------------------- 1 | {{page-title this.model.productArea}} 2 | 3 | 8 | -------------------------------------------------------------------------------- /web/app/templates/authenticated/projects.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /web/app/templates/authenticated/projects/index.hbs: -------------------------------------------------------------------------------- 1 | {{page-title "All Projects"}} 2 | 3 | 9 | -------------------------------------------------------------------------------- /web/app/templates/authenticated/projects/project.hbs: -------------------------------------------------------------------------------- 1 | {{page-title this.model.title}} 2 | {{set-body-class "project-screen"}} 3 | 4 | {{#unless this.newModelHasLoaded}} 5 | 6 | {{/unless}} 7 | -------------------------------------------------------------------------------- /web/app/templates/authenticated/results.hbs: -------------------------------------------------------------------------------- 1 | {{set-body-class "results"}} 2 | {{page-title this.pageTitle}} 3 | 4 |
    5 |

    6 | Results 7 | {{#if this.q}} 8 | for 9 | {{this.q}} 10 | {{/if}} 11 |

    12 | 13 |
    14 | 15 | 16 | 17 | 23 | -------------------------------------------------------------------------------- /web/app/templates/authenticated/settings.hbs: -------------------------------------------------------------------------------- 1 | {{page-title "Email Notifications"}} 2 | 3 |
    4 | 5 |

    Email notifications

    6 |

    Get notified when docs are created in 7 | the following areas...

    8 | 9 |
    10 | -------------------------------------------------------------------------------- /web/app/torii-providers/google-oauth2-bearer.js: -------------------------------------------------------------------------------- 1 | import GoogleOauth2BearerV2 from "torii/providers/google-oauth2-bearer-v2"; 2 | 3 | export default class GoogleToriiProvider extends GoogleOauth2BearerV2 { 4 | redirectUri = window.location.origin + "/torii/redirect.html"; 5 | 6 | fetch(data) { 7 | return data; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /web/app/types/document-routes.ts: -------------------------------------------------------------------------------- 1 | import { SearchScope } from "hermes/routes/authenticated/results"; 2 | 3 | export interface DocumentsRouteParams { 4 | docType: string[]; 5 | owners: string[]; 6 | page: number; 7 | product: string[]; 8 | sortBy: string; 9 | status: string[]; 10 | } 11 | 12 | export interface ResultsRouteParams extends DocumentsRouteParams { 13 | q: string; 14 | page: number; 15 | scope: SearchScope; 16 | } 17 | -------------------------------------------------------------------------------- /web/app/types/document-type.d.ts: -------------------------------------------------------------------------------- 1 | import { CustomEditableField } from "./document"; 2 | 3 | export interface HermesDocumentType { 4 | Template: string; 5 | 6 | checks?: { 7 | label: string; 8 | helperText?: string; 9 | links?: { 10 | text: string; 11 | url: string; 12 | }[]; 13 | }; 14 | name: string; 15 | longName: string; 16 | description: string; 17 | flightIcon?: string; 18 | moreInfoLink?: { 19 | text: string; 20 | url: string; 21 | }; 22 | customFields?: { 23 | name: string; 24 | readOnly: boolean; 25 | type: "string" | "people"; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /web/app/types/facets.d.ts: -------------------------------------------------------------------------------- 1 | import { FacetName } from "hermes/components/header/toolbar"; 2 | 3 | /** 4 | * E.g., { docType: { "API": { count: 1, isSelected: false }}} 5 | */ 6 | export type FacetDropdownGroups = { 7 | [name in FacetName]: FacetDropdownObjects; 8 | }; 9 | 10 | /** 11 | * E.g., { "API": { count: 1, isSelected: false }} 12 | */ 13 | export interface FacetDropdownObjects { 14 | [key: string]: FacetDropdownObjectDetails; 15 | } 16 | 17 | /** 18 | * E.g., {count: 1, isSelected: false} 19 | */ 20 | export type FacetDropdownObjectDetails = { 21 | count: number; 22 | isSelected: boolean; 23 | }; 24 | 25 | export type FacetRecord = Record; 26 | export type FacetRecords = Record; 27 | -------------------------------------------------------------------------------- /web/app/types/route-models.ts: -------------------------------------------------------------------------------- 1 | // https://docs.ember-cli-typescript.com/cookbook/working-with-route-models 2 | 3 | import Route from "@ember/routing/route"; 4 | /** 5 | * Get the resolved type of an item. 6 | * - If the item is a promise, the result will be the resolved value type 7 | * - If the item is not a promise, the result will just be the type of the item 8 | */ 9 | export type Resolved

    = P extends Promise ? T : P; 10 | 11 | /** Get the resolved model value from a route. */ 12 | export type ModelFrom = Resolved>; 13 | -------------------------------------------------------------------------------- /web/app/types/sizes.ts: -------------------------------------------------------------------------------- 1 | export enum HermesSize { 2 | Small = "small", 3 | Medium = "medium", 4 | Large = "large", 5 | XL = "xl", 6 | } 7 | -------------------------------------------------------------------------------- /web/app/utils/blink-element.ts: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | 3 | /** 4 | * Blinks an element twice by toggling its visibility. 5 | * Used for emphasis, such as to reiterate an 6 | * already-visible form error. 7 | */ 8 | const DURATION = Ember.testing ? 0 : 100; 9 | 10 | export default function blinkElement(element?: Element | null) { 11 | if (!element) { 12 | return; 13 | } 14 | 15 | for (let i = 0; i < 4; i++) { 16 | // Alternate between hidden and visible 17 | let visibility = i % 2 === 0 ? "hidden" : "visible"; 18 | 19 | setTimeout(() => { 20 | if (element) { 21 | element.setAttribute("style", `visibility: ${visibility}`); 22 | } 23 | }, i * DURATION); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /web/app/utils/create-draft-url-search-params.ts: -------------------------------------------------------------------------------- 1 | import { HITS_PER_PAGE } from "hermes/services/algolia"; 2 | 3 | export function createDraftURLSearchParams(options: { 4 | ownerEmail: string; 5 | hitsPerPage?: number; 6 | page?: number; 7 | facetFilters?: string[]; 8 | }): URLSearchParams { 9 | const { ownerEmail, page, hitsPerPage, facetFilters } = options; 10 | return new URLSearchParams( 11 | Object.entries({ 12 | hitsPerPage: hitsPerPage ?? HITS_PER_PAGE, 13 | maxValuesPerFacet: 1, 14 | page: page ? page - 1 : 0, 15 | ownerEmail, 16 | facetFilters, 17 | }) 18 | .map(([key, val]) => `${key}=${val}`) 19 | .join("&"), 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /web/app/utils/ember-animated/easings.ts: -------------------------------------------------------------------------------- 1 | // https://spicyyoghurt.com/tools/easing-functions 2 | 3 | let b = 0; // start value 4 | let c = 1; // change 5 | let d = 1; // duration 6 | 7 | export function easeOutQuad(time: number): number { 8 | return -c * (time /= d) * (time - 2) + b; 9 | } 10 | 11 | export function easeOutExpo(time: number) { 12 | return time == d ? b + c : c * (-Math.pow(2, (-10 * time) / d) + 1) + b; 13 | } 14 | -------------------------------------------------------------------------------- /web/app/utils/ember-animated/empty-transition.ts: -------------------------------------------------------------------------------- 1 | import { TransitionContext } from "ember-animated/."; 2 | 3 | export function* emptyTransition(_context: TransitionContext) {} 4 | -------------------------------------------------------------------------------- /web/app/utils/ember-cli-flash/timeouts.ts: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | 3 | export const FLASH_MESSAGES_LONG_TIMEOUT = Ember.testing ? 0 : 10000; 4 | -------------------------------------------------------------------------------- /web/app/utils/facets.js: -------------------------------------------------------------------------------- 1 | /** 2 | * mapKeys. 3 | * 4 | * @param Object obj 5 | * @param Function mapper 6 | * 7 | * Iterates over the properties on an object calling mapper on each value. 8 | * 9 | * mapKeys({ residence: 'house', footwear: 'shoes' }, x => 'boat'+x) 10 | * // { residence: 'boathouse', footwear: 'boatshoes' } 11 | */ 12 | export const mapKeys = (obj, mapper) => 13 | Object.entries(obj).reduce((newObj, [key, val]) => { 14 | newObj[key] = mapper(val); 15 | return newObj; 16 | }, {}); 17 | 18 | /** 19 | * statefulFacet. 20 | * 21 | * @param Object facet 22 | * Iterates over the keys of a facet object and transforms the count value 23 | * to an object that has count and selected properties 24 | */ 25 | export const statefulFacet = (facet) => 26 | mapKeys(facet, (count) => ({ count, isSelected: false })); 27 | 28 | export const markSelected = (facet, selection) => 29 | (selection || []).forEach((param) => (facet[param].isSelected = true)); 30 | -------------------------------------------------------------------------------- /web/app/utils/get-product-id.ts: -------------------------------------------------------------------------------- 1 | export default function getProductId(productName?: string) { 2 | if (!productName) { 3 | return; 4 | } 5 | let product = productName.toLowerCase(); 6 | 7 | switch (product) { 8 | case "boundary": 9 | case "consul": 10 | case "nomad": 11 | case "packer": 12 | case "terraform": 13 | case "vagrant": 14 | case "vault": 15 | case "waypoint": 16 | return product; 17 | case "cloud platform": 18 | return "hcp"; 19 | default: 20 | return; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /web/app/utils/get-product-label.ts: -------------------------------------------------------------------------------- 1 | export default function getProductLabel(product?: string) { 2 | if (!product) { 3 | return "Unknown"; 4 | } 5 | 6 | switch (product) { 7 | case "Cloud Platform": 8 | return "HCP"; 9 | default: 10 | return product; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /web/app/utils/hermes-urls.ts: -------------------------------------------------------------------------------- 1 | export const HERMES_GITHUB_REPO_URL = 2 | "https://github.com/hashicorp-forge/hermes"; 3 | -------------------------------------------------------------------------------- /web/app/utils/html-element.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "@ember/debug"; 2 | 3 | export default function htmlElement(selector: string): HTMLElement { 4 | const element = document.querySelector(selector); 5 | assert( 6 | "selector target must be an HTMLElement", 7 | element instanceof HTMLElement 8 | ); 9 | return element; 10 | } 11 | -------------------------------------------------------------------------------- /web/app/utils/is-valid-u-r-l.ts: -------------------------------------------------------------------------------- 1 | export default function isValidURL(string: string): boolean { 2 | try { 3 | new URL(string); 4 | return true; 5 | } catch { 6 | return false; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /web/app/utils/mockdate/dates.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_MOCK_DATE = "2000-01-01T06:00:00.000-07:00"; 2 | -------------------------------------------------------------------------------- /web/app/utils/parse-date.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts a valid Date/timestamp into a string formatted like 3 | * "21 Dec. 2023" or "21 December 2023" (if monthFormat is "long"). 4 | * Returns null if the time parameter is invalid. 5 | */ 6 | export default function parseDate( 7 | time?: string | number | Date, 8 | monthFormat: "short" | "long" = "short", 9 | ): string | null { 10 | if (!time) { 11 | return null; 12 | } 13 | const date = new Date(time); 14 | 15 | if (isNaN(date.getTime())) { 16 | return null; 17 | } 18 | 19 | let day = date.getDate(); 20 | let year = date.getFullYear(); 21 | let month = date.toLocaleString("default", { month: monthFormat }); 22 | 23 | if (monthFormat === "short") { 24 | month += "."; 25 | } 26 | 27 | return `${day} ${month} ${year}`; 28 | } 29 | -------------------------------------------------------------------------------- /web/app/utils/scroll-into-view-if-needed.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A `scrollIntoView` wrapper with default options. 3 | * https://github.com/scroll-into-view/compute-scroll-into-view?tab=readme-ov-file#api 4 | */ 5 | 6 | import scrollIntoView from "scroll-into-view-if-needed"; 7 | 8 | export default function scrollIntoViewIfNeeded( 9 | element: Element, 10 | options?: ScrollIntoViewOptions, 11 | ): void { 12 | scrollIntoView(element, { 13 | scrollMode: "if-needed", 14 | block: "start", 15 | inline: "nearest", 16 | ...options, 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /web/app/utils/simple-timeout.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A timeout function for polling tasks. 3 | * Not registered with Ember's runloop 4 | * (unlike ember-concurrency's timeout helper), 5 | * so it doesn't hang in acceptance tests. 6 | * 7 | * See: https://ember-concurrency.com/docs/testing-debugging 8 | */ 9 | 10 | export default function simpleTimeout(timeout: number) { 11 | return new Promise((resolve) => { 12 | setTimeout(resolve, timeout); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /web/app/utils/tooltip-text.ts: -------------------------------------------------------------------------------- 1 | export const IS_SUBSCRIBED_TOOLTIP_TEXT = 2 | "You'll be emailed when a document is published in this product/area."; 3 | 4 | export const NOT_SUBSCRIBED_TOOLTIP_TEXT = 5 | "Get emailed when a document is published in this product/area"; 6 | -------------------------------------------------------------------------------- /web/app/utils/update-related-resources-sort-order.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RelatedExternalLink, 3 | RelatedHermesDocument, 4 | } from "hermes/components/related-resources"; 5 | 6 | export default function updateRelatedResourcesSortOrder( 7 | hermesDocuments: RelatedHermesDocument[], 8 | externalLinks: RelatedExternalLink[], 9 | ) { 10 | hermesDocuments.forEach((doc, index) => { 11 | doc.sortOrder = index + 1; 12 | }); 13 | 14 | externalLinks.forEach((link, index) => { 15 | link.sortOrder = index + 1 + hermesDocuments.length; 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /web/config/deprecation-workflow.js: -------------------------------------------------------------------------------- 1 | self.deprecationWorkflow = self.deprecationWorkflow || {}; 2 | self.deprecationWorkflow.config = { 3 | workflow: [ 4 | { handler: "log", matchId: "ember-global" }, 5 | { 6 | handler: "log", 7 | matchId: "ember-simple-auth.initializer.setup-session-restoration", 8 | }, 9 | { handler: "log", matchId: "deprecate-router-events" }, 10 | { handler: "log", matchId: "this-property-fallback" }, 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /web/config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "3.28.6", 7 | "blueprints": [ 8 | { 9 | "name": "app", 10 | "outputRepo": "https://github.com/ember-cli/ember-new-output", 11 | "codemodsSource": "ember-app-codemods-manifest@1", 12 | "isBaseBlueprint": true 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /web/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /web/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions', 7 | ]; 8 | 9 | // Ember's browser support policy is changing, and IE11 support will end in 10 | // v4.0 onwards. 11 | // 12 | // See https://deprecations.emberjs.com/v3.x#toc_3-0-browser-support-policy 13 | // 14 | // If you need IE11 support on a version of Ember that still offers support 15 | // for it, uncomment the code block below. 16 | // 17 | // const isCI = Boolean(process.env.CI); 18 | // const isProduction = process.env.EMBER_ENV === 'production'; 19 | // 20 | // if (isCI || isProduction) { 21 | // browsers.push('ie 11'); 22 | // } 23 | 24 | module.exports = { 25 | browsers, 26 | }; 27 | -------------------------------------------------------------------------------- /web/mirage/algolia/hosts.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Algolia has one static endpoint and several wildcard hosts, e.g., 3 | * - appID-1.algolianet.com 4 | * - appID-2.algolianet.com 5 | * 6 | * Mirage lacks wildcard support, so we create a route for each. 7 | * Used in tests to mock Algolia endpoints. 8 | */ 9 | 10 | import config from "hermes/config/environment"; 11 | 12 | // Start with the static route. 13 | let algoliaHosts = [ 14 | `https://${config.algolia.appID}-dsn.algolia.net/1/indexes/**`, 15 | ]; 16 | 17 | // Add wildcard routes. 18 | for (let i = 1; i <= 9; i++) { 19 | algoliaHosts.push( 20 | `https://${config.algolia.appID}-${i}.algolianet.com/1/indexes/**`, 21 | ); 22 | } 23 | 24 | export default algoliaHosts; 25 | -------------------------------------------------------------------------------- /web/mirage/factories/document-type.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from "miragejs"; 2 | 3 | export default Factory.extend({ 4 | name: (i: number) => `DT${i}`, 5 | longName: (i: number) => `Document Type ${i}`, 6 | description: "This is a test document type", 7 | }); 8 | -------------------------------------------------------------------------------- /web/mirage/factories/group.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from "miragejs"; 2 | 3 | export default Factory.extend({ 4 | name: (i: number) => `Group ${i}`, 5 | email: (i: number) => `group-${i}@hashicorp.com`, 6 | }); 7 | -------------------------------------------------------------------------------- /web/mirage/factories/jira-issue.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from "miragejs"; 2 | import { 3 | TEST_JIRA_PRIORITY, 4 | TEST_JIRA_PRIORITY_IMAGE, 5 | TEST_JIRA_ISSUE_TYPE, 6 | TEST_JIRA_ISSUE_TYPE_IMAGE, 7 | TEST_JIRA_ASSIGNEE, 8 | TEST_JIRA_ISSUE_STATUS, 9 | TEST_JIRA_ISSUE_URL, 10 | TEST_JIRA_ISSUE_SUMMARY, 11 | } from "../utils"; 12 | 13 | export default Factory.extend({ 14 | id: (i) => i, // mirage only 15 | key: (i) => `KEY-00${i}`, 16 | summary: TEST_JIRA_ISSUE_SUMMARY, 17 | url: TEST_JIRA_ISSUE_URL, 18 | status: TEST_JIRA_ISSUE_STATUS, 19 | assignee: TEST_JIRA_ASSIGNEE, 20 | issueType: TEST_JIRA_ISSUE_TYPE, 21 | issueTypeImage: TEST_JIRA_ISSUE_TYPE_IMAGE, 22 | priority: TEST_JIRA_PRIORITY, 23 | priorityImage: TEST_JIRA_PRIORITY_IMAGE, 24 | project: "", // unused 25 | }); 26 | -------------------------------------------------------------------------------- /web/mirage/factories/jira-picker-result.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from "miragejs"; 2 | import { 3 | TEST_JIRA_ISSUE_SUMMARY, 4 | TEST_JIRA_ISSUE_URL, 5 | TEST_JIRA_ISSUE_TYPE_IMAGE, 6 | } from "../utils"; 7 | 8 | export default Factory.extend({ 9 | id: (i) => i, // Mirage-only 10 | key: (i) => `KEY-00${i}`, 11 | url: TEST_JIRA_ISSUE_URL, 12 | issueTypeImage: TEST_JIRA_ISSUE_TYPE_IMAGE, 13 | summary: TEST_JIRA_ISSUE_SUMMARY, 14 | }); 15 | -------------------------------------------------------------------------------- /web/mirage/factories/me.ts: -------------------------------------------------------------------------------- 1 | import { Factory, ModelInstance } from "miragejs"; 2 | import { 3 | TEST_USER_EMAIL, 4 | TEST_USER_NAME, 5 | TEST_USER_GIVEN_NAME, 6 | TEST_USER_PHOTO, 7 | } from "../utils"; 8 | 9 | export default Factory.extend({ 10 | id: TEST_USER_EMAIL, 11 | subscriptions: [], 12 | isLoggedIn: true, // Mirage-only attribute 13 | 14 | // these are part of the back-end response for `me` 15 | email: TEST_USER_EMAIL, 16 | name: TEST_USER_NAME, 17 | given_name: TEST_USER_GIVEN_NAME, 18 | picture: TEST_USER_PHOTO, 19 | }); 20 | -------------------------------------------------------------------------------- /web/mirage/factories/person.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from "miragejs"; 2 | import { TEST_USER_PHOTO } from "../utils"; 3 | 4 | export default Factory.extend({ 5 | id: (i) => `person-${i}`, 6 | name: "Foo Bar", 7 | firstName: "Foo", 8 | email() { 9 | return `${this.id}@hashicorp.com`; 10 | }, 11 | picture: TEST_USER_PHOTO, 12 | }); 13 | -------------------------------------------------------------------------------- /web/mirage/factories/product.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from "miragejs"; 2 | 3 | export default Factory.extend({ 4 | name: (i: number) => `Test Product ${i}`, 5 | abbreviation: (i: number) => `TP${i}`, 6 | }); 7 | -------------------------------------------------------------------------------- /web/mirage/factories/recently-viewed-doc.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from "miragejs"; 2 | 3 | export default Factory.extend({ 4 | id: (i: number) => `doc-${i}`, 5 | name: (i: number) => `Document ${i}`, 6 | viewedTime: 1, 7 | isDraft: false, 8 | isLegacy: false, 9 | 10 | /** 11 | * Associate the record with a document from the database. 12 | * Create one if it doesn't already exist. 13 | */ 14 | afterCreate(recentlyViewedDoc, server) { 15 | const { id } = recentlyViewedDoc; 16 | const doc = server.schema.document.find(id); 17 | 18 | if (doc) { 19 | recentlyViewedDoc.update({ doc: doc.attrs }); 20 | } else { 21 | recentlyViewedDoc.update({ 22 | doc: server.create("document", { 23 | id, 24 | objectID: id, 25 | }).attrs, 26 | }); 27 | } 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /web/mirage/factories/recently-viewed-docs-database.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from "miragejs"; 2 | 3 | export default Factory.extend({ 4 | name: "recently_viewed_docs.json", 5 | id: "1", 6 | }); 7 | -------------------------------------------------------------------------------- /web/mirage/factories/recently-viewed-project.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from "miragejs"; 2 | 3 | export default Factory.extend({ 4 | id: (i: number) => i, 5 | viewedTime: 1, 6 | 7 | /** 8 | * Associate the record with a project from the database. 9 | * Create one if it doesn't already exist. 10 | */ 11 | afterCreate(recentlyViewedProject, server) { 12 | const { id } = recentlyViewedProject; 13 | const project = server.schema.projects.find(id); 14 | 15 | if (project) { 16 | recentlyViewedProject.update({ project: project.attrs }); 17 | } else { 18 | recentlyViewedProject.update({ 19 | project: server.create("project", { 20 | id, 21 | }).attrs, 22 | }); 23 | } 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /web/mirage/factories/related-external-link.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from "miragejs"; 2 | 3 | export default Factory.extend({ 4 | id: (i) => i, 5 | sortOrder: (i) => i, 6 | name() { 7 | return `Related External Link ${this.id}`; 8 | }, 9 | url() { 10 | return `https://${this.id}.hashicorp.com`; 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /web/mirage/helpers.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "@ember/debug"; 2 | import { Server } from "miragejs"; 3 | /** 4 | * Provides access to the Mirage server outside of test contexts (e.g. in a Factory). 5 | * If you are in a test, use MirageTestContext instead. 6 | */ 7 | export const MirageContext = { 8 | get server(): Server { 9 | let { server } = window as { server?: Server }; 10 | assert("Expected a global `server` but found none", server); 11 | return server; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /web/mirage/models/document-type.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "miragejs"; 2 | 3 | export default Model.extend({ 4 | // Required for Mirage, even though it's empty 5 | }); 6 | -------------------------------------------------------------------------------- /web/mirage/models/document.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "miragejs"; 2 | 3 | export default Model.extend({ 4 | // Required for Mirage, even though it's empty 5 | }); 6 | -------------------------------------------------------------------------------- /web/mirage/models/google/person.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "miragejs"; 2 | 3 | export default Model.extend({ 4 | // Required for Mirage, even though it's empty 5 | }); 6 | -------------------------------------------------------------------------------- /web/mirage/models/group.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "miragejs"; 2 | 3 | export default Model.extend({ 4 | // Required for Mirage, even though it's empty 5 | }); 6 | -------------------------------------------------------------------------------- /web/mirage/models/jira-issue.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "miragejs"; 2 | 3 | export default Model.extend({ 4 | // Required for Mirage, even though it's empty 5 | }); 6 | -------------------------------------------------------------------------------- /web/mirage/models/jira-picker-result.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "miragejs"; 2 | 3 | export default Model.extend({ 4 | // Required for Mirage, even though it's empty 5 | }); 6 | -------------------------------------------------------------------------------- /web/mirage/models/me.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Model } from "miragejs"; 3 | 4 | export default Model.extend({ 5 | // Required for Mirage, even though it's empty 6 | }); 7 | -------------------------------------------------------------------------------- /web/mirage/models/person.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "miragejs"; 2 | 3 | export default Model.extend({ 4 | // Required for Mirage, even though it's empty 5 | }); 6 | -------------------------------------------------------------------------------- /web/mirage/models/product.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "miragejs"; 2 | 3 | export default Model.extend({ 4 | // Required for Mirage, even though it's empty 5 | }); 6 | -------------------------------------------------------------------------------- /web/mirage/models/project.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "miragejs"; 2 | 3 | export default Model.extend({ 4 | // Required for Mirage, even though it's empty 5 | }); 6 | -------------------------------------------------------------------------------- /web/mirage/models/recently-viewed-doc.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "miragejs"; 2 | 3 | export default Model.extend({ 4 | // Required for Mirage, even though it's empty 5 | }); 6 | -------------------------------------------------------------------------------- /web/mirage/models/recently-viewed-docs-database.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "miragejs"; 2 | 3 | export default Model.extend({ 4 | // Required for Mirage, even though it's empty 5 | }); 6 | -------------------------------------------------------------------------------- /web/mirage/models/recently-viewed-project.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "miragejs"; 2 | 3 | export default Model.extend({ 4 | // Required for Mirage, even though it's empty 5 | }); 6 | -------------------------------------------------------------------------------- /web/mirage/models/related-external-link.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "miragejs"; 2 | 3 | export default Model.extend({ 4 | // Required for Mirage, even though it's empty 5 | }); 6 | -------------------------------------------------------------------------------- /web/mirage/models/related-hermes-document.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "miragejs"; 2 | 3 | export default Model.extend({ 4 | // Required for Mirage, even though it's empty 5 | }); 6 | -------------------------------------------------------------------------------- /web/public/images/document.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-forge/hermes/caa1b990d237b39a99643f84ec5cbdbc6e086f2b/web/public/images/document.png -------------------------------------------------------------------------------- /web/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-forge/hermes/caa1b990d237b39a99643f84ec5cbdbc6e086f2b/web/public/images/favicon.png -------------------------------------------------------------------------------- /web/public/images/hermes-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-forge/hermes/caa1b990d237b39a99643f84ec5cbdbc6e086f2b/web/public/images/hermes-logo.png -------------------------------------------------------------------------------- /web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /web/testem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: ['Chrome'], 7 | launch_in_dev: ['Chrome'], 8 | browser_start_timeout: 120, 9 | browser_args: { 10 | Chrome: { 11 | ci: [ 12 | // --no-sandbox is needed when running Chrome inside a container 13 | process.env.CI ? '--no-sandbox' : null, 14 | '--headless', 15 | '--disable-dev-shm-usage', 16 | '--disable-software-rasterizer', 17 | '--mute-audio', 18 | '--remote-debugging-port=0', 19 | '--window-size=1440,900', 20 | ].filter(Boolean), 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /web/tests/acceptance/404-test.ts: -------------------------------------------------------------------------------- 1 | import { visit } from "@ember/test-helpers"; 2 | import { setupApplicationTest } from "ember-qunit"; 3 | import { module, test } from "qunit"; 4 | import { getPageTitle } from "ember-page-title/test-support"; 5 | import MockDate from "mockdate"; 6 | import { DEFAULT_MOCK_DATE } from "hermes/utils/mockdate/dates"; 7 | 8 | module("Acceptance | 404", function (hooks) { 9 | setupApplicationTest(hooks); 10 | 11 | test("unknown URLs get the 404 treatment", async function (assert) { 12 | MockDate.set(DEFAULT_MOCK_DATE); 13 | 14 | await visit("/not_real_url"); 15 | 16 | assert.equal(getPageTitle(), "Page not found | Hermes"); 17 | 18 | assert.dom("h1").hasText("[E-404] Page not found"); 19 | assert.dom("[data-test-404-logged-time]").hasText("Logged: 1 January 2000"); 20 | 21 | MockDate.reset(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /web/tests/acceptance/authenticate-test.ts: -------------------------------------------------------------------------------- 1 | import { visit } from "@ember/test-helpers"; 2 | import { setupApplicationTest } from "ember-qunit"; 3 | import { module, test } from "qunit"; 4 | import { MirageTestContext, setupMirage } from "ember-cli-mirage/test-support"; 5 | import { getPageTitle } from "ember-page-title/test-support"; 6 | 7 | interface AuthenticateRouteTestContext extends MirageTestContext {} 8 | 9 | module("Acceptance | authenticate", function (hooks) { 10 | setupApplicationTest(hooks); 11 | setupMirage(hooks); 12 | 13 | test("the page title is correct", async function (this: AuthenticateRouteTestContext, assert) { 14 | await visit("/authenticate"); 15 | assert.equal(getPageTitle(), "Authenticate | Hermes"); 16 | }); 17 | 18 | test('the footer has the compact class', async function (this: AuthenticateRouteTestContext, assert) { 19 | await visit("/authenticate"); 20 | assert.dom(".footer").hasClass("compact"); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /web/tests/acceptance/authenticated/drafts-test.ts: -------------------------------------------------------------------------------- 1 | import { currentURL, visit } from "@ember/test-helpers"; 2 | import { setupApplicationTest } from "ember-qunit"; 3 | import { module, test } from "qunit"; 4 | import { authenticateSession } from "ember-simple-auth/test-support"; 5 | import { MirageTestContext, setupMirage } from "ember-cli-mirage/test-support"; 6 | 7 | interface AuthenticatedDraftRouteTestContext extends MirageTestContext {} 8 | 9 | module("Acceptance | authenticated/drafts", function (hooks) { 10 | setupApplicationTest(hooks); 11 | setupMirage(hooks); 12 | 13 | hooks.beforeEach(async function () { 14 | await authenticateSession({}); 15 | }); 16 | 17 | test("it redirects to the my route", async function (this: AuthenticatedDraftRouteTestContext, assert) { 18 | await visit("/drafts"); 19 | assert.equal(currentURL(), "/my/documents"); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /web/tests/acceptance/authenticated/my-test.ts: -------------------------------------------------------------------------------- 1 | import { setupApplicationTest } from "ember-qunit"; 2 | import { module, test } from "qunit"; 3 | import { authenticateSession } from "ember-simple-auth/test-support"; 4 | import { MirageTestContext, setupMirage } from "ember-cli-mirage/test-support"; 5 | import { currentURL, visit } from "@ember/test-helpers"; 6 | 7 | module("Acceptance | authenticated/my", function (hooks) { 8 | setupApplicationTest(hooks); 9 | setupMirage(hooks); 10 | 11 | hooks.beforeEach(async function () { 12 | await authenticateSession({}); 13 | }); 14 | 15 | test("it redirects to the my/documents route", async function (this: MirageTestContext, assert) { 16 | await visit("/my"); 17 | assert.equal(currentURL(), "/my/documents"); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /web/tests/acceptance/authenticated/settings-test.ts: -------------------------------------------------------------------------------- 1 | import { visit } from "@ember/test-helpers"; 2 | import { setupApplicationTest } from "ember-qunit"; 3 | import { module, test } from "qunit"; 4 | import { authenticateSession } from "ember-simple-auth/test-support"; 5 | import { MirageTestContext, setupMirage } from "ember-cli-mirage/test-support"; 6 | import { getPageTitle } from "ember-page-title/test-support"; 7 | 8 | interface AuthenticatedSettingsRouteTestContext extends MirageTestContext {} 9 | 10 | module("Acceptance | authenticated/settings", function (hooks) { 11 | setupApplicationTest(hooks); 12 | setupMirage(hooks); 13 | 14 | hooks.beforeEach(async function () { 15 | await authenticateSession({}); 16 | }); 17 | 18 | test("the page title is correct", async function (this: AuthenticatedSettingsRouteTestContext, assert) { 19 | await visit("/settings"); 20 | assert.equal(getPageTitle(), "Email Notifications | Hermes"); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /web/tests/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-forge/hermes/caa1b990d237b39a99643f84ec5cbdbc6e086f2b/web/tests/helpers/.gitkeep -------------------------------------------------------------------------------- /web/tests/helpers/flash-message.js: -------------------------------------------------------------------------------- 1 | import FlashObject from 'ember-cli-flash/flash/object'; 2 | 3 | FlashObject.reopen({ init() {} }); 4 | -------------------------------------------------------------------------------- /web/tests/integration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-forge/hermes/caa1b990d237b39a99643f84ec5cbdbc6e086f2b/web/tests/integration/.gitkeep -------------------------------------------------------------------------------- /web/tests/integration/components/empty-state-text-test.ts: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { hbs } from "ember-cli-htmlbars"; 3 | import { setupRenderingTest } from "ember-qunit"; 4 | import { module, test } from "qunit"; 5 | 6 | module("Integration | Component | empty-state-text", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("it renders as expected", async function (assert) { 10 | await render(hbs` 11 | 12 | 13 | `); 14 | 15 | assert.dom("[data-test-one]").hasText("None"); 16 | assert.dom("[data-test-two]").hasText("foo"); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /web/tests/integration/components/match-count-headline-test.ts: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | import { setupRenderingTest } from "ember-qunit"; 3 | import { render, TestContext } from "@ember/test-helpers"; 4 | import { hbs } from "ember-cli-htmlbars"; 5 | 6 | interface Context extends TestContext { 7 | count: number; 8 | } 9 | 10 | module("Integration | Component | match-count-headline", function (hooks) { 11 | setupRenderingTest(hooks); 12 | 13 | test("it uses the correct grammar", async function (assert) { 14 | this.set("count", 0); 15 | 16 | await render(hbs` 17 | 18 | `); 19 | 20 | assert.dom("h1").hasText("0 matches"); 21 | 22 | this.set("count", 1); 23 | 24 | assert.dom("h1").hasText("1 match"); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /web/tests/integration/components/modals-test.ts: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | import { setupRenderingTest } from "ember-qunit"; 3 | import { render, rerender } from "@ember/test-helpers"; 4 | import { hbs } from "ember-cli-htmlbars"; 5 | import ModalAlertsService, { ModalType } from "hermes/services/modal-alerts"; 6 | 7 | module("Integration | Component | modals", function (hooks) { 8 | setupRenderingTest(hooks); 9 | 10 | test("it conditionally renders modals", async function (assert) { 11 | let modalAlerts = this.owner.lookup( 12 | "service:modal-alerts", 13 | ) as ModalAlertsService; 14 | 15 | await render(hbs``); 16 | 17 | assert.dom("dialog").doesNotExist(); 18 | modalAlerts.open(ModalType.DraftCreated); 19 | 20 | await rerender(); 21 | assert.dom("dialog").exists("draftCreated modal shown"); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /web/tests/integration/helpers/get-product-id-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | import { setupRenderingTest } from "ember-qunit"; 3 | import { render } from "@ember/test-helpers"; 4 | import { hbs } from "ember-cli-htmlbars"; 5 | 6 | module("Integration | Helper | get-product-id", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("it returns the product ID if it exists", async function (assert) { 10 | await render(hbs` 11 |

    12 | {{get-product-id "Cloud Platform"}} 13 |
    14 | 15 | {{#if (get-product-id "Not a product")}} 16 |
    17 | This will not appear because the product name is not valid. 18 |
    19 | {{/if}} 20 | `); 21 | 22 | assert.dom('.product').hasText("hcp"); 23 | assert.dom('.non-product').doesNotExist(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /web/tests/integration/helpers/get-product-label-test.ts: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | import { setupRenderingTest } from "ember-qunit"; 3 | import { render } from "@ember/test-helpers"; 4 | import { hbs } from "ember-cli-htmlbars"; 5 | 6 | module("Integration | Helper | get-product-label", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("it works as expected", async function (assert) { 10 | await render(hbs` 11 |
    {{get-product-label "Terraform"}}
    12 |
    {{get-product-label "Cloud Platform"}}
    13 |
    {{get-product-label undefined}}
    14 | `); 15 | 16 | assert.dom("[data-test-one]").hasText("Terraform"); 17 | assert.dom("[data-test-two]").hasText("HCP"); 18 | assert.dom("[data-test-three]").hasText("Unknown"); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /web/tests/integration/helpers/highlight-text-test.ts: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | import { setupRenderingTest } from "ember-qunit"; 3 | import { render } from "@ember/test-helpers"; 4 | import { hbs } from "ember-cli-htmlbars"; 5 | 6 | module("Integration | Helper | highlight-text", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("it highlights text that matches a query", async function (assert) { 10 | await render(hbs` 11 |
    {{highlight-text "Hello, world!" "world"}}
    12 | `); 13 | 14 | assert.dom().hasText("Hello, world!"); 15 | assert.dom("mark").hasText("world"); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /web/tests/integration/helpers/html-element-test.ts: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | import { setupRenderingTest } from "ember-qunit"; 3 | import { render } from "@ember/test-helpers"; 4 | import { hbs } from "ember-cli-htmlbars"; 5 | 6 | module("Integration | Helper | html-element", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("it works as expected", async function (assert) { 10 | await render(hbs` 11 | {{! @glint-nocheck: not typesafe yet }} 12 |
    13 | 14 | {{#in-element (html-element ".container")}} 15 |
    16 | Like magic 17 |
    18 | {{/in-element}} 19 | `); 20 | 21 | assert.dom(".container .content").hasText("Like magic"); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /web/tests/integration/helpers/lowercase-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | import { setupRenderingTest } from "ember-qunit"; 3 | import { render } from "@ember/test-helpers"; 4 | import { hbs } from "ember-cli-htmlbars"; 5 | 6 | module("Integration | Helper | lowercase", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("formats Word as word", async function (assert) { 10 | this.set("string", "Word"); 11 | 12 | await render(hbs`{{lowercase this.string}}`); 13 | 14 | assert.equal(this.element.textContent.trim(), "word"); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /web/tests/mirage-helpers/utils.ts: -------------------------------------------------------------------------------- 1 | import { MirageTestContext } from "ember-cli-mirage/test-support"; 2 | import ProductAreasService from "hermes/services/product-areas"; 3 | 4 | export function startFactories(mirage: MirageTestContext) { 5 | mirage.server.create("product"); 6 | } 7 | 8 | export const setupProductIndex = async (mirage: MirageTestContext) => { 9 | const productAreas = mirage.owner.lookup( 10 | "service:product-areas", 11 | ) as ProductAreasService; 12 | 13 | await productAreas.fetch.perform(); 14 | }; 15 | -------------------------------------------------------------------------------- /web/tests/test-helper.ts: -------------------------------------------------------------------------------- 1 | import Application from 'hermes/app'; 2 | import config from 'hermes/config/environment'; 3 | import * as QUnit from 'qunit'; 4 | import { setApplication } from '@ember/test-helpers'; 5 | import { setup } from 'qunit-dom'; 6 | import { start } from 'ember-qunit'; 7 | import './helpers/flash-message'; 8 | 9 | 10 | setApplication(Application.create(config.APP)); 11 | 12 | setup(QUnit.assert); 13 | 14 | start(); 15 | -------------------------------------------------------------------------------- /web/tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-forge/hermes/caa1b990d237b39a99643f84ec5cbdbc6e086f2b/web/tests/unit/.gitkeep -------------------------------------------------------------------------------- /web/tests/unit/services/latest-docs-test.ts: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | import { setupTest } from "ember-qunit"; 3 | import { MirageTestContext, setupMirage } from "ember-cli-mirage/test-support"; 4 | import { waitUntil } from "@ember/test-helpers"; 5 | import LatestDocsService from "hermes/services/latest-docs"; 6 | 7 | interface Context extends MirageTestContext { 8 | latestDocs: LatestDocsService; 9 | } 10 | 11 | module("Unit | Service | latest", function (hooks) { 12 | setupTest(hooks); 13 | setupMirage(hooks); 14 | 15 | hooks.beforeEach(function (this: Context) { 16 | this.set("latestDocs", this.owner.lookup("service:latest-docs")); 17 | }); 18 | 19 | test("it fetches latest docs", async function (this: Context, assert) { 20 | this.server.createList("document", 10); 21 | 22 | assert.equal(this.latestDocs.index, null, "the index is empty"); 23 | 24 | await this.latestDocs.fetchAll.perform(); 25 | 26 | await waitUntil(() => this.latestDocs.index?.length === 10); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /web/tests/unit/services/viewport-test.ts: -------------------------------------------------------------------------------- 1 | import { setupTest } from "ember-qunit"; 2 | import ViewportService from "hermes/services/viewport"; 3 | import { module, test } from "qunit"; 4 | import window from "ember-window-mock"; 5 | 6 | module("Unit | Service | viewport", function (hooks) { 7 | setupTest(hooks); 8 | 9 | test("it returns the width of the viewport", function (assert) { 10 | const viewport = this.owner.lookup("service:viewport") as ViewportService; 11 | 12 | assert.equal( 13 | viewport.width, 14 | window.innerWidth, 15 | "viewport width is correct", 16 | ); 17 | 18 | const randomWidth = Math.floor(Math.random() * 1000); 19 | 20 | viewport.width = randomWidth; 21 | 22 | assert.equal( 23 | viewport.width, 24 | randomWidth, 25 | "width property updates as expected", 26 | ); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /web/tests/unit/utils/blink-element-test.ts: -------------------------------------------------------------------------------- 1 | import { waitUntil } from "@ember/test-helpers"; 2 | import blinkElement from "hermes/utils/blink-element"; 3 | import { module, test } from "qunit"; 4 | 5 | module("Unit | Utility | blink-element", function () { 6 | test("it blinks an element", async function (assert) { 7 | assert.expect(0); 8 | 9 | const div = document.createElement("div"); 10 | 11 | blinkElement(div); 12 | 13 | await waitUntil(() => div.style.visibility === "hidden"); 14 | await waitUntil(() => div.style.visibility === "visible"); 15 | await waitUntil(() => div.style.visibility === "hidden"); 16 | await waitUntil(() => div.style.visibility === "visible"); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /web/tests/unit/utils/get-product-id-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | import getProductId from "hermes/utils/get-product-id"; 3 | 4 | module("Unit | Utility | get-product-id", function () { 5 | test("it returns the product ID if it exists", function (assert) { 6 | assert.equal(getProductId("Boundary"), "boundary"); 7 | assert.equal(getProductId("Consul"), "consul"); 8 | assert.equal(getProductId("Nomad"), "nomad"); 9 | assert.equal(getProductId("Packer"), "packer"); 10 | assert.equal(getProductId("Terraform"), "terraform"); 11 | assert.equal(getProductId("Vagrant"), "vagrant"); 12 | assert.equal(getProductId("Vault"), "vault"); 13 | assert.equal(getProductId("WAYPOINT"), "waypoint"); 14 | assert.equal(getProductId("Cloud Platform"), "hcp"); 15 | assert.equal(getProductId("Not a product"), null); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /web/tests/unit/utils/get-product-label-test.ts: -------------------------------------------------------------------------------- 1 | import getProductLabel from "hermes/utils/get-product-label"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Utility | get-product-label", function () { 5 | test("it returns the correct label", function (assert) { 6 | assert.equal(getProductLabel("Terraform"), "Terraform"); 7 | assert.equal(getProductLabel("Cloud Platform"), "HCP"); 8 | assert.equal(getProductLabel(), "Unknown"); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /web/tests/unit/utils/html-element-test.ts: -------------------------------------------------------------------------------- 1 | import htmlElement from "hermes/utils/html-element"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Utility | html-element", function () { 5 | test("it asserts and returns valid html elements", function (assert) { 6 | const div = document.createElement("div"); 7 | const button = document.createElement("button"); 8 | 9 | div.classList.add("html-element-test-div"); 10 | button.classList.add("html-element-test-button"); 11 | 12 | document.body.appendChild(div); 13 | document.body.appendChild(button); 14 | 15 | assert.true(htmlElement(".html-element-test-div") instanceof HTMLElement); 16 | assert.true( 17 | htmlElement(".html-element-test-button") instanceof HTMLElement 18 | ); 19 | 20 | assert.throws(() => { 21 | htmlElement(".hope-for-the-future"); 22 | }, "throws an error when an element isn't found"); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /web/tests/unit/utils/is-valid-u-r-l-test.ts: -------------------------------------------------------------------------------- 1 | import isValidURL from "hermes/utils/is-valid-u-r-l"; 2 | import { module, test } from "qunit"; 3 | 4 | module("Unit | Utility | is-valid-u-r-l", function () { 5 | test("it returns whether a url is valid", function (assert) { 6 | const validURL = "https://www.google.com"; 7 | const invalidURL = "xyz"; 8 | 9 | assert.true(isValidURL(validURL)); 10 | assert.false(isValidURL(invalidURL)); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /web/types/ember-animated-tools/animated-tools.d.ts: -------------------------------------------------------------------------------- 1 | declare module "ember-animated-tools/components/animated-tools" { 2 | import Component from "@ember/component"; 3 | 4 | export default class AnimatedTools extends Component {} 5 | } 6 | -------------------------------------------------------------------------------- /web/types/ember-animated/transition-rules.d.ts: -------------------------------------------------------------------------------- 1 | export interface TransitionRules { 2 | firstTime: boolean; 3 | oldItems: unknown[]; 4 | newItems: unknown[]; 5 | } 6 | -------------------------------------------------------------------------------- /web/types/ember-animated/transition.ts: -------------------------------------------------------------------------------- 1 | export type EmberAnimatedTransition = Generator; 2 | -------------------------------------------------------------------------------- /web/types/ember-cli-mirage/test-support.d.ts: -------------------------------------------------------------------------------- 1 | import { TestContext } from "@ember/test-helpers"; 2 | import { Server } from "ember-cli-mirage"; 3 | 4 | export function setupMirage(hooks: NestedHooks): void; 5 | 6 | // Lets you use `this.server` in Mirage tests. 7 | export interface MirageTestContext extends TestContext { 8 | server: Server; 9 | set(key: string, value: T): T; 10 | setProperties>(hash: T): T; 11 | get(key: string): unknown; 12 | getProperties(...args: string[]): Record; 13 | pauseTest(): Promise; 14 | } 15 | -------------------------------------------------------------------------------- /web/types/ember-data/types/registries/model.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Catch-all for ember-data. 3 | */ 4 | export default interface ModelRegistry { 5 | [key: string]: any; 6 | } 7 | -------------------------------------------------------------------------------- /web/types/ember-metrics/metrics-adapters/google-analytics-four.d.ts: -------------------------------------------------------------------------------- 1 | interface BaseMetricsAdapter { 2 | constructor(config: any); 3 | install(): void; 4 | } 5 | 6 | declare module "ember-metrics/metrics-adapters/google-analytics-four" { 7 | export default class GoogleAnalyticsFour extends BaseMetricsAdapter { 8 | constructor(config: any); 9 | install(): void; 10 | gtag(...args: any[]): void; 11 | options: any; 12 | config: { 13 | id: string; 14 | }; 15 | _injectScript(GoogleAnalyticsTagID: string): void; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /web/types/ember-metrics/services/metrics.d.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/adopted-ember-addons/ember-metrics/blob/master/addon/services/metrics.js 2 | 3 | import Service from "@ember/service"; 4 | 5 | declare module "ember-metrics/services/metrics" { 6 | // Note: Incomplete types; only the methods we use are defined. 7 | export default class EmberMetricsService extends Service { 8 | _adapters: any; 9 | _options: any; 10 | _activateAdapter({}): unknown; 11 | _lookupAdapter(adapterName: string): any; 12 | trackPage(): void; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /web/types/ember-page-title/index.ts: -------------------------------------------------------------------------------- 1 | import { HelperLike } from "@glint/template"; 2 | 3 | export type EmberPageTitleHelper = HelperLike<{ 4 | Args: { 5 | Positional: [string]; 6 | }; 7 | Return: void; 8 | }>; 9 | -------------------------------------------------------------------------------- /web/types/ember-page-title/test-support/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "ember-page-title/test-support" { 2 | import { getPageTitle } from "ember-page-title/test-support"; 3 | export function getPageTitle(): string; 4 | } 5 | -------------------------------------------------------------------------------- /web/types/ember-set-body-class/index.ts: -------------------------------------------------------------------------------- 1 | import { HelperLike } from "@glint/template"; 2 | 3 | export type EmberSetBodyClassHelper = HelperLike<{ 4 | Args: { 5 | Positional: [string]; 6 | }; 7 | Return: void; 8 | }>; 9 | -------------------------------------------------------------------------------- /web/types/ember-simple-auth/services/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "ember-simple-auth/test-support" { 2 | import { SessionAuthenticatedData } from "ember-simple-auth/services/session"; 3 | export function authenticateSession( 4 | responseFromApi: SessionAuthenticatedData 5 | ): Promise; 6 | export function invalidateSession(): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /web/types/global.d.ts: -------------------------------------------------------------------------------- 1 | // Types for compiled templates 2 | declare module 'hermes/templates/*' { 3 | import { TemplateFactory } from 'ember-cli-htmlbars'; 4 | 5 | const tmpl: TemplateFactory; 6 | export default tmpl; 7 | } 8 | -------------------------------------------------------------------------------- /web/types/hds/alert.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/alert?tab=code#component-api 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | import { 5 | HdsButtonColor, 6 | HdsIconPosition, 7 | HdsComponentSize, 8 | HdsAlertColor, 9 | } from "hds/_shared"; 10 | 11 | interface HdsAlertComponentSignature { 12 | Element: HTMLDivElement; 13 | Args: { 14 | type: "page" | "inline" | "compact"; 15 | color?: HdsAlertColor; 16 | icon?: string | false; 17 | onDismiss?: () => void; 18 | }; 19 | Blocks: { 20 | default: [ 21 | A: { 22 | // TODO: Type these 23 | Title: any; 24 | Description: any; 25 | Button: any; 26 | "Link::Standalone": any; 27 | Generic: any; 28 | } 29 | ]; 30 | }; 31 | } 32 | 33 | export type HdsAlertComponent = ComponentLike; 34 | -------------------------------------------------------------------------------- /web/types/hds/badge-count.d.ts: -------------------------------------------------------------------------------- 1 | // helios.hashicorp.design/components/badge-count?tab=code#component-api 2 | 3 | declare module "@hashicorp/design-system-components/components/hds/badge-count" { 4 | import Component from "@glimmer/component"; 5 | import { ComponentLike } from "@glint/template"; 6 | import { HdsBadgeCountColor, HdsBadgeType } from "hds/_shared"; 7 | import { HdsBadgeCountSize } from "hermes/types/HdsBadgeCountSize"; 8 | 9 | interface HdsBadgeCountComponentSignature { 10 | Element: HTMLDivElement; 11 | Args: { 12 | text: string; 13 | size?: HdsBadgeCountSize; 14 | type?: HdsBadgeType; 15 | color?: HdsBadgeCountColor; 16 | }; 17 | } 18 | 19 | export type HdsBadgeCountComponent = 20 | ComponentLike; 21 | export default class HdsBadgeCount extends Component {} 22 | } 23 | -------------------------------------------------------------------------------- /web/types/hds/badge.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/badge?tab=code#component-api 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | import { HdsBadgeColor, HdsBadgeType, HdsComponentSize } from "hds/_shared"; 5 | 6 | interface HdsBadgeComponentSignature { 7 | Element: HTMLDivElement; 8 | Args: { 9 | text: string; 10 | color?: HdsBadgeColor; 11 | type?: HdsBadgeType; 12 | size?: HdsComponentSize; 13 | icon?: string; 14 | isIconOnly?: boolean; 15 | }; 16 | } 17 | 18 | export type HdsBadgeComponent = ComponentLike; 19 | -------------------------------------------------------------------------------- /web/types/hds/button-set.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/button-set?tab=code#component-api 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | 5 | interface HdsButtonSetComponentSignature { 6 | Element: HTMLDivElement; 7 | Args: {}; 8 | Blocks: { 9 | default: []; 10 | } 11 | } 12 | 13 | export type HdsButtonSetComponent = 14 | ComponentLike; 15 | -------------------------------------------------------------------------------- /web/types/hds/button.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/button?tab=code#component-api 2 | declare module "@hashicorp/design-system-components/components/hds/button" { 3 | import Component from "@glimmer/component"; 4 | import { ComponentLike } from "@glint/template"; 5 | import { 6 | HdsButtonColor, 7 | HdsIconPosition, 8 | HdsComponentSize, 9 | HdsAnchorComponentArgs, 10 | } from "hds/_shared"; 11 | 12 | interface HdsButtonComponentSignature { 13 | Element: HTMLButtonElement; 14 | Args: HdsAnchorComponentArgs & { 15 | text: string; 16 | size?: HdsComponentSize; 17 | color?: HdsButtonColor; 18 | isIconOnly?: boolean; 19 | isFullWidth?: boolean; 20 | isRouteExternal?: boolean; 21 | }; 22 | } 23 | 24 | export type HdsButtonComponent = ComponentLike; 25 | export default class HdsButton extends Component {} 26 | } 27 | -------------------------------------------------------------------------------- /web/types/hds/card/container.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/card?tab=code#component-api 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | import { HdsCardBackgroundColor, HdsComponentOverflow, HdsComponentShadowLevel } from "hds/_shared"; 5 | 6 | interface HdsCardContainerComponentSignature { 7 | Element: HTMLDivElement; 8 | Args: { 9 | level?: HdsComponentShadowLevel; 10 | levelHover?: HdsComponentShadowLevel; 11 | levelActive?: HdsComponentShadowLevel; 12 | background?: HdsCardBackgroundColor; 13 | hasBorder?: boolean; 14 | overflow?: HdsComponentOverflow; 15 | }; 16 | Blocks: { 17 | default: []; 18 | } 19 | } 20 | 21 | export type HdsCardContainerComponent = 22 | ComponentLike; 23 | -------------------------------------------------------------------------------- /web/types/hds/dropdown.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/toast?tab=code#component-api 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | import { 5 | HdsBadgeColor, 6 | HdsBadgeType, 7 | HdsComponentSize, 8 | } from "hermes/enums/hds-components"; 9 | 10 | interface HdsBadgeComponentSignature { 11 | Element: HTMLDivElement; 12 | Args: { 13 | color?: HdsBadgeColor; 14 | type?: HdsBadgeType; 15 | size?: HdsComponentSize; 16 | text?: string; 17 | icon?: string; 18 | isIconOnly?: boolean; 19 | }; 20 | } 21 | 22 | export type HdsBadgeComponent = ComponentLike; 23 | -------------------------------------------------------------------------------- /web/types/hds/flight-icon.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@hashicorp/ember-flight-icons/components/flight-icon" { 2 | import Component from "@glimmer/component"; 3 | import { ComponentLike } from "@glint/template"; 4 | 5 | // https://helios.hashicorp.design/icons/usage-guidelines?tab=code 6 | 7 | interface FlightIconComponentSignature { 8 | Element: SVGElement; 9 | Args: { 10 | name: string; 11 | size?: "16" | "24"; 12 | color?: string; 13 | stretched?: boolean; 14 | }; 15 | } 16 | 17 | export type FlightIconComponent = ComponentLike; 18 | export default class FlightIcon extends Component {} 19 | } 20 | -------------------------------------------------------------------------------- /web/types/hds/form/checkbox/fields.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/form/checkbox?tab=code#formcheckboxfield-1 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | 5 | interface HdsFormCheckboxFieldComponentSignature { 6 | Element: HTMLInputElement; 7 | Args: { 8 | id?: string; 9 | extraDescribedBy?: string; 10 | }; 11 | Blocks: { 12 | default: [ 13 | F: { 14 | // TODO: Type these 15 | Label: any; 16 | Error: any; 17 | HelperText: any; 18 | } 19 | ]; 20 | } 21 | } 22 | 23 | export type HdsFormCheckboxFieldComponent = 24 | ComponentLike; 25 | -------------------------------------------------------------------------------- /web/types/hds/form/error/index.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/form/primitives?tab=code#formerror-1 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | import { HdsFormTextInputArgs } from "."; 5 | 6 | export interface HdsFormErrorComponentSignature { 7 | // Only the default invocation is typed. 8 | // We'll add the multiple-messages types if we need them. 9 | Element: HTMLDivElement; 10 | Args: { 11 | controlID?: string; 12 | }; 13 | Blocks: { 14 | default: []; 15 | }; 16 | } 17 | 18 | export type HdsFormErrorComponent = 19 | ComponentLike; 20 | -------------------------------------------------------------------------------- /web/types/hds/form/field.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/form/primitives?tab=code#formfield-1 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | import { HdsFormFieldLayout } from "hds/_shared"; 5 | 6 | interface HdsFormFieldComponentSignature { 7 | Element: HTMLDivElement; 8 | Args: { 9 | layout?: HdsFormFieldLayout; 10 | id?: string; 11 | extraDescribedBy?: string; 12 | isRequired?: boolean; 13 | isOptional?: boolean; 14 | }; 15 | Blocks: { 16 | default: [ 17 | F: { 18 | // TODO: Type these 19 | Label: any; 20 | HelperText: any; 21 | Error: any; 22 | Control: any; 23 | } 24 | ]; 25 | }; 26 | } 27 | 28 | export type HdsFormFieldComponent = 29 | ComponentLike; 30 | -------------------------------------------------------------------------------- /web/types/hds/form/label.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/form/primitives?tab=code#formlabel-2 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | 5 | interface HdsFormLabelComponentSignature { 6 | Element: HTMLLabelElement; 7 | Args: { 8 | controlId?: string; 9 | isRequired?: boolean; 10 | isOptional?: boolean; 11 | }; 12 | Blocks: { 13 | default: []; 14 | }; 15 | } 16 | 17 | export type HdsFormLabelComponent = 18 | ComponentLike; 19 | -------------------------------------------------------------------------------- /web/types/hds/form/text-input/base.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/form/text-input?tab=code#formtextinputbase-1 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | import { HdsFormTextInputArgs } from "."; 5 | 6 | export interface HdsFormTextInputBaseComponentSignature { 7 | Element: HTMLInputElement; 8 | Args: { 9 | type?: string; 10 | value: string | number | Date; 11 | isInvalid?: boolean; 12 | width?: string; 13 | }; 14 | } 15 | 16 | export type HdsFormTextInputBaseComponent = 17 | ComponentLike; 18 | -------------------------------------------------------------------------------- /web/types/hds/form/text-input/field.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/form/text-input?tab=code#formtextinputfield-1 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | import { HdsFormTextInputBaseSignature } from "./base"; 5 | 6 | interface HdsFormTextInputFieldComponentSignature { 7 | Element: HdsFormTextInputBaseComponentSignature["Element"]; 8 | Args: HdsFormTextInputBaseComponentSignature["Args"] & { 9 | isRequired?: boolean; 10 | isOptional?: boolean; 11 | id?: string; 12 | extraAriaDescribedBy?: string; 13 | }; 14 | Blocks: { 15 | default: [ 16 | F: { 17 | // TODO: Type these 18 | Label: any; 19 | HelperText: any; 20 | Error: any; 21 | } 22 | ]; 23 | }; 24 | } 25 | 26 | export type HdsFormTextInputFieldComponent = 27 | ComponentLike; 28 | -------------------------------------------------------------------------------- /web/types/hds/form/textarea/field.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/form/textarea?tab=code#formtextareafield-1 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | 5 | interface HdsFormTextareaFieldComponentSignature { 6 | Element: HTMLTextAreaElement; 7 | Args: { 8 | value: string; 9 | id?: string; 10 | isInvalid?: boolean; 11 | isRequired?: boolean; 12 | isOptional?: boolean; 13 | extraAriaDescribedBy?: string; 14 | }; 15 | Blocks: { 16 | default: [ 17 | F: { 18 | // TODO: Type these 19 | Label: any; 20 | HelperText: any; 21 | Error: any; 22 | } 23 | ]; 24 | }; 25 | } 26 | 27 | export type HdsFormTextareaFieldComponent = 28 | ComponentLike; 29 | -------------------------------------------------------------------------------- /web/types/hds/form/toggle/base.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/form/toggle?tab=code#formtogglebase-1 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | 5 | interface HdsFormToggleBaseComponentSignature { 6 | Element: HTMLInputElement; 7 | Args: {}; 8 | Blocks: { 9 | default: [ 10 | F: { 11 | // TODO: Type these 12 | Label: any; 13 | HelperText: any; 14 | Error: any; 15 | } 16 | ]; 17 | }; 18 | } 19 | 20 | export type HdsFormToggleBaseComponent = 21 | ComponentLike; 22 | -------------------------------------------------------------------------------- /web/types/hds/icon-tile.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/icon-tile?tab=code#component-api 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | import { 5 | HdsComponentSize, 6 | HdsIconTileColor, 7 | HdsProductLogoName, 8 | } from "hds/_shared"; 9 | 10 | interface HdsIconTileComponentSignature { 11 | Element: HTMLDivElement; 12 | Args: { 13 | size?: HdsComponentSize; 14 | logo?: HdsProductLogoName; 15 | icon?: string; 16 | iconSecondary?: string; 17 | color?: HdsIconTileColor; 18 | }; 19 | } 20 | 21 | export type HdsIconTileComponent = ComponentLike; 22 | -------------------------------------------------------------------------------- /web/types/hds/link/inline.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/link/inline?tab=code#component-api 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | import { 5 | HdsAnchorComponentArgs, 6 | HdsIconPosition, 7 | HdsLinkColor, 8 | } from "hds/_shared"; 9 | 10 | export interface HdsLinkComponentSignature { 11 | Element: HTMLAnchorElement; 12 | Args: HdsAnchorComponentArgs & { 13 | color?: HdsLinkColor; 14 | }; 15 | Blocks: { 16 | default: []; 17 | }; 18 | } 19 | 20 | export type HdsLinkInlineComponent = ComponentLike; 21 | -------------------------------------------------------------------------------- /web/types/hds/link/standalone.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/link/standalone?tab=code#component-api 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | import { HdsLinkComponentSignature } from "./inline"; 5 | import { HdsComponentSize } from "hds/_shared"; 6 | 7 | interface HdsLinkStandaloneComponentSignature { 8 | Element: HdsLinkComponentSignature["Element"]; 9 | Args: HdsLinkComponentSignature["Args"] & { 10 | text: string; 11 | size?: HdsComponentSize; 12 | }; 13 | Blocks: HdsLinkComponentSignature["Blocks"]; 14 | } 15 | 16 | export type HdsLinkStandaloneComponent = 17 | ComponentLike; 18 | -------------------------------------------------------------------------------- /web/types/hds/modal.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/modal?tab=code#component-api 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | import { 5 | HdsComponentSize, 6 | HdsIconPosition, 7 | HdsLinkColor, 8 | HdsModalColor, 9 | } from "hds/_shared"; 10 | 11 | export interface HdsModalComponentSignature { 12 | Element: HTMLDialogElement; 13 | Args: { 14 | size?: HdsComponentSize; 15 | color?: HdsModalColor; 16 | onOpen?: () => void; 17 | onClose?: () => void; 18 | isDismissDisabled?: boolean; 19 | }; 20 | Blocks: { 21 | default: [ 22 | M: { 23 | // TODO: Type these 24 | Header: any; 25 | Body: any; 26 | Footer: any; 27 | } 28 | ]; 29 | }; 30 | } 31 | 32 | export type HdsModalComponent = ComponentLike; 33 | -------------------------------------------------------------------------------- /web/types/hds/table/index.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/table?tab=code#table 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | import { 5 | HdsTableDensity, 6 | HdsTableSortOrder, 7 | HdsTableVerticalAlignment, 8 | } from "hds/_shared"; 9 | 10 | interface HdsTableComponentSignature { 11 | Element: HTMLTableElement; 12 | Args: { 13 | sortBy?: string; 14 | sortOrder?: HdsTableSortOrder; 15 | isStriped?: boolean; 16 | isFixedLayout?: boolean; 17 | density?: HdsTableDensity; 18 | valign?: HdsTableVerticalAlignment; 19 | caption?: string; 20 | identityKey?: string; 21 | sortedMessageText?: string; 22 | onSort?: () => void; 23 | 24 | // TODO: Type these 25 | model?: any; 26 | columns?: any; 27 | }; 28 | Blocks: { 29 | // TODO: Type these 30 | head: any; 31 | body: any; 32 | }; 33 | } 34 | 35 | export type HdsTableComponent = ComponentLike; 36 | -------------------------------------------------------------------------------- /web/types/hds/table/td.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/table?tab=code#tabletd 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | import { HdsTableHorizontalAlignment } from "hds/_shared"; 5 | 6 | interface HdsTableTdComponentSignature { 7 | Element: HTMLTableDataCellElement; 8 | Args: { 9 | align?: HdsTableHorizontalAlignment; 10 | }; 11 | } 12 | 13 | export type HdsTableTdComponent = ComponentLike; 14 | -------------------------------------------------------------------------------- /web/types/hds/table/tr.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/table?tab=code#tabletr 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | 5 | interface HdsTableTrComponentSignature { 6 | Element: HTMLTableRowElement; 7 | Args: {}; 8 | } 9 | 10 | export type HdsTableTrComponent = ComponentLike; 11 | -------------------------------------------------------------------------------- /web/types/hds/toast.d.ts: -------------------------------------------------------------------------------- 1 | // https://helios.hashicorp.design/components/toast?tab=code#component-api 2 | 3 | import { ComponentLike } from "@glint/template"; 4 | import { HdsAlertComponent } from "./alert"; 5 | 6 | interface HdsToastComponentSignature { 7 | Element: HTMLDivElement; 8 | Args: HdsAlertComponent["Args"]; 9 | Blocks: HdsAlertComponent["Blocks"]; 10 | } 11 | 12 | export type HdsToastComponent = ComponentLike; 13 | -------------------------------------------------------------------------------- /web/types/hermes/index.d.ts: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | declare global { 4 | // Prevents ESLint from "fixing" this via its auto-fix to turn it into a type 5 | // alias (e.g. after running any Ember CLI generator) 6 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 7 | interface Array extends Ember.ArrayPrototypeExtensions {} 8 | // interface Function extends Ember.FunctionPrototypeExtensions {} 9 | } 10 | 11 | export {}; 12 | -------------------------------------------------------------------------------- /web/vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp-forge/hermes/caa1b990d237b39a99643f84ec5cbdbc6e086f2b/web/vendor/.gitkeep --------------------------------------------------------------------------------