]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-14617 Embed the sonar-ui-common library
authorPhilippe Perrin <philippe.perrin@sonarsource.com>
Thu, 15 Jul 2021 13:55:30 +0000 (15:55 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 21 Jul 2021 20:03:01 +0000 (20:03 +0000)
387 files changed:
server/sonar-ui-common/.eslintrc [new file with mode: 0644]
server/sonar-ui-common/HEADER [new file with mode: 0644]
server/sonar-ui-common/components/__tests__/__snapshots__/lazyLoadComponent-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/__tests__/getHistory-test.ts [new file with mode: 0644]
server/sonar-ui-common/components/__tests__/lazyLoadComponent-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/AdvancedTimeline.css [new file with mode: 0644]
server/sonar-ui-common/components/charts/AdvancedTimeline.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/BarChart.css [new file with mode: 0644]
server/sonar-ui-common/components/charts/BarChart.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/BubbleChart.css [new file with mode: 0644]
server/sonar-ui-common/components/charts/BubbleChart.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/ColorGradientLegend.css [new file with mode: 0644]
server/sonar-ui-common/components/charts/ColorGradientLegend.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/DonutChart.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/Histogram.css [new file with mode: 0644]
server/sonar-ui-common/components/charts/Histogram.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/LineChart.css [new file with mode: 0644]
server/sonar-ui-common/components/charts/LineChart.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/TreeMap.css [new file with mode: 0644]
server/sonar-ui-common/components/charts/TreeMap.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/TreeMapRect.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/ZoomTimeLine.css [new file with mode: 0644]
server/sonar-ui-common/components/charts/ZoomTimeLine.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/__tests__/AdvancedTimeline-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/__tests__/BarChart-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/__tests__/BubbleChart-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/__tests__/ColorGradientLegend-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/__tests__/DonutChart-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/__tests__/Histogram-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/__tests__/LineChart-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/__tests__/TreeMap-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/__tests__/ZoomTimeLine-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/charts/__tests__/__snapshots__/AdvancedTimeline-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/charts/__tests__/__snapshots__/ColorGradientLegend-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/charts/__tests__/__snapshots__/DonutChart-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/charts/__tests__/__snapshots__/Histogram-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/ActionsDropdown.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/BackButton.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/BoxedGroupAccordion.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/BoxedTabs.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/Checkbox.css [new file with mode: 0644]
server/sonar-ui-common/components/controls/Checkbox.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/ClickEventBoundary.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/ConfirmButton.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/ConfirmModal.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/DocumentClickHandler.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/Dropdown.css [new file with mode: 0644]
server/sonar-ui-common/components/controls/Dropdown.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/EscKeydownHandler.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/FavoriteButton.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/GlobalMessages.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/HelpTooltip.css [new file with mode: 0644]
server/sonar-ui-common/components/controls/HelpTooltip.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/IdentityProviderLink.css [new file with mode: 0644]
server/sonar-ui-common/components/controls/IdentityProviderLink.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/InputValidationField.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/ListFooter.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/Modal.css [new file with mode: 0644]
server/sonar-ui-common/components/controls/Modal.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/ModalButton.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/ModalValidationField.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/OutsideClickHandler.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/Radio.css [new file with mode: 0644]
server/sonar-ui-common/components/controls/Radio.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/RadioCard.css [new file with mode: 0644]
server/sonar-ui-common/components/controls/RadioCard.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/RadioToggle.css [new file with mode: 0644]
server/sonar-ui-common/components/controls/RadioToggle.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/ReloadButton.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/ScreenPositionFixer.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/SearchBox.css [new file with mode: 0644]
server/sonar-ui-common/components/controls/SearchBox.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/SearchSelect.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/Select.css [new file with mode: 0644]
server/sonar-ui-common/components/controls/Select.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/SelectList.css [new file with mode: 0644]
server/sonar-ui-common/components/controls/SelectList.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/SelectListListContainer.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/SelectListListElement.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/SimpleModal.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/Tabs.css [new file with mode: 0644]
server/sonar-ui-common/components/controls/Tabs.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/Toggle.css [new file with mode: 0644]
server/sonar-ui-common/components/controls/Toggle.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/Toggler.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/Tooltip.css [new file with mode: 0644]
server/sonar-ui-common/components/controls/Tooltip.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/ValidationForm.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/ValidationInput.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/ValidationModal.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/ActionsDropdown-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/BackButton-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/BoxedGroupAccordion-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/BoxedTabs-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/Checkbox-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/ClickEventBoundary-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/ConfirmButton-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/ConfirmModal-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/Dropdown-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/EscKeydownHandler-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/FavoriteButton-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/GlobalMessages-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/HelpTooltip-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/IdentityProviderLink-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/InputValidationField-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/ListFooter-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/ModalButton-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/ModalValidationField-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/Radio-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/RadioCard-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/RadioToggle-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/ReloadButton-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/ScreenPositionFixer-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/SearchBox-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/SearchSelect-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/SelectList-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/SelectListListContainer-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/SelectListListElement-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/SimpleModal-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/Tabs-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/Toggle-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/Toggler-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/Tooltip-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/ValidationForm-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/ValidationInput-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/ValidationModal-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/ActionsDropdown-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/BackButton-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/BoxedGroupAccordion-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/BoxedTabs-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/Checkbox-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/ClickEventBoundary-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/ConfirmButton-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/ConfirmModal-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/EscKeydownHandler-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/FavoriteButton-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/GlobalMessages-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/HelpTooltip-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/IdentityProviderLink-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/InputValidationField-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/ListFooter-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/ModalValidationField-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/Radio-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/RadioCard-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/RadioToggle-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/ReloadButton-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/SearchBox-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/SearchSelect-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/SelectList-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/SelectListListContainer-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/SelectListListElement-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/SimpleModal-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/Tabs-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/Toggle-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/Toggler-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/Tooltip-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/ValidationForm-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/ValidationModal-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/buttons-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/__snapshots__/clipboard-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/buttons-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/__tests__/clipboard-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/buttons.css [new file with mode: 0644]
server/sonar-ui-common/components/controls/buttons.tsx [new file with mode: 0644]
server/sonar-ui-common/components/controls/clipboard.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/AlertErrorIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/AlertSuccessIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/AlertWarnIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/ArrowIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/BackIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/BranchIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/BubblesIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/BugIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/BugTrackerIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/BulletListIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/CalendarIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/ChartLegendIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/CheckIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/ChevronDownIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/ChevronLeftIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/ChevronRightIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/ChevronUpIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/ChevronsIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/ClearIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/ClockIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/CodeSmellIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/CogIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/CollapseIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/ContinuousIntegrationIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/CopyIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/DeleteIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/DetachIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/DropdownIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/EditIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/EllipsisIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/ExpandIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/ExpandSnippetIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/FavoriteIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/FilterIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/GroupIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/HelpIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/HistoryIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/HomeIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/HouseIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/Icon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/InfoIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/IssueIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/IssueTypeIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/LightBulbIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/LinkIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/ListIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/LockIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/LongLivingBranchIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/MeasuresIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/MinimizeIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/NotificationIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/OnboardingAddMembersIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/OnboardingProjectIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/OnboardingTeamIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/OpenCloseIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/PendingIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/PinIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/PlusCircleIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/PlusIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/ProjectEventIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/ProjectLinkIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/PullRequestIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/QualifierIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/RecommendedIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/RocketIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/RuleScopeIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/SCMIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/SearchIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/SecurityHotspotIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/SettingsIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/SeverityIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/ShortLivingBranchIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/SortAscIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/SortDescIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/StatusIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/TagsIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/TestStatusIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/TreeIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/TreemapIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/VisibleIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/VulnerabilityIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/WarningIcon.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/__tests__/Icon-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/__tests__/IssueIcon-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/__tests__/IssueTypeIcon-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/__tests__/TestStatusIcon-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/icons/__tests__/__snapshots__/Icon-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/icons/__tests__/__snapshots__/IssueIcon-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/icons/__tests__/__snapshots__/IssueTypeIcon-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/icons/__tests__/__snapshots__/TestStatusIcon-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/intl/DateFormatter.tsx [new file with mode: 0644]
server/sonar-ui-common/components/intl/DateFromNow.tsx [new file with mode: 0644]
server/sonar-ui-common/components/intl/DateTimeFormatter.tsx [new file with mode: 0644]
server/sonar-ui-common/components/intl/TimeFormatter.tsx [new file with mode: 0644]
server/sonar-ui-common/components/intl/__mocks__/DateFromNow.tsx [new file with mode: 0644]
server/sonar-ui-common/components/intl/__tests__/DateFormatter-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/intl/__tests__/DateFromNow-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/intl/__tests__/DateTimeFormatter-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/intl/__tests__/TimeFormatter-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/intl/__tests__/__snapshots__/DateFormatter-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/intl/__tests__/__snapshots__/DateFromNow-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/intl/__tests__/__snapshots__/DateTimeFormatter-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/intl/__tests__/__snapshots__/TimeFormatter-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/lazyLoadComponent.tsx [new file with mode: 0644]
server/sonar-ui-common/components/theme.ts [new file with mode: 0644]
server/sonar-ui-common/components/ui/Alert.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/AutoEllipsis.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/ContextNavBar.css [new file with mode: 0644]
server/sonar-ui-common/components/ui/ContextNavBar.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/DeferredSpinner.css [new file with mode: 0644]
server/sonar-ui-common/components/ui/DeferredSpinner.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/DuplicationsRating.css [new file with mode: 0644]
server/sonar-ui-common/components/ui/DuplicationsRating.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/FilesCounter.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/GenericAvatar.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/Level.css [new file with mode: 0644]
server/sonar-ui-common/components/ui/Level.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/MandatoryFieldMarker.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/MandatoryFieldsExplanation.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/NavBar.css [new file with mode: 0644]
server/sonar-ui-common/components/ui/NavBar.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/NavBarTabs.css [new file with mode: 0644]
server/sonar-ui-common/components/ui/NavBarTabs.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/NewsBox.css [new file with mode: 0644]
server/sonar-ui-common/components/ui/NewsBox.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/PageActions.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/Rating.css [new file with mode: 0644]
server/sonar-ui-common/components/ui/Rating.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/SizeRating.css [new file with mode: 0644]
server/sonar-ui-common/components/ui/SizeRating.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/Alert-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/AutoEllipsis-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/DeferredSpinner-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/FilesCounter-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/GenericAvatar-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/Level-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/MandatoryFieldMarker-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/MandatoryFieldsExplanation-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/NavBar-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/NewsBox-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/PageActions-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/Rating-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/SizeRating-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/__snapshots__/AutoEllipsis-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/__snapshots__/DeferredSpinner-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/__snapshots__/FilesCounter-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/__snapshots__/GenericAvatar-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/__snapshots__/Level-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/__snapshots__/MandatoryFieldMarker-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/__snapshots__/MandatoryFieldsExplanation-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/__snapshots__/NavBar-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/__snapshots__/NewsBox-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/__snapshots__/PageActions-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/__snapshots__/Rating-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/__snapshots__/SizeRating-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/__snapshots__/popups-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/ui/__tests__/popups-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/popups.css [new file with mode: 0644]
server/sonar-ui-common/components/ui/popups.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/update-center/MetaData.css [new file with mode: 0644]
server/sonar-ui-common/components/ui/update-center/MetaData.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/update-center/MetaDataVersion.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/update-center/MetaDataVersions.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/update-center/__tests__/MetaData-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/update-center/__tests__/MetaDataVersion-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/update-center/__tests__/MetaDataVersions-test.tsx [new file with mode: 0644]
server/sonar-ui-common/components/ui/update-center/__tests__/__snapshots__/MetaData-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/ui/update-center/__tests__/__snapshots__/MetaDataVersion-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/ui/update-center/__tests__/__snapshots__/MetaDataVersions-test.tsx.snap [new file with mode: 0644]
server/sonar-ui-common/components/ui/update-center/mocks/update-center-metadata.ts [new file with mode: 0644]
server/sonar-ui-common/components/ui/update-center/update-center-metadata.ts [new file with mode: 0644]
server/sonar-ui-common/config/jest/CSSStub.js [new file with mode: 0644]
server/sonar-ui-common/config/jest/FileStub.js [new file with mode: 0644]
server/sonar-ui-common/config/jest/SetupEnzyme.js [new file with mode: 0644]
server/sonar-ui-common/config/jest/SetupSUC.ts [new file with mode: 0644]
server/sonar-ui-common/config/jest/SetupTestEnvironment.js [new file with mode: 0644]
server/sonar-ui-common/config/jest/testTheme.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/__tests__/__snapshots__/query-test.ts.snap [new file with mode: 0644]
server/sonar-ui-common/helpers/__tests__/colors-test.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/__tests__/dates-test.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/__tests__/handleRequiredAuthentication-test.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/__tests__/init-test.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/__tests__/l10n-test.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/__tests__/measures-test.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/__tests__/path-test.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/__tests__/query-test.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/__tests__/request-test.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/__tests__/scrolling-test.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/__tests__/strings-test.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/__tests__/urls-test.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/colors.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/cookies.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/csv.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/dates.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/getHistory.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/handleRequiredAuthentication.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/init.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/keycodes.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/l10n.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/measures.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/pages.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/path.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/query.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/ratings.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/request.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/scrolling.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/search.tsx [new file with mode: 0644]
server/sonar-ui-common/helpers/storage.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/strings.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/testUtils.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/types.ts [new file with mode: 0644]
server/sonar-ui-common/helpers/urls.ts [new file with mode: 0644]
server/sonar-ui-common/package.json [new file with mode: 0644]
server/sonar-ui-common/scripts/build.sh [new file with mode: 0755]
server/sonar-ui-common/scripts/license-check.js [new file with mode: 0644]
server/sonar-ui-common/scripts/release.sh [new file with mode: 0755]
server/sonar-ui-common/tsconfig.json [new file with mode: 0644]
server/sonar-ui-common/types.d.ts [new file with mode: 0644]
server/sonar-ui-common/yarn.lock [new file with mode: 0644]

diff --git a/server/sonar-ui-common/.eslintrc b/server/sonar-ui-common/.eslintrc
new file mode 100644 (file)
index 0000000..f3c47f9
--- /dev/null
@@ -0,0 +1,3 @@
+{
+  "extends": "sonarqube"
+}
diff --git a/server/sonar-ui-common/HEADER b/server/sonar-ui-common/HEADER
new file mode 100644 (file)
index 0000000..e288f64
--- /dev/null
@@ -0,0 +1,19 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
\ No newline at end of file
diff --git a/server/sonar-ui-common/components/__tests__/__snapshots__/lazyLoadComponent-test.tsx.snap b/server/sonar-ui-common/components/__tests__/__snapshots__/lazyLoadComponent-test.tsx.snap
new file mode 100644 (file)
index 0000000..7f00f47
--- /dev/null
@@ -0,0 +1,46 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should correctly set given display name 1`] = `
+<div>
+  <CustomDisplayName />
+</div>
+`;
+
+exports[`should lazy load and display the component 1`] = `
+<LazyComponentWrapper>
+  <LazyErrorBoundary>
+    <Suspense
+      fallback={null}
+    />
+  </LazyErrorBoundary>
+</LazyComponentWrapper>
+`;
+
+exports[`should lazy load and display the component 2`] = `null`;
+
+exports[`should lazy load and display the component 3`] = `
+<LazyComponentWrapper>
+  <LazyErrorBoundary>
+    <Suspense
+      fallback={null}
+    >
+      <Checkbox>
+        <a
+          className="icon-checkbox"
+          href="#"
+          onClick={[Function]}
+          role="checkbox"
+        />
+      </Checkbox>
+    </Suspense>
+  </LazyErrorBoundary>
+</LazyComponentWrapper>
+`;
+
+exports[`should lazy load and display the component 4`] = `
+<a
+  class="icon-checkbox"
+  href="#"
+  role="checkbox"
+/>
+`;
diff --git a/server/sonar-ui-common/components/__tests__/getHistory-test.ts b/server/sonar-ui-common/components/__tests__/getHistory-test.ts
new file mode 100644 (file)
index 0000000..251db85
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import getHistory from '../../helpers/getHistory';
+import * as Router from 'react-router';
+
+it('should get browser history properly', () => {
+  expect(getHistory()).not.toBeUndefined();
+  expect(getHistory().getCurrentLocation().pathname).toBe('/');
+  const pathname = '/foo/bar';
+  Router.browserHistory.push(pathname);
+  expect(getHistory().getCurrentLocation().pathname).toBe(pathname);
+});
diff --git a/server/sonar-ui-common/components/__tests__/lazyLoadComponent-test.tsx b/server/sonar-ui-common/components/__tests__/lazyLoadComponent-test.tsx
new file mode 100644 (file)
index 0000000..9528fb8
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { mount, shallow } from 'enzyme';
+import * as React from 'react';
+import { waitAndUpdate } from '../../helpers/testUtils';
+import { lazyLoadComponent } from '../lazyLoadComponent';
+
+const factory = jest.fn().mockImplementation(() => import('../controls/Checkbox'));
+
+beforeEach(() => {
+  factory.mockClear();
+});
+
+it('should lazy load and display the component', async () => {
+  const LazyComponent = lazyLoadComponent(factory);
+  const wrapper = mount(<LazyComponent />);
+  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.render()).toMatchSnapshot();
+  expect(factory).toBeCalledTimes(1);
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.render()).toMatchSnapshot();
+  expect(factory).toBeCalledTimes(1);
+});
+
+it('should correctly handle import errors', () => {
+  const LazyComponent = lazyLoadComponent(factory);
+  const wrapper = mount(<LazyComponent />);
+  wrapper.find('Suspense').simulateError({ request: 'test' });
+  expect(wrapper.find('Alert').exists()).toBe(true);
+});
+
+it('should correctly set given display name', () => {
+  const LazyComponent = lazyLoadComponent(factory, 'CustomDisplayName');
+  const wrapper = shallow(
+    <div>
+      <LazyComponent />
+    </div>
+  );
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-ui-common/components/charts/AdvancedTimeline.css b/server/sonar-ui-common/components/charts/AdvancedTimeline.css
new file mode 100644 (file)
index 0000000..e372b12
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.line-tooltip {
+  fill: none;
+  stroke: var(--secondFontColor);
+  stroke-width: 1px;
+  shape-rendering: crispEdges;
+}
+
+.chart-mouse-events-overlay {
+  fill: none;
+  stroke: none;
+  pointer-events: all;
+}
+
+.chart-zoomed .line-chart-area {
+  clip-path: url(#chart-clip);
+}
+
+.chart-zoomed .line-chart-path {
+  clip-path: url(#chart-clip);
+}
+
+.chart-zoomed .leak-chart-rect {
+  clip-path: url(#chart-clip);
+}
+
+.line-chart-dot {
+  fill: var(--blue);
+}
+
+.line-chart-dot.line-chart-dot-1 {
+  fill: var(--darkBlue);
+}
+
+.line-chart-dot.line-chart-dot-2 {
+  fill: #24c6e0;
+}
+
+.line-chart-event {
+  fill: #fff;
+  stroke: var(--blue);
+  stroke-width: 2px;
+}
+
+.line-chart-event.VERSION {
+  stroke: var(--blue);
+}
+
+.line-chart-event.QUALITY_GATE {
+  stroke: var(--green);
+}
+
+.line-chart-event.QUALITY_PROFILE {
+  stroke: var(--orange);
+}
+
+.line-chart-event.OTHER {
+  stroke: var(--purple);
+}
+
+.new-code-legend {
+  fill: var(--secondFontColor);
+  font-size: var(--smallFontSize);
+}
diff --git a/server/sonar-ui-common/components/charts/AdvancedTimeline.tsx b/server/sonar-ui-common/components/charts/AdvancedTimeline.tsx
new file mode 100644 (file)
index 0000000..7357f24
--- /dev/null
@@ -0,0 +1,623 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import { bisector, extent, max } from 'd3-array';
+import { scaleLinear, scalePoint, scaleTime, ScaleTime } from 'd3-scale';
+import { area, curveBasis, line as d3Line } from 'd3-shape';
+import { flatten, isEqual, sortBy, throttle, uniq } from 'lodash';
+import * as React from 'react';
+import { isDefined } from '../../helpers/types';
+import { Theme, ThemeConsumer } from '../theme';
+import './AdvancedTimeline.css';
+import './LineChart.css';
+
+export interface Props {
+  basisCurve?: boolean;
+  endDate?: Date;
+  disableZoom?: boolean;
+  displayNewCodeLegend?: boolean;
+  formatYTick?: (tick: number | string) => string;
+  hideGrid?: boolean;
+  hideXAxis?: boolean;
+  height: number;
+  width: number;
+  leakPeriodDate?: Date;
+  // used to avoid same y ticks labels
+  maxYTicksCount: number;
+  metricType: string;
+  padding: number[];
+  selectedDate?: Date;
+  series: T.Chart.Serie[];
+  showAreas?: boolean;
+  startDate?: Date;
+  updateSelectedDate?: (selectedDate?: Date) => void;
+  updateTooltip?: (selectedDate?: Date, tooltipXPos?: number, tooltipIdx?: number) => void;
+  updateZoom?: (start?: Date, endDate?: Date) => void;
+  zoomSpeed: number;
+}
+
+type XScale = ScaleTime<number, number>;
+// TODO it should be `ScaleLinear<number, number> | ScalePoint<number> | ScalePoint<string>`, but it's super hard to make it work :'(
+type YScale = any;
+
+const LEGEND_LINE_HEIGHT = 16;
+
+interface State {
+  leakLegendTextWidth?: number;
+  maxXRange: number[];
+  mouseOver?: boolean;
+  selectedDate?: Date;
+  selectedDateXPos?: number;
+  selectedDateIdx?: number;
+  yScale: YScale;
+  xScale: XScale;
+}
+
+export default class AdvancedTimeline extends React.PureComponent<Props, State> {
+  static defaultProps = {
+    eventSize: 8,
+    maxYTicksCount: 4,
+    padding: [26, 10, 50, 60],
+    zoomSpeed: 1,
+  };
+
+  constructor(props: Props) {
+    super(props);
+    const scales = this.getScales(props);
+    const selectedDatePos = this.getSelectedDatePos(scales.xScale, props.selectedDate);
+    this.state = { ...scales, ...selectedDatePos };
+    this.updateTooltipPos = throttle(this.updateTooltipPos, 40);
+    this.handleZoomUpdate = throttle(this.handleZoomUpdate, 40);
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    let scales;
+    let selectedDatePos;
+    if (
+      this.props.metricType !== prevProps.metricType ||
+      this.props.startDate !== prevProps.startDate ||
+      this.props.endDate !== prevProps.endDate ||
+      this.props.width !== prevProps.width ||
+      this.props.padding !== prevProps.padding ||
+      this.props.height !== prevProps.height ||
+      this.props.series !== prevProps.series
+    ) {
+      scales = this.getScales(this.props);
+      if (this.state.selectedDate != null) {
+        selectedDatePos = this.getSelectedDatePos(scales.xScale, this.state.selectedDate);
+      }
+    }
+
+    if (!isEqual(this.props.selectedDate, prevProps.selectedDate)) {
+      const xScale = scales ? scales.xScale : this.state.xScale;
+      selectedDatePos = this.getSelectedDatePos(xScale, this.props.selectedDate);
+    }
+
+    if (scales || selectedDatePos) {
+      if (scales) {
+        this.setState({ ...scales });
+      }
+      if (selectedDatePos) {
+        this.setState({ ...selectedDatePos });
+      }
+
+      if (selectedDatePos && this.props.updateTooltip) {
+        this.props.updateTooltip(
+          selectedDatePos.selectedDate,
+          selectedDatePos.selectedDateXPos,
+          selectedDatePos.selectedDateIdx
+        );
+      }
+    }
+  }
+
+  getRatingScale = (availableHeight: number) => {
+    return scalePoint<number>().domain([5, 4, 3, 2, 1]).range([availableHeight, 0]);
+  };
+
+  getLevelScale = (availableHeight: number) => {
+    return scalePoint().domain(['ERROR', 'WARN', 'OK']).range([availableHeight, 0]);
+  };
+
+  getYScale = (props: Props, availableHeight: number, flatData: T.Chart.Point[]): YScale => {
+    if (props.metricType === 'RATING') {
+      return this.getRatingScale(availableHeight);
+    } else if (props.metricType === 'LEVEL') {
+      return this.getLevelScale(availableHeight);
+    } else {
+      return scaleLinear()
+        .range([availableHeight, 0])
+        .domain([0, max(flatData, (d) => Number(d.y || 0)) || 1])
+        .nice();
+    }
+  };
+
+  getXScale = (
+    { startDate, endDate }: Props,
+    availableWidth: number,
+    flatData: T.Chart.Point[]
+  ) => {
+    const dateRange = extent(flatData, (d) => d.x) as [Date, Date];
+    const start = startDate && startDate > dateRange[0] ? startDate : dateRange[0];
+    const end = endDate && endDate < dateRange[1] ? endDate : dateRange[1];
+    const xScale: ScaleTime<number, number> = scaleTime()
+      .domain(sortBy([start, end]))
+      .range([0, availableWidth])
+      .clamp(false);
+    return {
+      xScale,
+      maxXRange: dateRange.map(xScale),
+    };
+  };
+
+  getScales = (props: Props) => {
+    const availableWidth = props.width - props.padding[1] - props.padding[3];
+    const availableHeight = props.height - props.padding[0] - props.padding[2];
+    const flatData = flatten(props.series.map((serie) => serie.data));
+    return {
+      ...this.getXScale(props, availableWidth, flatData),
+      yScale: this.getYScale(props, availableHeight, flatData),
+    };
+  };
+
+  getSelectedDatePos = (xScale: XScale, selectedDate?: Date) => {
+    const firstSerie = this.props.series[0];
+    if (selectedDate && firstSerie) {
+      const idx = firstSerie.data.findIndex((p) => p.x.valueOf() === selectedDate.valueOf());
+      const xRange = sortBy(xScale.range());
+      const xPos = xScale(selectedDate);
+      if (idx >= 0 && xPos >= xRange[0] && xPos <= xRange[1]) {
+        return {
+          selectedDate,
+          selectedDateXPos: xScale(selectedDate),
+          selectedDateIdx: idx,
+        };
+      }
+    }
+    return { selectedDate: undefined, selectedDateXPos: undefined, selectedDateIdx: undefined };
+  };
+
+  getEventMarker = (size: number) => {
+    const half = size / 2;
+    return `M${half} 0 L${size} ${half} L ${half} ${size} L0 ${half} L${half} 0 L${size} ${half}`;
+  };
+
+  handleWheel = (event: React.WheelEvent<SVGElement>) => {
+    event.preventDefault();
+    const { maxXRange, xScale } = this.state;
+    const parentBbox = event.currentTarget.getBoundingClientRect();
+    const mouseXPos = (event.pageX - parentBbox.left) / parentBbox.width;
+    const xRange = xScale.range();
+    const speed = event.deltaMode
+      ? (25 / event.deltaMode) * this.props.zoomSpeed
+      : this.props.zoomSpeed;
+    const leftPos = xRange[0] - Math.round(speed * event.deltaY * mouseXPos);
+    const rightPos = xRange[1] + Math.round(speed * event.deltaY * (1 - mouseXPos));
+    const startDate = leftPos > maxXRange[0] ? xScale.invert(leftPos) : undefined;
+    const endDate = rightPos < maxXRange[1] ? xScale.invert(rightPos) : undefined;
+    this.handleZoomUpdate(startDate, endDate);
+  };
+
+  handleZoomUpdate = (startDate?: Date, endDate?: Date) => {
+    if (this.props.updateZoom) {
+      this.props.updateZoom(startDate, endDate);
+    }
+  };
+
+  handleMouseMove = (event: React.MouseEvent<SVGElement>) => {
+    const parentBbox = event.currentTarget.getBoundingClientRect();
+    this.updateTooltipPos(event.pageX - parentBbox.left);
+  };
+
+  handleMouseEnter = () => {
+    this.setState({ mouseOver: true });
+  };
+
+  handleMouseOut = () => {
+    const { updateTooltip } = this.props;
+    if (updateTooltip) {
+      this.setState({
+        mouseOver: false,
+        selectedDate: undefined,
+        selectedDateXPos: undefined,
+        selectedDateIdx: undefined,
+      });
+      updateTooltip(undefined, undefined, undefined);
+    }
+  };
+
+  handleClick = () => {
+    const { updateSelectedDate } = this.props;
+    if (updateSelectedDate) {
+      updateSelectedDate(this.state.selectedDate || undefined);
+    }
+  };
+
+  setLeakLegendTextWidth = (node: SVGTextElement | null) => {
+    if (node) {
+      this.setState({ leakLegendTextWidth: node.getBoundingClientRect().width });
+    }
+  };
+
+  updateTooltipPos = (xPos: number) => {
+    this.setState((state) => {
+      const firstSerie = this.props.series[0];
+      if (state.mouseOver && firstSerie) {
+        const { updateTooltip } = this.props;
+        const date = state.xScale.invert(xPos);
+        const bisectX = bisector<T.Chart.Point, Date>((d) => d.x).right;
+        let idx = bisectX(firstSerie.data, date);
+        if (idx >= 0) {
+          const previousPoint = firstSerie.data[idx - 1];
+          const nextPoint = firstSerie.data[idx];
+          if (
+            !nextPoint ||
+            (previousPoint &&
+              date.valueOf() - previousPoint.x.valueOf() <= nextPoint.x.valueOf() - date.valueOf())
+          ) {
+            idx--;
+          }
+          const selectedDate = firstSerie.data[idx].x;
+          const xPos = state.xScale(selectedDate);
+          if (updateTooltip) {
+            updateTooltip(selectedDate, xPos, idx);
+          }
+          return { selectedDate, selectedDateXPos: xPos, selectedDateIdx: idx };
+        }
+      }
+      return null;
+    });
+  };
+
+  renderHorizontalGrid = () => {
+    const { formatYTick } = this.props;
+    const { xScale, yScale } = this.state;
+    const hasTicks = typeof yScale.ticks === 'function';
+    let ticks: Array<string | number> = hasTicks
+      ? yScale.ticks(this.props.maxYTicksCount)
+      : yScale.domain();
+
+    if (!ticks.length) {
+      ticks.push(yScale.domain()[1]);
+    }
+
+    // if there are duplicated ticks, that means 4 ticks are too much for this data
+    // so let's just use the domain values (min and max)
+    if (formatYTick) {
+      const formattedTicks = ticks.map((tick) => formatYTick(tick));
+      if (ticks.length > uniq(formattedTicks).length) {
+        ticks = yScale.domain();
+      }
+    }
+
+    return (
+      <g>
+        {ticks.map((tick) => (
+          <g key={tick}>
+            {formatYTick != null && (
+              <text
+                className="line-chart-tick line-chart-tick-x"
+                dx="-1em"
+                dy="0.3em"
+                textAnchor="end"
+                x={xScale.range()[0]}
+                y={yScale(tick)}>
+                {formatYTick(tick)}
+              </text>
+            )}
+            <line
+              className="line-chart-grid"
+              x1={xScale.range()[0]}
+              x2={xScale.range()[1]}
+              y1={yScale(tick)}
+              y2={yScale(tick)}
+            />
+          </g>
+        ))}
+      </g>
+    );
+  };
+
+  renderXAxisTicks = () => {
+    const { xScale, yScale } = this.state;
+    const format = xScale.tickFormat(7);
+    const ticks = xScale.ticks(7);
+    const y = yScale.range()[0];
+    return (
+      <g transform="translate(0, 20)">
+        {ticks.slice(0, -1).map((tick, index) => {
+          const nextTick = index + 1 < ticks.length ? ticks[index + 1] : xScale.domain()[1];
+          const x = (xScale(tick) + xScale(nextTick)) / 2;
+          return (
+            <text
+              className="line-chart-tick"
+              key={index}
+              textAnchor="end"
+              transform={`rotate(-35, ${x}, ${y})`}
+              x={x}
+              y={y}>
+              {format(tick)}
+            </text>
+          );
+        })}
+      </g>
+    );
+  };
+
+  renderNewCodeLegend = (params: { leakStart: number; leakWidth: number; theme: Theme }) => {
+    const { leakStart, leakWidth, theme } = params;
+    const { leakLegendTextWidth, xScale, yScale } = this.state;
+    const yRange = yScale.range();
+    const xRange = xScale.range();
+
+    const legendMinWidth = (leakLegendTextWidth || 0) + theme.rawSizes.grid;
+    const legendPadding = theme.rawSizes.grid / 2;
+
+    let legendBackgroundPosition;
+    let legendBackgroundWidth;
+    let legendMargin;
+    let legendPosition;
+    let legendTextAnchor;
+
+    if (leakWidth >= legendMinWidth) {
+      legendBackgroundWidth = leakWidth;
+      legendBackgroundPosition = leakStart;
+      legendMargin = 0;
+      legendPosition = legendBackgroundPosition + legendPadding;
+      legendTextAnchor = 'start';
+    } else {
+      legendBackgroundWidth = legendMinWidth;
+      legendBackgroundPosition = xRange[xRange.length - 1] - legendBackgroundWidth;
+      legendMargin = theme.rawSizes.grid / 2;
+      legendPosition = xRange[xRange.length - 1] - legendPadding;
+      legendTextAnchor = 'end';
+    }
+
+    return (
+      <>
+        <rect
+          fill={theme.colors.leakPrimaryColor}
+          height={LEGEND_LINE_HEIGHT}
+          width={legendBackgroundWidth}
+          x={legendBackgroundPosition}
+          y={yRange[yRange.length - 1] - LEGEND_LINE_HEIGHT - legendMargin}
+        />
+        <text
+          className="new-code-legend"
+          ref={this.setLeakLegendTextWidth}
+          x={legendPosition}
+          y={yRange[yRange.length - 1] - legendPadding - legendMargin}
+          textAnchor={legendTextAnchor}>
+          new code
+        </text>
+      </>
+    );
+  };
+
+  renderLeak = () => {
+    const { displayNewCodeLegend, leakPeriodDate } = this.props;
+    if (!leakPeriodDate) {
+      return null;
+    }
+    const { xScale, yScale } = this.state;
+    const yRange = yScale.range();
+    const xRange = xScale.range();
+
+    // truncate leak to start of chart to prevent weird visual artifacts when too far left
+    // (occurs when leak starts a long time before first analysis)
+    const leakStart = Math.max(xScale(leakPeriodDate), xRange[0]);
+
+    const leakWidth = xRange[xRange.length - 1] - leakStart;
+    if (leakWidth < 1) {
+      return null;
+    }
+
+    return (
+      <ThemeConsumer>
+        {(theme) => (
+          <>
+            {displayNewCodeLegend && this.renderNewCodeLegend({ leakStart, leakWidth, theme })}
+            <rect
+              className="leak-chart-rect"
+              fill={theme.colors.leakPrimaryColor}
+              height={yRange[0] - yRange[yRange.length - 1]}
+              width={leakWidth}
+              x={leakStart}
+              y={yRange[yRange.length - 1]}
+            />
+          </>
+        )}
+      </ThemeConsumer>
+    );
+  };
+
+  renderLines = () => {
+    const lineGenerator = d3Line<T.Chart.Point>()
+      .defined((d) => Boolean(d.y || d.y === 0))
+      .x((d) => this.state.xScale(d.x))
+      .y((d) => this.state.yScale(d.y));
+    if (this.props.basisCurve) {
+      lineGenerator.curve(curveBasis);
+    }
+    return (
+      <g>
+        {this.props.series.map((serie, idx) => (
+          <path
+            className={classNames('line-chart-path', 'line-chart-path-' + idx)}
+            d={lineGenerator(serie.data) || undefined}
+            key={serie.name}
+          />
+        ))}
+      </g>
+    );
+  };
+
+  renderDots = () => {
+    return (
+      <g>
+        {this.props.series
+          .map((serie, serieIdx) =>
+            serie.data
+              .map((point, idx) => {
+                const pointNotDefined = !point.y && point.y !== 0;
+                const hasPointBefore =
+                  serie.data[idx - 1] && (serie.data[idx - 1].y || serie.data[idx - 1].y === 0);
+                const hasPointAfter =
+                  serie.data[idx + 1] && (serie.data[idx + 1].y || serie.data[idx + 1].y === 0);
+                if (pointNotDefined || hasPointBefore || hasPointAfter) {
+                  return undefined;
+                }
+                return (
+                  <circle
+                    className={classNames('line-chart-dot', 'line-chart-dot-' + serieIdx)}
+                    cx={this.state.xScale(point.x)}
+                    cy={this.state.yScale(point.y)}
+                    key={serie.name + idx}
+                    r="2"
+                  />
+                );
+              })
+              .filter(isDefined)
+          )
+          .filter((dots) => dots.length > 0)}
+      </g>
+    );
+  };
+
+  renderAreas = () => {
+    const areaGenerator = area<T.Chart.Point>()
+      .defined((d) => Boolean(d.y || d.y === 0))
+      .x((d) => this.state.xScale(d.x))
+      .y1((d) => this.state.yScale(d.y))
+      .y0(this.state.yScale(0));
+    if (this.props.basisCurve) {
+      areaGenerator.curve(curveBasis);
+    }
+    return (
+      <g>
+        {this.props.series.map((serie, idx) => (
+          <path
+            className={classNames('line-chart-area', 'line-chart-area-' + idx)}
+            d={areaGenerator(serie.data) || undefined}
+            key={serie.name}
+          />
+        ))}
+      </g>
+    );
+  };
+
+  renderSelectedDate = () => {
+    const { selectedDateIdx, selectedDateXPos, yScale } = this.state;
+    const firstSerie = this.props.series[0];
+    if (selectedDateIdx == null || selectedDateXPos == null || !firstSerie) {
+      return null;
+    }
+
+    return (
+      <g>
+        <line
+          className="line-tooltip"
+          x1={selectedDateXPos}
+          x2={selectedDateXPos}
+          y1={yScale.range()[0]}
+          y2={yScale.range()[1]}
+        />
+        {this.props.series.map((serie, idx) => {
+          const point = serie.data[selectedDateIdx];
+          if (!point || (!point.y && point.y !== 0)) {
+            return null;
+          }
+          return (
+            <circle
+              className={classNames('line-chart-dot', 'line-chart-dot-' + idx)}
+              cx={selectedDateXPos}
+              cy={yScale(point.y)}
+              key={serie.name}
+              r="4"
+            />
+          );
+        })}
+      </g>
+    );
+  };
+
+  renderClipPath = () => {
+    return (
+      <defs>
+        <clipPath id="chart-clip">
+          <rect
+            height={this.state.yScale.range()[0] + 10}
+            transform="translate(0,-5)"
+            width={this.state.xScale.range()[1]}
+          />
+        </clipPath>
+      </defs>
+    );
+  };
+
+  renderMouseEventsOverlay = (zoomEnabled: boolean) => {
+    const mouseEvents: Partial<React.SVGProps<SVGRectElement>> = {};
+    if (zoomEnabled) {
+      mouseEvents.onWheel = this.handleWheel;
+    }
+    if (this.props.updateTooltip) {
+      mouseEvents.onMouseEnter = this.handleMouseEnter;
+      mouseEvents.onMouseMove = this.handleMouseMove;
+      mouseEvents.onMouseOut = this.handleMouseOut;
+    }
+    if (this.props.updateSelectedDate) {
+      mouseEvents.onClick = this.handleClick;
+    }
+    return (
+      <rect
+        className="chart-mouse-events-overlay"
+        height={this.state.yScale.range()[0]}
+        width={this.state.xScale.range()[1]}
+        {...mouseEvents}
+      />
+    );
+  };
+
+  render() {
+    if (!this.props.width || !this.props.height) {
+      return <div />;
+    }
+    const zoomEnabled = !this.props.disableZoom && this.props.updateZoom != null;
+    const isZoomed = Boolean(this.props.startDate || this.props.endDate);
+    return (
+      <svg
+        className={classNames('line-chart', { 'chart-zoomed': isZoomed })}
+        height={this.props.height}
+        width={this.props.width}>
+        {zoomEnabled && this.renderClipPath()}
+        <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
+          {this.props.leakPeriodDate != null && this.renderLeak()}
+          {!this.props.hideGrid && this.renderHorizontalGrid()}
+          {!this.props.hideXAxis && this.renderXAxisTicks()}
+          {this.props.showAreas && this.renderAreas()}
+          {this.renderLines()}
+          {this.renderDots()}
+          {this.renderSelectedDate()}
+          {this.renderMouseEventsOverlay(zoomEnabled)}
+        </g>
+      </svg>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/charts/BarChart.css b/server/sonar-ui-common/components/charts/BarChart.css
new file mode 100644 (file)
index 0000000..be261a9
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.bar-chart-bar {
+  fill: var(--blue);
+}
+
+.bar-chart-tick {
+  fill: var(--secondFontColor);
+  font-size: var(--smallFontSize);
+  text-anchor: middle;
+}
diff --git a/server/sonar-ui-common/components/charts/BarChart.tsx b/server/sonar-ui-common/components/charts/BarChart.tsx
new file mode 100644 (file)
index 0000000..470d5be
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { max } from 'd3-array';
+import { scaleBand, ScaleBand, scaleLinear, ScaleLinear } from 'd3-scale';
+import * as React from 'react';
+import Tooltip from '../controls/Tooltip';
+import './BarChart.css';
+
+interface DataPoint {
+  tooltip?: React.ReactNode;
+  x: number;
+  y: number;
+}
+
+interface Props<T> {
+  barsWidth: number;
+  data: Array<DataPoint & T>;
+  height: number;
+  onBarClick?: (point: DataPoint & T) => void;
+  padding?: [number, number, number, number];
+  width: number;
+  xTicks?: string[];
+  xValues?: string[];
+}
+
+export default class BarChart<T> extends React.PureComponent<Props<T>> {
+  handleClick = (point: DataPoint & T) => {
+    if (this.props.onBarClick) {
+      this.props.onBarClick(point);
+    }
+  };
+
+  renderXTicks = (xScale: ScaleBand<number>, yScale: ScaleLinear<number, number>) => {
+    const { data, xTicks = [] } = this.props;
+
+    if (!xTicks.length) {
+      return null;
+    }
+
+    const ticks = xTicks.map((tick, index) => {
+      const point = data[index];
+      const x = Math.round((xScale(point.x) as number) + xScale.bandwidth() / 2);
+      const y = yScale.range()[0];
+      const d = data[index];
+      const text = (
+        <text
+          className="bar-chart-tick"
+          dy="1.5em"
+          key={index}
+          onClick={() => this.handleClick(point)}
+          style={{ cursor: this.props.onBarClick ? 'pointer' : 'default' }}
+          x={x}
+          y={y}>
+          {tick}
+        </text>
+      );
+      return (
+        <Tooltip key={index} overlay={d.tooltip || undefined}>
+          {text}
+        </Tooltip>
+      );
+    });
+    return <g>{ticks}</g>;
+  };
+
+  renderXValues = (xScale: ScaleBand<number>, yScale: ScaleLinear<number, number>) => {
+    const { data, xValues = [] } = this.props;
+
+    if (!xValues.length) {
+      return null;
+    }
+
+    const ticks = xValues.map((value, index) => {
+      const point = data[index];
+      const x = Math.round((xScale(point.x) as number) + xScale.bandwidth() / 2);
+      const y = yScale(point.y);
+      const text = (
+        <text
+          className="bar-chart-tick"
+          dy="-1em"
+          key={index}
+          onClick={() => this.handleClick(point)}
+          style={{ cursor: this.props.onBarClick ? 'pointer' : 'default' }}
+          x={x}
+          y={y}>
+          {value}
+        </text>
+      );
+      return (
+        <Tooltip key={index} overlay={point.tooltip || undefined}>
+          {text}
+        </Tooltip>
+      );
+    });
+    return <g>{ticks}</g>;
+  };
+
+  renderBars = (xScale: ScaleBand<number>, yScale: ScaleLinear<number, number>) => {
+    const bars = this.props.data.map((point, index) => {
+      const x = Math.round(xScale(point.x) as number);
+      const maxY = yScale.range()[0];
+      const y = Math.round(yScale(point.y)) - /* minimum bar height */ 1;
+      const height = maxY - y;
+      const rect = (
+        <rect
+          className="bar-chart-bar"
+          height={height}
+          key={index}
+          onClick={() => this.handleClick(point)}
+          style={{ cursor: this.props.onBarClick ? 'pointer' : 'default' }}
+          width={this.props.barsWidth}
+          x={x}
+          y={y}
+        />
+      );
+      return (
+        <Tooltip key={index} overlay={point.tooltip || undefined}>
+          {rect}
+        </Tooltip>
+      );
+    });
+    return <g>{bars}</g>;
+  };
+
+  render() {
+    const { barsWidth, data, width, height, padding = [10, 10, 10, 10] } = this.props;
+
+    const availableWidth = width - padding[1] - padding[3];
+    const availableHeight = height - padding[0] - padding[2];
+
+    const innerPadding = (availableWidth - barsWidth * data.length) / (data.length - 1);
+    const relativeInnerPadding = innerPadding / (innerPadding + barsWidth);
+
+    const maxY = max(data, (d) => d.y) as number;
+    const xScale = scaleBand<number>()
+      .domain(data.map((d) => d.x))
+      .range([0, availableWidth])
+      .paddingInner(relativeInnerPadding);
+    const yScale = scaleLinear().domain([0, maxY]).range([availableHeight, 0]);
+
+    return (
+      <svg className="bar-chart" height={height} width={width}>
+        <g transform={`translate(${padding[3]}, ${padding[0]})`}>
+          {this.renderXTicks(xScale, yScale)}
+          {this.renderXValues(xScale, yScale)}
+          {this.renderBars(xScale, yScale)}
+        </g>
+      </svg>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/charts/BubbleChart.css b/server/sonar-ui-common/components/charts/BubbleChart.css
new file mode 100644 (file)
index 0000000..fc8be5f
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.bubble-chart text {
+  user-select: none;
+}
+
+.bubble-chart-bubble {
+  fill: var(--blue);
+  fill-opacity: 0.2;
+  stroke: var(--blue);
+  cursor: pointer;
+  transition: fill-opacity 0.2s ease;
+}
+
+.bubble-chart-bubble:hover {
+  fill-opacity: 0.8;
+}
+
+.bubble-chart-grid {
+  shape-rendering: crispedges;
+  stroke: #eee;
+}
+
+.bubble-chart-tick {
+  fill: var(--secondFontColor);
+  font-size: var(--smallFontSize);
+  text-anchor: middle;
+}
+
+.bubble-chart-tick-y {
+  text-anchor: end;
+}
+
+.bubble-chart-zoom {
+  position: absolute;
+  right: 20px;
+  top: 20px;
+  z-index: var(--aboveNormalZIndex);
+}
diff --git a/server/sonar-ui-common/components/charts/BubbleChart.tsx b/server/sonar-ui-common/components/charts/BubbleChart.tsx
new file mode 100644 (file)
index 0000000..dcaf101
--- /dev/null
@@ -0,0 +1,382 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import { max, min } from 'd3-array';
+import { scaleLinear, ScaleLinear } from 'd3-scale';
+import { event, select } from 'd3-selection';
+import { zoom, ZoomBehavior, zoomIdentity } from 'd3-zoom';
+import { sortBy, uniq } from 'lodash';
+import * as React from 'react';
+import { Link } from 'react-router';
+import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer';
+import { translate } from '../../helpers/l10n';
+import { Location } from '../../helpers/urls';
+import Tooltip from '../controls/Tooltip';
+import './BubbleChart.css';
+
+const TICKS_COUNT = 5;
+
+interface BubbleItem<T> {
+  color?: string;
+  key?: string;
+  link?: string | Location;
+  data?: T;
+  size: number;
+  tooltip?: React.ReactNode;
+  x: number;
+  y: number;
+}
+
+interface Props<T> {
+  displayXGrid?: boolean;
+  displayXTicks?: boolean;
+  displayYGrid?: boolean;
+  displayYTicks?: boolean;
+  formatXTick: (tick: number) => string;
+  formatYTick: (tick: number) => string;
+  height: number;
+  items: BubbleItem<T>[];
+  onBubbleClick?: (ref?: T) => void;
+  padding: [number, number, number, number];
+  sizeDomain?: [number, number];
+  sizeRange?: [number, number];
+  xDomain?: [number, number];
+  yDomain?: [number, number];
+}
+
+interface State {
+  transform: { x: number; y: number; k: number };
+}
+
+type Scale = ScaleLinear<number, number>;
+
+export default class BubbleChart<T> extends React.PureComponent<Props<T>, State> {
+  private node?: Element;
+  private zoom?: ZoomBehavior<Element, unknown>;
+
+  static defaultProps = {
+    displayXGrid: true,
+    displayXTicks: true,
+    displayYGrid: true,
+    displayYTicks: true,
+    formatXTick: (d: number) => String(d),
+    formatYTick: (d: number) => String(d),
+    padding: [10, 10, 10, 10],
+    sizeRange: [5, 45],
+  };
+
+  constructor(props: Props<T>) {
+    super(props);
+    this.state = { transform: { x: 0, y: 0, k: 1 } };
+  }
+
+  componentDidUpdate() {
+    if (this.zoom && this.node) {
+      const rect = this.node.getBoundingClientRect();
+      this.zoom.translateExtent([
+        [0, 0],
+        [rect.width, rect.height],
+      ]);
+    }
+  }
+
+  boundNode = (node: SVGSVGElement) => {
+    this.node = node;
+    this.zoom = zoom().scaleExtent([1, 10]).on('zoom', this.zoomed);
+    select(this.node).call(this.zoom);
+  };
+
+  zoomed = () => {
+    const { padding } = this.props;
+    const { x, y, k } = event.transform as { x: number; y: number; k: number };
+    this.setState({
+      transform: {
+        x: x + padding[3] * (k - 1),
+        y: y + padding[0] * (k - 1),
+        k,
+      },
+    });
+  };
+
+  resetZoom = (event: React.MouseEvent<Link>) => {
+    event.stopPropagation();
+    event.preventDefault();
+    if (this.zoom && this.node) {
+      select(this.node).call(this.zoom.transform, zoomIdentity);
+    }
+  };
+
+  getXRange(xScale: Scale, sizeScale: Scale, availableWidth: number) {
+    const minX = min(this.props.items, (d) => xScale(d.x) - sizeScale(d.size)) || 0;
+    const maxX = max(this.props.items, (d) => xScale(d.x) + sizeScale(d.size)) || 0;
+    const dMinX = minX < 0 ? xScale.range()[0] - minX : xScale.range()[0];
+    const dMaxX = maxX > xScale.range()[1] ? maxX - xScale.range()[1] : 0;
+    return [dMinX, availableWidth - dMaxX];
+  }
+
+  getYRange(yScale: Scale, sizeScale: Scale, availableHeight: number) {
+    const minY = min(this.props.items, (d) => yScale(d.y) - sizeScale(d.size)) || 0;
+    const maxY = max(this.props.items, (d) => yScale(d.y) + sizeScale(d.size)) || 0;
+    const dMinY = minY < 0 ? yScale.range()[1] - minY : yScale.range()[1];
+    const dMaxY = maxY > yScale.range()[0] ? maxY - yScale.range()[0] : 0;
+    return [availableHeight - dMaxY, dMinY];
+  }
+
+  getTicks(scale: Scale, format: (d: number) => string) {
+    const zoom = Math.ceil(this.state.transform.k);
+    const ticks = scale.ticks(TICKS_COUNT * zoom).map((tick) => format(tick));
+    const uniqueTicksCount = uniq(ticks).length;
+    const ticksCount =
+      uniqueTicksCount < TICKS_COUNT * zoom ? uniqueTicksCount - 1 : TICKS_COUNT * zoom;
+    return scale.ticks(ticksCount);
+  }
+
+  getZoomLevelLabel = () => Math.floor(this.state.transform.k * 100) + '%';
+
+  renderXGrid = (ticks: number[], xScale: Scale, yScale: Scale) => {
+    if (!this.props.displayXGrid) {
+      return null;
+    }
+
+    const { transform } = this.state;
+    const lines = ticks.map((tick, index) => {
+      const x = xScale(tick);
+      const y1 = yScale.range()[0];
+      const y2 = yScale.range()[1];
+      return (
+        <line
+          className="bubble-chart-grid"
+          key={index}
+          x1={x * transform.k + transform.x}
+          x2={x * transform.k + transform.x}
+          y1={y1 * transform.k}
+          y2={transform.k > 1 ? 0 : y2}
+        />
+      );
+    });
+
+    return <g>{lines}</g>;
+  };
+
+  renderYGrid = (ticks: number[], xScale: Scale, yScale: Scale) => {
+    if (!this.props.displayYGrid) {
+      return null;
+    }
+
+    const { transform } = this.state;
+    const lines = ticks.map((tick, index) => {
+      const y = yScale(tick);
+      const x1 = xScale.range()[0];
+      const x2 = xScale.range()[1];
+      return (
+        <line
+          className="bubble-chart-grid"
+          key={index}
+          x1={transform.k > 1 ? 0 : x1}
+          x2={x2 * transform.k}
+          y1={y * transform.k + transform.y}
+          y2={y * transform.k + transform.y}
+        />
+      );
+    });
+
+    return <g>{lines}</g>;
+  };
+
+  renderXTicks = (xTicks: number[], xScale: Scale, yScale: Scale) => {
+    if (!this.props.displayXTicks) {
+      return null;
+    }
+
+    const { transform } = this.state;
+    const ticks = xTicks.map((tick, index) => {
+      const x = xScale(tick) * transform.k + transform.x;
+      const y = yScale.range()[0];
+      const innerText = this.props.formatXTick(tick);
+      // as we modified the `x` using `transform`, check that it is inside the range again
+      return x > 0 && x < xScale.range()[1] ? (
+        <text className="bubble-chart-tick" dy="1.5em" key={index} x={x} y={y}>
+          {innerText}
+        </text>
+      ) : null;
+    });
+
+    return <g>{ticks}</g>;
+  };
+
+  renderYTicks = (yTicks: number[], xScale: Scale, yScale: Scale) => {
+    if (!this.props.displayYTicks) {
+      return null;
+    }
+
+    const { transform } = this.state;
+    const ticks = yTicks.map((tick, index) => {
+      const x = xScale.range()[0];
+      const y = yScale(tick) * transform.k + transform.y;
+      const innerText = this.props.formatYTick(tick);
+      // as we modified the `y` using `transform`, check that it is inside the range again
+      return y > 0 && y < yScale.range()[0] ? (
+        <text
+          className="bubble-chart-tick bubble-chart-tick-y"
+          dx="-0.5em"
+          dy="0.3em"
+          key={index}
+          x={x}
+          y={y}>
+          {innerText}
+        </text>
+      ) : null;
+    });
+
+    return <g>{ticks}</g>;
+  };
+
+  renderChart = (width: number) => {
+    const { transform } = this.state;
+    const availableWidth = width - this.props.padding[1] - this.props.padding[3];
+    const availableHeight = this.props.height - this.props.padding[0] - this.props.padding[2];
+
+    const xScale = scaleLinear()
+      .domain(this.props.xDomain || [0, max(this.props.items, (d) => d.x) || 0])
+      .range([0, availableWidth])
+      .nice();
+    const yScale = scaleLinear()
+      .domain(this.props.yDomain || [0, max(this.props.items, (d) => d.y) || 0])
+      .range([availableHeight, 0])
+      .nice();
+    const sizeScale = scaleLinear()
+      .domain(this.props.sizeDomain || [0, max(this.props.items, (d) => d.size) || 0])
+      .range(this.props.sizeRange || []);
+
+    const xScaleOriginal = xScale.copy();
+    const yScaleOriginal = yScale.copy();
+
+    xScale.range(this.getXRange(xScale, sizeScale, availableWidth));
+    yScale.range(this.getYRange(yScale, sizeScale, availableHeight));
+
+    const bubbles = sortBy(this.props.items, (b) => -b.size).map((item, index) => {
+      return (
+        <Bubble
+          color={item.color}
+          data={item.data}
+          key={item.key || index}
+          link={item.link}
+          onClick={this.props.onBubbleClick}
+          r={sizeScale(item.size)}
+          scale={1 / transform.k}
+          tooltip={item.tooltip}
+          x={xScale(item.x)}
+          y={yScale(item.y)}
+        />
+      );
+    });
+
+    const xTicks = this.getTicks(xScale, this.props.formatXTick);
+    const yTicks = this.getTicks(yScale, this.props.formatYTick);
+
+    return (
+      <svg
+        className={classNames('bubble-chart')}
+        height={this.props.height}
+        ref={this.boundNode}
+        width={width}>
+        <defs>
+          <clipPath id="graph-region">
+            <rect
+              // Extend clip by 2 pixels: one for clipRect border, and one for Bubble borders
+              height={availableHeight + 4}
+              width={availableWidth + 4}
+              x={-2}
+              y={-2}
+            />
+          </clipPath>
+        </defs>
+        <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
+          <g clipPath="url(#graph-region)">
+            {this.renderXGrid(xTicks, xScale, yScale)}
+            {this.renderYGrid(yTicks, xScale, yScale)}
+            <g transform={`translate(${transform.x}, ${transform.y}) scale(${transform.k})`}>
+              {bubbles}
+            </g>
+          </g>
+          {this.renderXTicks(xTicks, xScale, yScaleOriginal)}
+          {this.renderYTicks(yTicks, xScaleOriginal, yScale)}
+        </g>
+      </svg>
+    );
+  };
+
+  render() {
+    return (
+      <div>
+        <div className="bubble-chart-zoom">
+          <Tooltip overlay={translate('component_measures.bubble_chart.zoom_level')}>
+            <Link onClick={this.resetZoom} to="#">
+              {this.getZoomLevelLabel()}
+            </Link>
+          </Tooltip>
+        </div>
+        <AutoSizer disableHeight={true}>{(size) => this.renderChart(size.width)}</AutoSizer>
+      </div>
+    );
+  }
+}
+
+interface BubbleProps<T> {
+  color?: string;
+  link?: string | Location;
+  onClick?: (ref?: T) => void;
+  data?: T;
+  r: number;
+  scale: number;
+  tooltip?: string | React.ReactNode;
+  x: number;
+  y: number;
+}
+
+function Bubble<T>(props: BubbleProps<T>) {
+  const handleClick = (event: React.MouseEvent<SVGCircleElement>) => {
+    if (props.onClick) {
+      event.stopPropagation();
+      event.preventDefault();
+      props.onClick(props.data);
+    }
+  };
+
+  let circle = (
+    <circle
+      className="bubble-chart-bubble"
+      onClick={props.onClick ? handleClick : undefined}
+      r={props.r}
+      style={{ fill: props.color, stroke: props.color }}
+      transform={`translate(${props.x}, ${props.y}) scale(${props.scale})`}
+    />
+  );
+
+  if (props.link && !props.onClick) {
+    circle = <Link to={props.link}>{circle}</Link>;
+  }
+
+  return (
+    <Tooltip overlay={props.tooltip || undefined}>
+      <g>{circle}</g>
+    </Tooltip>
+  );
+}
diff --git a/server/sonar-ui-common/components/charts/ColorGradientLegend.css b/server/sonar-ui-common/components/charts/ColorGradientLegend.css
new file mode 100644 (file)
index 0000000..214832d
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.gradient-legend-text,
+.gradient-legend-na {
+  text-anchor: middle;
+  fill: var(--secondFontColor);
+  font-size: 10px;
+}
+
+.gradient-legend-text:first-of-type {
+  text-anchor: start;
+}
+
+.gradient-legend-text:last-of-type {
+  text-anchor: end;
+}
diff --git a/server/sonar-ui-common/components/charts/ColorGradientLegend.tsx b/server/sonar-ui-common/components/charts/ColorGradientLegend.tsx
new file mode 100644 (file)
index 0000000..50c253a
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { ScaleLinear, ScaleOrdinal } from 'd3-scale';
+import * as React from 'react';
+import { ThemeConsumer } from '../theme';
+import './ColorGradientLegend.css';
+
+interface Props {
+  className?: string;
+  colorScale:
+    | ScaleOrdinal<string, string> // used for LEVEL type
+    | ScaleLinear<string, string | number>; // used for RATING or PERCENT type
+  height: number;
+  padding?: [number, number, number, number];
+  showColorNA?: boolean;
+  width: number;
+}
+
+const NA_SPACING = 4;
+const NA_GRADIENT_LINE_INCREMENTS = [0, 8, 16, 24];
+
+export default function ColorGradientLegend({
+  className,
+  colorScale,
+  padding = [12, 24, 0, 0],
+  height,
+  showColorNA = false,
+  width,
+}: Props) {
+  const colorRange: Array<string | number> = colorScale.range();
+  const colorDomain: Array<string | number> = colorScale.domain();
+  const lastColorIdx = colorRange.length - 1;
+  const lastDomainIdx = colorDomain.length - 1;
+  const widthNoPadding = width - padding[1];
+  const rectHeight = height - padding[0];
+  return (
+    <ThemeConsumer>
+      {({ colors }) => (
+        <svg className={className} height={height} width={width}>
+          <defs>
+            <linearGradient id="gradient-legend">
+              {colorRange.map((color, idx) => (
+                <stop key={idx} offset={idx / lastColorIdx} stopColor={String(color)} />
+              ))}
+            </linearGradient>
+
+            <pattern
+              id="stripes"
+              width="30"
+              height="30"
+              patternTransform="rotate(45 0 0)"
+              patternUnits="userSpaceOnUse">
+              {NA_GRADIENT_LINE_INCREMENTS.map((i) => (
+                <React.Fragment key={i}>
+                  <line
+                    x1={i}
+                    y1="0"
+                    x2={i}
+                    y2="30"
+                    style={{ stroke: colors.gray71, strokeWidth: NA_SPACING }}
+                  />
+                  <line
+                    x1={i + NA_SPACING}
+                    y1="0"
+                    x2={i + NA_SPACING}
+                    y2="30"
+                    style={{ stroke: colors.gray60, strokeWidth: NA_SPACING }}
+                  />
+                </React.Fragment>
+              ))}
+            </pattern>
+          </defs>
+          <g transform={`translate(${padding[3]}, ${padding[0]})`}>
+            <rect
+              fill="url(#gradient-legend)"
+              height={rectHeight}
+              width={widthNoPadding}
+              x={0}
+              y={0}
+            />
+            {colorDomain.map((d, idx) => (
+              <text
+                className="gradient-legend-text"
+                dy="-2px"
+                key={idx}
+                x={widthNoPadding * (idx / lastDomainIdx)}
+                y={0}>
+                {d}
+              </text>
+            ))}
+          </g>
+          {showColorNA && (
+            <g transform={`translate(${widthNoPadding}, ${padding[0]})`}>
+              <rect
+                fill="url(#stripes)"
+                height={rectHeight}
+                width={padding[1] - NA_SPACING}
+                x={NA_SPACING}
+                y={0}
+              />
+              <text
+                className="gradient-legend-na"
+                dy="-2px"
+                x={NA_SPACING + (padding[1] - NA_SPACING) / 2}
+                y={0}>
+                N/A
+              </text>
+            </g>
+          )}
+        </svg>
+      )}
+    </ThemeConsumer>
+  );
+}
diff --git a/server/sonar-ui-common/components/charts/DonutChart.tsx b/server/sonar-ui-common/components/charts/DonutChart.tsx
new file mode 100644 (file)
index 0000000..195b592
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { arc as d3Arc, pie as d3Pie, PieArcDatum } from 'd3-shape';
+import * as React from 'react';
+
+interface DataPoint {
+  fill: string;
+  value: number;
+}
+
+export interface DonutChartProps {
+  data: DataPoint[];
+  height: number;
+  padAngle?: number;
+  padding?: [number, number, number, number];
+  thickness: number;
+  width: number;
+}
+
+export default function DonutChart(props: DonutChartProps) {
+  const { height, padding = [0, 0, 0, 0], width } = props;
+
+  const availableWidth = width - padding[1] - padding[3];
+  const availableHeight = height - padding[0] - padding[2];
+
+  const size = Math.min(availableWidth, availableHeight);
+  const radius = Math.floor(size / 2);
+
+  const pie = d3Pie<any, DataPoint>()
+    .sort(null)
+    .value((d) => d.value);
+
+  if (props.padAngle !== undefined) {
+    pie.padAngle(props.padAngle);
+  }
+
+  const sectors = pie(props.data).map((d, i) => {
+    return (
+      <Sector
+        data={d}
+        fill={props.data[i].fill}
+        key={i}
+        radius={radius}
+        thickness={props.thickness}
+      />
+    );
+  });
+
+  return (
+    <svg className="donut-chart" height={height} width={width}>
+      <g transform={`translate(${padding[3]}, ${padding[0]})`}>
+        <g transform={`translate(${radius}, ${radius})`}>{sectors}</g>
+      </g>
+    </svg>
+  );
+}
+
+interface SectorProps {
+  data: PieArcDatum<DataPoint>;
+  fill: string;
+  radius: number;
+  thickness: number;
+}
+
+function Sector(props: SectorProps) {
+  const arc = d3Arc<any, PieArcDatum<DataPoint>>()
+    .outerRadius(props.radius)
+    .innerRadius(props.radius - props.thickness);
+  const d = arc(props.data) as string;
+  return <path d={d} style={{ fill: props.fill }} />;
+}
diff --git a/server/sonar-ui-common/components/charts/Histogram.css b/server/sonar-ui-common/components/charts/Histogram.css
new file mode 100644 (file)
index 0000000..b3db981
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.histogram-tick {
+  text-anchor: end;
+}
+
+.histogram-tick-start {
+  text-anchor: start;
+}
+
+.histogram-value {
+  text-anchor: start;
+}
diff --git a/server/sonar-ui-common/components/charts/Histogram.tsx b/server/sonar-ui-common/components/charts/Histogram.tsx
new file mode 100644 (file)
index 0000000..ff39ead
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { max } from 'd3-array';
+import { scaleBand, ScaleBand, scaleLinear, ScaleLinear } from 'd3-scale';
+import * as React from 'react';
+import Tooltip from '../controls/Tooltip';
+import './BarChart.css';
+import './Histogram.css';
+
+interface Props {
+  alignTicks?: boolean;
+  bars: number[];
+  height: number;
+  padding?: [number, number, number, number];
+  yTicks?: string[];
+  yTooltips?: string[];
+  yValues?: string[];
+  width: number;
+}
+
+const BAR_HEIGHT = 10;
+const DEFAULT_PADDING = [10, 10, 10, 10];
+
+type XScale = ScaleLinear<number, number>;
+type YScale = ScaleBand<number>;
+
+export default class Histogram extends React.PureComponent<Props> {
+  renderBar(d: number, index: number, xScale: XScale, yScale: YScale) {
+    const { alignTicks, padding = DEFAULT_PADDING } = this.props;
+
+    const width = Math.round(xScale(d)) + /* minimum bar width */ 1;
+    const x = xScale.range()[0] + (alignTicks ? padding[3] : 0);
+    const y = Math.round(yScale(index)! + yScale.bandwidth() / 2);
+
+    return <rect className="bar-chart-bar" height={BAR_HEIGHT} width={width} x={x} y={y} />;
+  }
+
+  renderValue(d: number, index: number, xScale: XScale, yScale: YScale) {
+    const { alignTicks, padding = DEFAULT_PADDING, yValues } = this.props;
+
+    const value = yValues && yValues[index];
+
+    if (!value) {
+      return null;
+    }
+
+    const x = xScale(d) + (alignTicks ? padding[3] : 0);
+    const y = Math.round(yScale(index)! + yScale.bandwidth() / 2 + BAR_HEIGHT / 2);
+
+    return (
+      <Tooltip overlay={this.props.yTooltips && this.props.yTooltips[index]}>
+        <text className="bar-chart-tick histogram-value" dx="1em" dy="0.3em" x={x} y={y}>
+          {value}
+        </text>
+      </Tooltip>
+    );
+  }
+
+  renderTick(index: number, xScale: XScale, yScale: YScale) {
+    const { alignTicks, yTicks } = this.props;
+
+    const tick = yTicks && yTicks[index];
+
+    if (!tick) {
+      return null;
+    }
+
+    const x = xScale.range()[0];
+    const y = Math.round(yScale(index)! + yScale.bandwidth() / 2 + BAR_HEIGHT / 2);
+    const historyTickClass = alignTicks ? 'histogram-tick-start' : 'histogram-tick';
+
+    return (
+      <text
+        className={'bar-chart-tick ' + historyTickClass}
+        dx={alignTicks ? 0 : '-1em'}
+        dy="0.3em"
+        x={x}
+        y={y}>
+        {tick}
+      </text>
+    );
+  }
+
+  renderBars(xScale: XScale, yScale: YScale) {
+    return (
+      <g>
+        {this.props.bars.map((d, index) => {
+          return (
+            <g key={index}>
+              {this.renderBar(d, index, xScale, yScale)}
+              {this.renderValue(d, index, xScale, yScale)}
+              {this.renderTick(index, xScale, yScale)}
+            </g>
+          );
+        })}
+      </g>
+    );
+  }
+
+  render() {
+    const { bars, width, height, padding = DEFAULT_PADDING } = this.props;
+
+    const availableWidth = width - padding[1] - padding[3];
+    const xScale: XScale = scaleLinear()
+      .domain([0, max(bars)!])
+      .range([0, availableWidth]);
+
+    const availableHeight = height - padding[0] - padding[2];
+    const yScale: YScale = scaleBand<number>()
+      .domain(bars.map((_, index) => index))
+      .rangeRound([0, availableHeight]);
+
+    return (
+      <svg className="bar-chart" height={this.props.height} width={this.props.width}>
+        <g transform={`translate(${this.props.alignTicks ? 4 : padding[3]}, ${padding[0]})`}>
+          {this.renderBars(xScale, yScale)}
+        </g>
+      </svg>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/charts/LineChart.css b/server/sonar-ui-common/components/charts/LineChart.css
new file mode 100644 (file)
index 0000000..a8148c0
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.line-chart-path {
+  fill: none;
+  stroke: var(--blue);
+  stroke-width: 2px;
+}
+
+.line-chart-path.line-chart-path-1 {
+  stroke: var(--darkBlue);
+}
+
+.line-chart-path.line-chart-path-2 {
+  stroke: #24c6e0;
+}
+
+.line-chart-area {
+  fill: rgba(75, 159, 213, 0.3);
+  stroke-width: 0;
+}
+
+.line-chart-area.line-chart-area-1 {
+  fill: rgba(35, 106, 151, 0.3);
+}
+
+.line-chart-area.line-chart-area-2 {
+  fill: rgba(36, 198, 224, 0.3);
+}
+
+.line-chart-point {
+  fill: #fff;
+  stroke: var(--blue);
+  stroke-width: 2px;
+}
+
+.line-chart-tick {
+  fill: var(--secondFontColor);
+  font-size: var(--smallFontSize);
+}
+
+.line-chart-tick-x {
+  text-anchor: end;
+}
+
+.line-chart-tick-x-right {
+  text-anchor: start;
+}
+
+.line-chart-grid {
+  shape-rendering: crispedges;
+  stroke: #eee;
+}
diff --git a/server/sonar-ui-common/components/charts/LineChart.tsx b/server/sonar-ui-common/components/charts/LineChart.tsx
new file mode 100644 (file)
index 0000000..9267b42
--- /dev/null
@@ -0,0 +1,195 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { extent, max } from 'd3-array';
+import { scaleLinear, ScaleLinear } from 'd3-scale';
+import { area as d3Area, curveBasis, line as d3Line } from 'd3-shape';
+import * as React from 'react';
+import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer';
+import './LineChart.css';
+
+interface DataPoint {
+  x: number;
+  y?: number;
+}
+
+interface Props {
+  backdropConstraints?: [number, number];
+  data: DataPoint[];
+  displayBackdrop?: boolean;
+  displayPoints?: boolean;
+  displayVerticalGrid?: boolean;
+  domain?: [number, number];
+  height: number;
+  padding?: [number, number, number, number];
+  width?: number;
+  xTicks?: {}[];
+  xValues?: {}[];
+}
+
+export default class LineChart extends React.PureComponent<Props> {
+  renderBackdrop(xScale: ScaleLinear<number, number>, yScale: ScaleLinear<number, number>) {
+    const { displayBackdrop = true } = this.props;
+
+    if (!displayBackdrop) {
+      return null;
+    }
+
+    const area = d3Area<DataPoint>()
+      .x((d) => xScale(d.x))
+      .y0(yScale.range()[0])
+      .y1((d) => yScale(d.y || 0))
+      .defined((d) => d.y != null)
+      .curve(curveBasis);
+
+    let { data } = this.props;
+    if (this.props.backdropConstraints) {
+      const c = this.props.backdropConstraints;
+      data = data.filter((d) => c[0] <= d.x && d.x <= c[1]);
+    }
+
+    return <path className="line-chart-backdrop" d={area(data) as string} />;
+  }
+
+  renderPoints(xScale: ScaleLinear<number, number>, yScale: ScaleLinear<number, number>) {
+    const { displayPoints = true } = this.props;
+
+    if (!displayPoints) {
+      return null;
+    }
+
+    const points = this.props.data
+      .filter((point) => point.y != null)
+      .map((point, index) => {
+        const x = xScale(point.x);
+        const y = yScale(point.y || 0);
+        return <circle className="line-chart-point" cx={x} cy={y} key={index} r="3" />;
+      });
+    return <g>{points}</g>;
+  }
+
+  renderVerticalGrid(xScale: ScaleLinear<number, number>, yScale: ScaleLinear<number, number>) {
+    const { displayVerticalGrid = true } = this.props;
+
+    if (!displayVerticalGrid) {
+      return null;
+    }
+
+    const lines = this.props.data.map((point, index) => {
+      const x = xScale(point.x);
+      const y1 = yScale.range()[0];
+      const y2 = yScale(point.y || 0);
+      return <line className="line-chart-grid" key={index} x1={x} x2={x} y1={y1} y2={y2} />;
+    });
+    return <g>{lines}</g>;
+  }
+
+  renderXTicks(xScale: ScaleLinear<number, number>, yScale: ScaleLinear<number, number>) {
+    const { xTicks = [] } = this.props;
+
+    if (!xTicks.length) {
+      return null;
+    }
+
+    const ticks = xTicks.map((tick, index) => {
+      const point = this.props.data[index];
+      const x = xScale(point.x);
+      const y = yScale.range()[0];
+      return (
+        <text className="line-chart-tick" dy="1.5em" key={index} x={x} y={y}>
+          {tick}
+        </text>
+      );
+    });
+    return <g>{ticks}</g>;
+  }
+
+  renderXValues(xScale: ScaleLinear<number, number>, yScale: ScaleLinear<number, number>) {
+    const { xValues = [] } = this.props;
+
+    if (!xValues.length) {
+      return null;
+    }
+
+    const ticks = xValues.map((value, index) => {
+      const point = this.props.data[index];
+      const x = xScale(point.x);
+      const y = yScale(point.y || 0);
+      return (
+        <text className="line-chart-tick" dy="-1em" key={index} x={x} y={y}>
+          {value}
+        </text>
+      );
+    });
+    return <g>{ticks}</g>;
+  }
+
+  renderLine(xScale: ScaleLinear<number, number>, yScale: ScaleLinear<number, number>) {
+    const p = d3Line<DataPoint>()
+      .x((d) => xScale(d.x))
+      .y((d) => yScale(d.y || 0))
+      .defined((d) => d.y != null)
+      .curve(curveBasis);
+    return <path className="line-chart-path" d={p(this.props.data) as string} />;
+  }
+
+  renderChart = (width: number) => {
+    const { height, padding = [10, 10, 10, 10] } = this.props;
+
+    if (!width || !height) {
+      return <div />;
+    }
+
+    const availableWidth = width - padding[1] - padding[3];
+    const availableHeight = height - padding[0] - padding[2];
+
+    const xScale = scaleLinear()
+      .domain(extent(this.props.data, (d) => d.x) as [number, number])
+      .range([0, availableWidth]);
+    const yScale = scaleLinear().range([availableHeight, 0]);
+
+    if (this.props.domain) {
+      yScale.domain(this.props.domain);
+    } else {
+      const maxY = max(this.props.data, (d) => d.y) as number;
+      yScale.domain([0, maxY]);
+    }
+
+    return (
+      <svg className="line-chart" height={height} width={width}>
+        <g transform={`translate(${padding[3]}, ${padding[0]})`}>
+          {this.renderVerticalGrid(xScale, yScale)}
+          {this.renderBackdrop(xScale, yScale)}
+          {this.renderLine(xScale, yScale)}
+          {this.renderPoints(xScale, yScale)}
+          {this.renderXTicks(xScale, yScale)}
+          {this.renderXValues(xScale, yScale)}
+        </g>
+      </svg>
+    );
+  };
+
+  render() {
+    return this.props.width !== undefined ? (
+      this.renderChart(this.props.width)
+    ) : (
+      <AutoSizer disableHeight={true}>{(size) => this.renderChart(size.width)}</AutoSizer>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/charts/TreeMap.css b/server/sonar-ui-common/components/charts/TreeMap.css
new file mode 100644 (file)
index 0000000..379acf6
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.sonar-d3 .treemap-container {
+  position: relative;
+}
+
+.sonar-d3 .treemap-cell {
+  position: absolute;
+  border-right: 1px solid #fff;
+  border-bottom: 1px solid #fff;
+  box-sizing: border-box;
+  text-align: center;
+  overflow: hidden;
+}
+
+.sonar-d3 .treemap-cell:focus {
+  outline: none;
+}
+
+.sonar-d3 .treemap-inner {
+  display: inline-flex;
+  vertical-align: middle;
+  align-items: center;
+  justify-content: center;
+  flex-wrap: wrap;
+  padding: var(--gridSize);
+  box-sizing: border-box;
+  line-height: 1.2;
+  background: rgba(0, 0, 0, 0.6);
+  border-radius: 2px;
+}
+
+.sonar-d3 .treemap-inner .treemap-icon {
+  flex-shrink: 0;
+}
+
+.sonar-d3 .treemap-inner .treemap-icon svg {
+  margin-top: 2px;
+}
+
+.sonar-d3 .treemap-inner .treemap-icon svg path {
+  fill: var(--barBackgroundColor) !important;
+}
+
+.sonar-d3 .treemap-inner .treemap-text {
+  flex-shrink: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  text-align: center;
+  color: var(--barBackgroundColor);
+}
+
+.sonar-d3 .treemap-inner .treemap-text-suffix {
+  color: var(--barBorderColor);
+  font-size: var(--smallFontSize);
+}
+
+.sonar-d3 .treemap-link {
+  position: absolute;
+  z-index: var(--normalZIndex);
+  top: 5px;
+  right: 5px;
+  line-height: 14px;
+  font-size: var(--smallFontSize);
+  border-bottom: none;
+}
+
+.sonar-d3 .treemap-link:hover {
+  color: #d1eafb;
+}
+
+.sonar-d3 .treemap-link i,
+.sonar-d3 .treemap-link i:before {
+  vertical-align: top;
+  font-size: inherit;
+  line-height: inherit;
+}
diff --git a/server/sonar-ui-common/components/charts/TreeMap.tsx b/server/sonar-ui-common/components/charts/TreeMap.tsx
new file mode 100644 (file)
index 0000000..6d99476
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { hierarchy as d3Hierarchy, treemap as d3Treemap } from 'd3-hierarchy';
+import * as React from 'react';
+import { formatMeasure, localizeMetric } from '../../helpers/measures';
+import { Location } from '../../helpers/urls';
+import './TreeMap.css';
+import TreeMapRect from './TreeMapRect';
+
+export interface TreeMapItem {
+  color?: string;
+  gradient?: string;
+  icon?: React.ReactNode;
+  key: string;
+  label: string;
+  link?: string | Location;
+  measureValue?: string;
+  metric?: { key: string; type: string };
+  size: number;
+  tooltip?: React.ReactNode;
+}
+
+interface HierarchicalTreemapItem extends TreeMapItem {
+  children?: TreeMapItem[];
+}
+
+interface Props {
+  height: number;
+  items: TreeMapItem[];
+  onRectangleClick?: (item: string) => void;
+  width: number;
+}
+
+export default class TreeMap extends React.PureComponent<Props> {
+  mostCommitPrefix = (labels: string[]) => {
+    const sortedLabels = labels.slice(0).sort();
+    const firstLabel = sortedLabels[0];
+    const firstLabelLength = firstLabel.length;
+    const lastLabel = sortedLabels[sortedLabels.length - 1];
+    let i = 0;
+    while (i < firstLabelLength && firstLabel.charAt(i) === lastLabel.charAt(i)) {
+      i++;
+    }
+    const prefix = firstLabel.substr(0, i);
+    const prefixTokens = prefix.split(/[\s\\/]/);
+    const lastPrefixPart = prefixTokens[prefixTokens.length - 1];
+    return prefix.substr(0, prefix.length - lastPrefixPart.length);
+  };
+
+  render() {
+    const { items, height, width } = this.props;
+    const hierarchy = d3Hierarchy({ children: items } as HierarchicalTreemapItem)
+      .sum((d) => d.size)
+      .sort((a, b) => (b.value || 0) - (a.value || 0));
+
+    const treemap = d3Treemap<TreeMapItem>().round(true).size([width, height]);
+
+    const nodes = treemap(hierarchy).leaves();
+    const prefix = this.mostCommitPrefix(items.map((item) => item.label));
+    const halfWidth = width / 2;
+    return (
+      <div className="sonar-d3">
+        <div className="treemap-container" style={{ width, height }}>
+          {nodes.map((node) => (
+            <TreeMapRect
+              fill={node.data.color}
+              gradient={node.data.gradient}
+              height={node.y1 - node.y0}
+              icon={node.data.icon}
+              itemKey={node.data.key}
+              key={node.data.key}
+              label={node.data.label}
+              link={node.data.link}
+              onClick={this.props.onRectangleClick}
+              placement={node.x0 === 0 || node.x1 < halfWidth ? 'right' : 'left'}
+              prefix={prefix}
+              value={
+                node.data.metric && (
+                  <>
+                    {formatMeasure(node.data.measureValue, node.data.metric.type)}
+                    <span className="little-spacer-left">
+                      {localizeMetric(node.data.metric.key)}
+                    </span>
+                  </>
+                )
+              }
+              tooltip={node.data.tooltip}
+              width={node.x1 - node.x0}
+              x={node.x0}
+              y={node.y0}
+            />
+          ))}
+        </div>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/charts/TreeMapRect.tsx b/server/sonar-ui-common/components/charts/TreeMapRect.tsx
new file mode 100644 (file)
index 0000000..b691a12
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import { scaleLinear } from 'd3-scale';
+import * as React from 'react';
+import { Link } from 'react-router';
+import { Location } from '../../helpers/urls';
+import Tooltip, { Placement } from '../controls/Tooltip';
+import LinkIcon from '../icons/LinkIcon';
+
+const SIZE_SCALE = scaleLinear().domain([3, 15]).range([11, 18]).clamp(true);
+
+interface Props {
+  fill?: string;
+  gradient?: string;
+  height: number;
+  icon?: React.ReactNode;
+  itemKey: string;
+  label: string;
+  link?: string | Location;
+  onClick?: (item: string) => void;
+  placement?: Placement;
+  prefix: string;
+  tooltip?: React.ReactNode;
+  value?: React.ReactNode;
+  width: number;
+  x: number;
+  y: number;
+}
+
+const TEXT_VISIBLE_AT_WIDTH = 80;
+const TEXT_VISIBLE_AT_HEIGHT = 50;
+const ICON_VISIBLE_AT_WIDTH = 60;
+const ICON_VISIBLE_AT_HEIGHT = 30;
+export default class TreeMapRect extends React.PureComponent<Props> {
+  handleLinkClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+    event.stopPropagation();
+  };
+
+  handleRectClick = () => {
+    if (this.props.onClick) {
+      this.props.onClick(this.props.itemKey);
+    }
+  };
+
+  renderLink = () => {
+    const { link, height, width } = this.props;
+    const hasMinSize = width >= 24 && height >= 24 && (width >= 48 || height >= 50);
+    if (!hasMinSize || link == null) {
+      return null;
+    }
+    return (
+      <Link className="treemap-link" onClick={this.handleLinkClick} to={link}>
+        <LinkIcon />
+      </Link>
+    );
+  };
+
+  renderCell = () => {
+    const cellStyles = {
+      left: this.props.x,
+      top: this.props.y,
+      width: this.props.width,
+      height: this.props.height,
+      backgroundColor: this.props.fill,
+      backgroundImage: this.props.gradient,
+      backgroundSize: '12px 12px',
+      fontSize: SIZE_SCALE(this.props.width / this.props.label.length),
+      lineHeight: `${this.props.height}px`,
+      cursor: this.props.onClick != null ? 'pointer' : 'default',
+    };
+    const isTextVisible =
+      this.props.width >= TEXT_VISIBLE_AT_WIDTH && this.props.height >= TEXT_VISIBLE_AT_HEIGHT;
+    const isIconVisible =
+      this.props.width >= ICON_VISIBLE_AT_WIDTH && this.props.height >= ICON_VISIBLE_AT_HEIGHT;
+
+    return (
+      <div
+        className="treemap-cell"
+        onClick={this.handleRectClick}
+        role="treeitem"
+        style={cellStyles}
+        tabIndex={0}>
+        {isTextVisible && (
+          <div className="treemap-inner" style={{ maxWidth: this.props.width }}>
+            {this.props.prefix || this.props.value ? (
+              <div className="treemap-text">
+                <div>
+                  {isIconVisible && (
+                    <span className={classNames('treemap-icon', { 'spacer-right': isTextVisible })}>
+                      {this.props.icon}
+                    </span>
+                  )}
+
+                  {this.props.prefix && (
+                    <>
+                      {this.props.prefix}
+                      <br />
+                    </>
+                  )}
+
+                  {this.props.label.substr(this.props.prefix.length)}
+                </div>
+
+                <div className="treemap-text-suffix little-spacer-top">{this.props.value}</div>
+              </div>
+            ) : (
+              <div className="treemap-text">{this.props.label}</div>
+            )}
+          </div>
+        )}
+        {this.renderLink()}
+      </div>
+    );
+  };
+
+  render() {
+    const { placement, tooltip } = this.props;
+    return (
+      <Tooltip overlay={tooltip || undefined} placement={placement || 'left'}>
+        {this.renderCell()}
+      </Tooltip>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/charts/ZoomTimeLine.css b/server/sonar-ui-common/components/charts/ZoomTimeLine.css
new file mode 100644 (file)
index 0000000..4274a32
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.chart-zoom-tick {
+  fill: var(--secondFontColor);
+  font-size: 10px;
+  text-anchor: middle;
+  user-select: none;
+}
+
+.chart-zoom .zoom-overlay {
+  fill: none;
+  stroke: none;
+  cursor: crosshair;
+  pointer-events: all;
+}
+
+.chart-zoom .zoom-selection {
+  fill: var(--secondFontColor);
+  fill-opacity: 0.2;
+  stroke: var(--secondFontColor);
+  shape-rendering: crispEdges;
+  cursor: move;
+}
+
+.chart-zoom .zoom-selection-handle {
+  cursor: ew-resize;
+  fill-opacity: 0;
+  stroke: none;
+}
diff --git a/server/sonar-ui-common/components/charts/ZoomTimeLine.tsx b/server/sonar-ui-common/components/charts/ZoomTimeLine.tsx
new file mode 100644 (file)
index 0000000..b3c75c4
--- /dev/null
@@ -0,0 +1,399 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import { extent, max } from 'd3-array';
+import { scaleLinear, scalePoint, scaleTime, ScaleTime } from 'd3-scale';
+import { area, curveBasis, line as d3Line } from 'd3-shape';
+import { flatten, sortBy, throttle } from 'lodash';
+import * as React from 'react';
+import Draggable, { DraggableBounds, DraggableCore, DraggableData } from 'react-draggable';
+import { ThemeConsumer } from '../theme';
+import './LineChart.css';
+import './ZoomTimeLine.css';
+
+export interface Props {
+  basisCurve?: boolean;
+  endDate?: Date;
+  height: number;
+  leakPeriodDate?: Date;
+  metricType: string;
+  padding: number[];
+  series: T.Chart.Serie[];
+  showAreas?: boolean;
+  showXTicks: boolean;
+  startDate?: Date;
+  updateZoom: (start?: Date, endDate?: Date) => void;
+  width: number;
+}
+
+interface State {
+  overlayLeftPos?: number;
+  newZoomStart?: number;
+}
+
+type XScale = ScaleTime<number, number>;
+// TODO it should be `ScaleLinear<number, number> | ScalePoint<number> | ScalePoint<string>`, but it's super hard to make it work :'(
+type YScale = any;
+
+export default class ZoomTimeLine extends React.PureComponent<Props, State> {
+  static defaultProps = {
+    padding: [0, 0, 18, 0],
+    showXTicks: true,
+  };
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {};
+    this.handleZoomUpdate = throttle(this.handleZoomUpdate, 40);
+  }
+
+  getRatingScale = (availableHeight: number) => {
+    return scalePoint<number>().domain([5, 4, 3, 2, 1]).range([availableHeight, 0]);
+  };
+
+  getLevelScale = (availableHeight: number) => {
+    return scalePoint().domain(['ERROR', 'WARN', 'OK']).range([availableHeight, 0]);
+  };
+
+  getYScale = (availableHeight: number, flatData: T.Chart.Point[]): YScale => {
+    if (this.props.metricType === 'RATING') {
+      return this.getRatingScale(availableHeight);
+    } else if (this.props.metricType === 'LEVEL') {
+      return this.getLevelScale(availableHeight);
+    } else {
+      return scaleLinear()
+        .range([availableHeight, 0])
+        .domain([0, max(flatData, (d) => Number(d.y || 0)) as number])
+        .nice();
+    }
+  };
+
+  getXScale = (availableWidth: number, flatData: T.Chart.Point[]): XScale => {
+    return scaleTime()
+      .domain(extent(flatData, (d) => d.x) as [Date, Date])
+      .range([0, availableWidth])
+      .clamp(true);
+  };
+
+  getScales = () => {
+    const availableWidth = this.props.width - this.props.padding[1] - this.props.padding[3];
+    const availableHeight = this.props.height - this.props.padding[0] - this.props.padding[2];
+    const flatData = flatten(this.props.series.map((serie) => serie.data));
+    return {
+      xScale: this.getXScale(availableWidth, flatData),
+      yScale: this.getYScale(availableHeight, flatData),
+    };
+  };
+
+  getEventMarker = (size: number) => {
+    const half = size / 2;
+    return `M${half} 0 L${size} ${half} L ${half} ${size} L0 ${half} L${half} 0 L${size} ${half}`;
+  };
+
+  handleDoubleClick = (xScale: XScale, xDim: number[]) => () => {
+    this.handleZoomUpdate(xScale, xDim);
+  };
+
+  handleSelectionDrag = (xScale: XScale, width: number, xDim: number[], checkDelta?: boolean) => (
+    _: MouseEvent,
+    data: DraggableData
+  ) => {
+    if (!checkDelta || data.deltaX) {
+      const x = Math.max(xDim[0], Math.min(data.x, xDim[1] - width));
+      this.handleZoomUpdate(xScale, [x, width + x]);
+    }
+  };
+
+  handleSelectionHandleDrag = (
+    xScale: XScale,
+    fixedX: number,
+    xDim: number[],
+    handleDirection: string,
+    checkDelta?: boolean
+  ) => (_: MouseEvent, data: DraggableData) => {
+    if (!checkDelta || data.deltaX) {
+      const x = Math.max(xDim[0], Math.min(data.x, xDim[1]));
+      this.handleZoomUpdate(xScale, handleDirection === 'right' ? [fixedX, x] : [x, fixedX]);
+    }
+  };
+
+  handleNewZoomDragStart = (xDim: number[]) => (_: MouseEvent, data: DraggableData) => {
+    const overlayLeftPos = data.node.getBoundingClientRect().left;
+    this.setState({
+      overlayLeftPos,
+      newZoomStart: Math.round(Math.max(xDim[0], Math.min(data.x - overlayLeftPos, xDim[1]))),
+    });
+  };
+
+  handleNewZoomDrag = (xScale: XScale, xDim: number[]) => (_: MouseEvent, data: DraggableData) => {
+    const { newZoomStart, overlayLeftPos } = this.state;
+    if (newZoomStart != null && overlayLeftPos != null && data.deltaX) {
+      this.handleZoomUpdate(
+        xScale,
+        sortBy([newZoomStart, Math.max(xDim[0], Math.min(data.x - overlayLeftPos, xDim[1]))])
+      );
+    }
+  };
+
+  handleNewZoomDragEnd = (xScale: XScale, xDim: number[]) => (
+    _: MouseEvent,
+    data: DraggableData
+  ) => {
+    const { newZoomStart, overlayLeftPos } = this.state;
+    if (newZoomStart !== undefined && overlayLeftPos !== undefined) {
+      const x = Math.round(Math.max(xDim[0], Math.min(data.x - overlayLeftPos, xDim[1])));
+      this.handleZoomUpdate(xScale, newZoomStart === x ? xDim : sortBy([newZoomStart, x]));
+      this.setState({ newZoomStart: undefined, overlayLeftPos: undefined });
+    }
+  };
+
+  handleZoomUpdate = (xScale: XScale, xArray: number[]) => {
+    const xRange = xScale.range();
+    const startDate =
+      xArray[0] > xRange[0] && xArray[0] < xRange[xRange.length - 1]
+        ? xScale.invert(xArray[0])
+        : undefined;
+    const endDate =
+      xArray[1] > xRange[0] && xArray[1] < xRange[xRange.length - 1]
+        ? xScale.invert(xArray[1])
+        : undefined;
+    if (this.props.startDate !== startDate || this.props.endDate !== endDate) {
+      this.props.updateZoom(startDate, endDate);
+    }
+  };
+
+  renderBaseLine = (xScale: XScale, yScale: YScale) => {
+    return (
+      <line
+        className="line-chart-grid"
+        x1={xScale.range()[0]}
+        x2={xScale.range()[1]}
+        y1={yScale.range()[0]}
+        y2={yScale.range()[0]}
+      />
+    );
+  };
+
+  renderTicks = (xScale: XScale, yScale: YScale) => {
+    const format = xScale.tickFormat(7);
+    const ticks = xScale.ticks(7);
+    const y = yScale.range()[0];
+    return (
+      <g>
+        {ticks.slice(0, -1).map((tick, index) => {
+          const nextTick = index + 1 < ticks.length ? ticks[index + 1] : xScale.domain()[1];
+          const x = (xScale(tick) + xScale(nextTick)) / 2;
+          return (
+            <text className="chart-zoom-tick" dy="1.3em" key={index} x={x} y={y}>
+              {format(tick)}
+            </text>
+          );
+        })}
+      </g>
+    );
+  };
+
+  renderLeak = (xScale: XScale, yScale: YScale) => {
+    const { leakPeriodDate } = this.props;
+    if (!leakPeriodDate) {
+      return null;
+    }
+    const yRange = yScale.range();
+    return (
+      <ThemeConsumer>
+        {(theme) => (
+          <rect
+            fill={theme.colors.leakPrimaryColor}
+            height={yRange[0] - yRange[yRange.length - 1]}
+            width={xScale.range()[1] - xScale(leakPeriodDate)}
+            x={xScale(leakPeriodDate)}
+            y={yRange[yRange.length - 1]}
+          />
+        )}
+      </ThemeConsumer>
+    );
+  };
+
+  renderLines = (xScale: XScale, yScale: YScale) => {
+    const lineGenerator = d3Line<T.Chart.Point>()
+      .defined((d) => Boolean(d.y || d.y === 0))
+      .x((d) => xScale(d.x))
+      .y((d) => yScale(d.y));
+    if (this.props.basisCurve) {
+      lineGenerator.curve(curveBasis);
+    }
+    return (
+      <g>
+        {this.props.series.map((serie, idx) => (
+          <path
+            className={classNames('line-chart-path', 'line-chart-path-' + idx)}
+            d={lineGenerator(serie.data) || undefined}
+            key={serie.name}
+          />
+        ))}
+      </g>
+    );
+  };
+
+  renderAreas = (xScale: XScale, yScale: YScale) => {
+    const areaGenerator = area<T.Chart.Point>()
+      .defined((d) => Boolean(d.y || d.y === 0))
+      .x((d) => xScale(d.x))
+      .y1((d) => yScale(d.y))
+      .y0(yScale(0));
+    if (this.props.basisCurve) {
+      areaGenerator.curve(curveBasis);
+    }
+    return (
+      <g>
+        {this.props.series.map((serie, idx) => (
+          <path
+            className={classNames('line-chart-area', 'line-chart-area-' + idx)}
+            d={areaGenerator(serie.data) || undefined}
+            key={serie.name}
+          />
+        ))}
+      </g>
+    );
+  };
+
+  renderZoomHandle = (options: {
+    xScale: XScale;
+    xPos: number;
+    fixedPos: number;
+    yDim: number[];
+    xDim: number[];
+    direction: string;
+  }) => (
+    <Draggable
+      axis="x"
+      bounds={{ left: options.xDim[0], right: options.xDim[1] } as DraggableBounds}
+      onDrag={this.handleSelectionHandleDrag(
+        options.xScale,
+        options.fixedPos,
+        options.xDim,
+        options.direction,
+        true
+      )}
+      onStop={this.handleSelectionHandleDrag(
+        options.xScale,
+        options.fixedPos,
+        options.xDim,
+        options.direction
+      )}
+      position={{ x: options.xPos, y: 0 }}>
+      <rect
+        className="zoom-selection-handle"
+        height={options.yDim[0] - options.yDim[1] + 1}
+        width={6}
+        x={-3}
+        y={options.yDim[1]}
+      />
+    </Draggable>
+  );
+
+  renderZoom = (xScale: XScale, yScale: YScale) => {
+    const xRange = xScale.range();
+    const yRange = yScale.range();
+    const xDim = [xRange[0], xRange[xRange.length - 1]];
+    const yDim = [yRange[0], yRange[yRange.length - 1]];
+    const startX = Math.round(this.props.startDate ? xScale(this.props.startDate) : xDim[0]);
+    const endX = Math.round(this.props.endDate ? xScale(this.props.endDate) : xDim[1]);
+    const xArray = sortBy([startX, endX]);
+    const zoomBoxWidth = xArray[1] - xArray[0];
+    const showZoomArea =
+      this.state.newZoomStart == null ||
+      this.state.newZoomStart === startX ||
+      this.state.newZoomStart === endX;
+
+    return (
+      <g className="chart-zoom">
+        <DraggableCore
+          onDrag={this.handleNewZoomDrag(xScale, xDim)}
+          onStart={this.handleNewZoomDragStart(xDim)}
+          onStop={this.handleNewZoomDragEnd(xScale, xDim)}>
+          <rect
+            className="zoom-overlay"
+            height={yDim[0] - yDim[1]}
+            width={xDim[1] - xDim[0]}
+            x={xDim[0]}
+            y={yDim[1]}
+          />
+        </DraggableCore>
+        {showZoomArea && (
+          <Draggable
+            axis="x"
+            bounds={{ left: xDim[0], right: Math.floor(xDim[1] - zoomBoxWidth) } as DraggableBounds}
+            onDrag={this.handleSelectionDrag(xScale, zoomBoxWidth, xDim, true)}
+            onStop={this.handleSelectionDrag(xScale, zoomBoxWidth, xDim)}
+            position={{ x: xArray[0], y: 0 }}>
+            <rect
+              className="zoom-selection"
+              height={yDim[0] - yDim[1] + 1}
+              onDoubleClick={this.handleDoubleClick(xScale, xDim)}
+              width={zoomBoxWidth}
+              x={0}
+              y={yDim[1]}
+            />
+          </Draggable>
+        )}
+        {showZoomArea &&
+          this.renderZoomHandle({
+            xScale,
+            xPos: startX,
+            fixedPos: endX,
+            xDim,
+            yDim,
+            direction: 'left',
+          })}
+        {showZoomArea &&
+          this.renderZoomHandle({
+            xScale,
+            xPos: endX,
+            fixedPos: startX,
+            xDim,
+            yDim,
+            direction: 'right',
+          })}
+      </g>
+    );
+  };
+
+  render() {
+    if (!this.props.width || !this.props.height) {
+      return <div />;
+    }
+
+    const { xScale, yScale } = this.getScales();
+
+    return (
+      <svg className="line-chart " height={this.props.height} width={this.props.width}>
+        <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0] + 2})`}>
+          {this.renderLeak(xScale, yScale)}
+          {this.renderBaseLine(xScale, yScale)}
+          {this.props.showXTicks && this.renderTicks(xScale, yScale)}
+          {this.props.showAreas && this.renderAreas(xScale, yScale)}
+          {this.renderLines(xScale, yScale)}
+          {this.renderZoom(xScale, yScale)}
+        </g>
+      </svg>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/charts/__tests__/AdvancedTimeline-test.tsx b/server/sonar-ui-common/components/charts/__tests__/AdvancedTimeline-test.tsx
new file mode 100644 (file)
index 0000000..736ffcf
--- /dev/null
@@ -0,0 +1,192 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { ThemeConsumer } from '../../theme';
+import AdvancedTimeline from '../AdvancedTimeline';
+
+const newCodeLegendClass = '.new-code-legend';
+
+// Replace scaleTime with scaleUtc to avoid timezone-dependent snapshots
+jest.mock('d3-scale', () => {
+  const { scaleUtc, ...others } = jest.requireActual('d3-scale');
+
+  return {
+    ...others,
+    scaleTime: scaleUtc,
+  };
+});
+
+jest.mock('lodash', () => {
+  const lodash = jest.requireActual('lodash');
+  return { ...lodash, throttle: (f) => f };
+});
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should render leak correctly', () => {
+  const wrapper = shallowRender({ leakPeriodDate: new Date('2019-10-02') });
+
+  const leakNode = wrapper.find(ThemeConsumer).dive().find('.leak-chart-rect');
+  expect(leakNode.exists()).toBe(true);
+  expect(leakNode.getElement().props.width).toBe(15);
+});
+
+it('should render leak legend correctly', () => {
+  const wrapper = shallowRender({
+    displayNewCodeLegend: true,
+    leakPeriodDate: new Date('2019-10-02'),
+  });
+
+  const leakNode = wrapper.find(ThemeConsumer).dive();
+  expect(leakNode.find(newCodeLegendClass).exists()).toBe(true);
+  expect(leakNode.find(newCodeLegendClass).props().textAnchor).toBe('start');
+  expect(leakNode).toMatchSnapshot();
+});
+
+it('should render leak legend correctly for small leak', () => {
+  const wrapper = shallowRender({
+    displayNewCodeLegend: true,
+    leakPeriodDate: new Date('2020-02-06'),
+    series: [
+      mockData(1, '2020-02-01'),
+      mockData(2, '2020-02-02'),
+      mockData(3, '2020-02-03'),
+      mockData(4, '2020-02-04'),
+      mockData(5, '2020-02-05'),
+      mockData(6, '2020-02-06'),
+      mockData(7, '2020-02-07'),
+    ],
+  });
+
+  const leakNode = wrapper.find(ThemeConsumer).dive();
+  expect(leakNode.find(newCodeLegendClass).exists()).toBe(true);
+  expect(leakNode.find(newCodeLegendClass).props().textAnchor).toBe('end');
+});
+
+it('should set leakLegendTextWidth correctly', () => {
+  const wrapper = shallowRender();
+
+  wrapper.instance().setLeakLegendTextWidth({
+    getBoundingClientRect: () => ({ width: 12 } as DOMRect),
+  } as SVGTextElement);
+
+  expect(wrapper.state().leakLegendTextWidth).toBe(12);
+
+  wrapper.instance().setLeakLegendTextWidth(null);
+
+  expect(wrapper.state().leakLegendTextWidth).toBe(12);
+});
+
+it('should render old leak correctly', () => {
+  const wrapper = shallowRender({ leakPeriodDate: new Date('2014-10-02') });
+
+  const leakNode = wrapper.find(ThemeConsumer).dive().find('.leak-chart-rect');
+  expect(leakNode.exists()).toBe(true);
+  expect(leakNode.getElement().props.width).toBe(30);
+});
+
+it('should find date to display based on mouse location', () => {
+  const wrapper = shallowRender();
+
+  wrapper.instance().updateTooltipPos(0);
+  expect(wrapper.state().selectedDateIdx).toBeUndefined();
+
+  wrapper.instance().handleMouseEnter();
+  wrapper.instance().updateTooltipPos(10);
+  expect(wrapper.state().selectedDateIdx).toBe(1);
+});
+
+it('should update timeline when width changes', () => {
+  const updateTooltip = jest.fn();
+  const wrapper = shallowRender({ selectedDate: new Date('2019-10-02'), updateTooltip });
+  const { xScale, selectedDateXPos } = wrapper.state();
+
+  wrapper.setProps({ width: 200 });
+  expect(wrapper.state().xScale).not.toBe(xScale);
+  expect(wrapper.state().xScale).toEqual(expect.any(Function));
+  expect(wrapper.state().selectedDateXPos).not.toBe(selectedDateXPos);
+  expect(wrapper.state().selectedDateXPos).toEqual(expect.any(Number));
+  expect(updateTooltip).toBeCalled();
+});
+
+it('should update tootlips when selected date changes', () => {
+  const updateTooltip = jest.fn();
+
+  const wrapper = shallowRender({ selectedDate: new Date('2019-10-01'), updateTooltip });
+  const { xScale, selectedDateXPos } = wrapper.state();
+  const selectedDate = new Date('2019-10-02');
+
+  wrapper.setProps({ selectedDate });
+  expect(wrapper.state().xScale).toBe(xScale);
+  expect(wrapper.state().selectedDate).toBe(selectedDate);
+  expect(wrapper.state().selectedDateXPos).not.toBe(selectedDateXPos);
+  expect(wrapper.state().selectedDateXPos).toEqual(expect.any(Number));
+  expect(updateTooltip).toBeCalled();
+});
+
+function shallowRender(props?: Partial<AdvancedTimeline['props']>) {
+  return shallow<AdvancedTimeline>(
+    <AdvancedTimeline
+      height={100}
+      maxYTicksCount={10}
+      metricType="TEST_METRIC"
+      series={[
+        {
+          name: 'test-1',
+          type: 'test-type-1',
+          data: [
+            {
+              x: new Date('2019-10-01'),
+              y: 1,
+            },
+            {
+              x: new Date('2019-10-02'),
+              y: 2,
+            },
+          ],
+        },
+        {
+          name: 'test-2',
+          type: 'test-type-2',
+          data: [
+            {
+              x: new Date('2019-10-03'),
+              y: 3,
+            },
+          ],
+        },
+      ]}
+      width={100}
+      zoomSpeed={1}
+      {...props}
+    />
+  );
+}
+
+function mockData(i: number, date: string) {
+  return {
+    name: `t${i}`,
+    type: 'type',
+    data: [{ x: new Date(date), y: i }],
+  };
+}
diff --git a/server/sonar-ui-common/components/charts/__tests__/BarChart-test.tsx b/server/sonar-ui-common/components/charts/__tests__/BarChart-test.tsx
new file mode 100644 (file)
index 0000000..3ba15c9
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import BarChart from '../BarChart';
+
+it('should display bars', () => {
+  const data = [
+    { x: 1, y: 10 },
+    { x: 2, y: 30 },
+    { x: 3, y: 20 },
+  ];
+  const chart = shallow(<BarChart barsWidth={20} data={data} height={100} width={100} />);
+  expect(chart.find('.bar-chart-bar').length).toBe(3);
+});
+
+it('should display ticks', () => {
+  const data = [
+    { x: 1, y: 10 },
+    { x: 2, y: 30 },
+    { x: 3, y: 20 },
+  ];
+  const ticks = ['A', 'B', 'C'];
+  const chart = shallow(
+    <BarChart barsWidth={20} data={data} height={100} width={100} xTicks={ticks} />
+  );
+  expect(chart.find('.bar-chart-tick').length).toBe(3);
+});
+
+it('should display values', () => {
+  const data = [
+    { x: 1, y: 10 },
+    { x: 2, y: 30 },
+    { x: 3, y: 20 },
+  ];
+  const values = ['A', 'B', 'C'];
+  const chart = shallow(
+    <BarChart barsWidth={20} data={data} height={100} width={100} xValues={values} />
+  );
+  expect(chart.find('.bar-chart-tick').length).toBe(3);
+});
+
+it('should display bars, ticks and values', () => {
+  const data = [
+    { x: 1, y: 10 },
+    { x: 2, y: 30 },
+    { x: 3, y: 20 },
+  ];
+  const ticks = ['A', 'B', 'C'];
+  const values = ['A', 'B', 'C'];
+  const chart = shallow(
+    <BarChart barsWidth={20} data={data} height={100} width={100} xTicks={ticks} xValues={values} />
+  );
+  expect(chart.find('.bar-chart-bar').length).toBe(3);
+  expect(chart.find('.bar-chart-tick').length).toBe(6);
+});
diff --git a/server/sonar-ui-common/components/charts/__tests__/BubbleChart-test.tsx b/server/sonar-ui-common/components/charts/__tests__/BubbleChart-test.tsx
new file mode 100644 (file)
index 0000000..20b57ac
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { mount } from 'enzyme';
+import * as React from 'react';
+import { AutoSizerProps } from 'react-virtualized';
+import BubbleChart from '../BubbleChart';
+
+jest.mock('react-virtualized/dist/commonjs/AutoSizer', () => ({
+  AutoSizer: ({ children }: AutoSizerProps) => children({ width: 100, height: NaN }),
+}));
+
+it('should display bubbles', () => {
+  const items = [
+    { x: 1, y: 10, size: 7 },
+    { x: 2, y: 30, size: 5 },
+  ];
+  const chart = mount(<BubbleChart height={100} items={items} padding={[0, 0, 0, 0]} />);
+  chart.find('Bubble').forEach((bubble) => expect(bubble).toMatchSnapshot());
+
+  chart.setProps({ height: 120 });
+});
+
+it('should render bubble links', () => {
+  const items = [
+    { x: 1, y: 10, size: 7, link: 'foo' },
+    { x: 2, y: 30, size: 5, link: 'bar' },
+  ];
+  const chart = mount(<BubbleChart height={100} items={items} padding={[0, 0, 0, 0]} />);
+  chart.find('Bubble').forEach((bubble) => expect(bubble).toMatchSnapshot());
+});
+
+it('should render bubbles with click handlers', () => {
+  const onClick = jest.fn();
+  const items = [
+    { x: 1, y: 10, size: 7, data: 'foo' },
+    { x: 2, y: 30, size: 5, data: 'bar' },
+  ];
+  const chart = mount(
+    <BubbleChart height={100} items={items} onBubbleClick={onClick} padding={[0, 0, 0, 0]} />
+  );
+  chart.find('Bubble').forEach((bubble) => expect(bubble).toMatchSnapshot());
+});
diff --git a/server/sonar-ui-common/components/charts/__tests__/ColorGradientLegend-test.tsx b/server/sonar-ui-common/components/charts/__tests__/ColorGradientLegend-test.tsx
new file mode 100644 (file)
index 0000000..cf5a9cb
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { scaleLinear } from 'd3-scale';
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import testTheme from '../../../config/jest/testTheme';
+import ColorGradientLegend from '../ColorGradientLegend';
+
+const { colors } = testTheme;
+const COLORS = [colors.green, colors.lightGreen, colors.yellow, colors.orange, colors.red];
+
+it('should render properly', () => {
+  const colorScale = scaleLinear<string, string>().domain([0, 25, 50, 75, 100]).range(COLORS);
+  const wrapper = shallow(
+    <ColorGradientLegend
+      className="measure-details-treemap-legend"
+      colorScale={colorScale}
+      showColorNA={true}
+      height={20}
+      width={200}
+    />
+  );
+  expect(wrapper.dive()).toMatchSnapshot();
+});
diff --git a/server/sonar-ui-common/components/charts/__tests__/DonutChart-test.tsx b/server/sonar-ui-common/components/charts/__tests__/DonutChart-test.tsx
new file mode 100644 (file)
index 0000000..62e7f9f
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import DonutChart, { DonutChartProps } from '../DonutChart';
+
+it('should render correctly', () => {
+  const wrapper = shallowRender();
+  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.find('Sector').first().dive()).toMatchSnapshot();
+});
+
+it('should render correctly with padding and pad angle too', () => {
+  expect(shallowRender({ padAngle: 0.1, padding: [2, 2, 2, 2] })).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<DonutChartProps> = {}) {
+  return shallow(
+    <DonutChart
+      data={[
+        { fill: '#000000', value: 25 },
+        { fill: '#ffffff', value: 75 },
+      ]}
+      height={20}
+      thickness={2}
+      width={20}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-ui-common/components/charts/__tests__/Histogram-test.tsx b/server/sonar-ui-common/components/charts/__tests__/Histogram-test.tsx
new file mode 100644 (file)
index 0000000..58c92c1
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import Histogram from '../Histogram';
+
+it('renders', () => {
+  expect(shallow(<Histogram bars={[100, 75, 150]} height={75} width={100} />)).toMatchSnapshot();
+});
+
+it('renders with yValues', () => {
+  expect(
+    shallow(
+      <Histogram
+        bars={[100, 75, 150]}
+        height={75}
+        width={100}
+        yValues={['100.0', '75.0', '150.0']}
+      />
+    )
+  ).toMatchSnapshot();
+});
+
+it('renders with yValues and yTicks', () => {
+  expect(
+    shallow(
+      <Histogram
+        bars={[100, 75, 150]}
+        height={75}
+        width={100}
+        yTicks={['a', 'b', 'c']}
+        yValues={['100.0', '75.0', '150.0']}
+      />
+    )
+  ).toMatchSnapshot();
+});
+
+it('renders with yValues, yTicks and yTooltips', () => {
+  expect(
+    shallow(
+      <Histogram
+        bars={[100, 75, 150]}
+        height={75}
+        width={100}
+        yTicks={['a', 'b', 'c']}
+        yTooltips={['a - 100', 'b - 75', 'c - 150']}
+        yValues={['100.0', '75.0', '150.0']}
+      />
+    )
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-ui-common/components/charts/__tests__/LineChart-test.tsx b/server/sonar-ui-common/components/charts/__tests__/LineChart-test.tsx
new file mode 100644 (file)
index 0000000..77128ab
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import LineChart from '../LineChart';
+
+it('should display line', () => {
+  const data = [
+    { x: 1, y: 10 },
+    { x: 2, y: 30 },
+    { x: 3, y: 20 },
+  ];
+  const chart = shallow(<LineChart data={data} height={100} width={100} />);
+  expect(chart.find('.line-chart-path').length).toBe(1);
+});
+
+it('should display ticks', () => {
+  const data = [
+    { x: 1, y: 10 },
+    { x: 2, y: 30 },
+    { x: 3, y: 20 },
+  ];
+  const ticks = ['A', 'B', 'C'];
+  const chart = shallow(<LineChart data={data} height={100} width={100} xTicks={ticks} />);
+  expect(chart.find('.line-chart-tick').length).toBe(3);
+});
+
+it('should display values', () => {
+  const data = [
+    { x: 1, y: 10 },
+    { x: 2, y: 30 },
+    { x: 3, y: 20 },
+  ];
+  const values = ['A', 'B', 'C'];
+  const chart = shallow(<LineChart data={data} height={100} width={100} xValues={values} />);
+  expect(chart.find('.line-chart-tick').length).toBe(3);
+});
diff --git a/server/sonar-ui-common/components/charts/__tests__/TreeMap-test.tsx b/server/sonar-ui-common/components/charts/__tests__/TreeMap-test.tsx
new file mode 100644 (file)
index 0000000..168ad9a
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { mount } from 'enzyme';
+import * as React from 'react';
+import TreeMap from '../TreeMap';
+import TreeMapRect from '../TreeMapRect';
+
+it('should render correctly', () => {
+  const items = [
+    { key: '1', size: 10, color: '#777', label: 'SonarQube :: Server' },
+    { key: '2', size: 30, color: '#777', label: 'SonarQube :: Web' },
+    {
+      key: '3',
+      size: 20,
+      gradient: '#777',
+      label: 'SonarQube :: Search',
+      metric: { key: 'coverage', type: 'PERCENT' },
+    },
+  ];
+  const onRectClick = jest.fn();
+  const chart = mount(
+    <TreeMap height={100} items={items} onRectangleClick={onRectClick} width={100} />
+  );
+  const rects = chart.find(TreeMapRect);
+  expect(rects).toHaveLength(3);
+
+  const event: React.MouseEvent<HTMLAnchorElement> = {
+    stopPropagation: jest.fn(),
+  } as any;
+
+  (rects.first().instance() as TreeMapRect).handleLinkClick(event);
+  expect(event.stopPropagation).toHaveBeenCalled();
+
+  (rects.first().instance() as TreeMapRect).handleRectClick();
+  expect(onRectClick).toHaveBeenCalledWith('2');
+});
diff --git a/server/sonar-ui-common/components/charts/__tests__/ZoomTimeLine-test.tsx b/server/sonar-ui-common/components/charts/__tests__/ZoomTimeLine-test.tsx
new file mode 100644 (file)
index 0000000..07b2378
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import testTheme from '../../../config/jest/testTheme';
+import ZoomTimeLine from '../ZoomTimeLine';
+
+const series = [
+  {
+    data: [
+      {
+        x: new Date('2020-01-01'),
+        y: 'beginning',
+      },
+      {
+        x: new Date('2020-02-01'),
+        y: 'end',
+      },
+    ],
+    name: 'foo',
+    translatedName: 'foo-translated',
+    type: 'bar',
+  },
+];
+
+it('should draw a graph with lines', () => {
+  const wrapper = shallowRender();
+  expect(wrapper.find('.line-chart-grid').exists()).toBe(true);
+  expect(wrapper.find('.line-chart-path').exists()).toBe(true);
+  expect(wrapper.find('.chart-zoom-tick').exists()).toBe(true);
+  expect(wrapper.find('.line-chart-area').exists()).toBe(false);
+});
+
+it('should be zoomable', () => {
+  expect(shallowRender().find('.chart-zoom').exists()).toBe(true);
+});
+
+it('should render a leak period', () => {
+  expect(
+    shallowRender({ leakPeriodDate: new Date('2020-01-01') })
+      .find('ContextConsumer')
+      .dive()
+      .find(`rect[fill="${testTheme.colors.leakPrimaryColor}"]`)
+      .exists()
+  ).toBe(true);
+});
+
+it('should render areas under the graph lines', () => {
+  expect(shallowRender({ showAreas: true }).find('.line-chart-area').exists()).toBe(true);
+});
+
+function shallowRender(props: Partial<ZoomTimeLine['props']> = {}) {
+  return shallow<ZoomTimeLine>(
+    <ZoomTimeLine
+      width={300}
+      series={series}
+      updateZoom={jest.fn()}
+      metricType="RATING"
+      height={300}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-ui-common/components/charts/__tests__/__snapshots__/AdvancedTimeline-test.tsx.snap b/server/sonar-ui-common/components/charts/__tests__/__snapshots__/AdvancedTimeline-test.tsx.snap
new file mode 100644 (file)
index 0000000..d3b1d29
--- /dev/null
@@ -0,0 +1,330 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<svg
+  className="line-chart"
+  height={100}
+  width={100}
+>
+  <g
+    transform="translate(60, 26)"
+  >
+    <g>
+      <g
+        key="0"
+      >
+        <line
+          className="line-chart-grid"
+          x1={0}
+          x2={30}
+          y1={24}
+          y2={24}
+        />
+      </g>
+      <g
+        key="0.2"
+      >
+        <line
+          className="line-chart-grid"
+          x1={0}
+          x2={30}
+          y1={22.4}
+          y2={22.4}
+        />
+      </g>
+      <g
+        key="0.4"
+      >
+        <line
+          className="line-chart-grid"
+          x1={0}
+          x2={30}
+          y1={20.8}
+          y2={20.8}
+        />
+      </g>
+      <g
+        key="0.6"
+      >
+        <line
+          className="line-chart-grid"
+          x1={0}
+          x2={30}
+          y1={19.200000000000003}
+          y2={19.200000000000003}
+        />
+      </g>
+      <g
+        key="0.8"
+      >
+        <line
+          className="line-chart-grid"
+          x1={0}
+          x2={30}
+          y1={17.6}
+          y2={17.6}
+        />
+      </g>
+      <g
+        key="1"
+      >
+        <line
+          className="line-chart-grid"
+          x1={0}
+          x2={30}
+          y1={16}
+          y2={16}
+        />
+      </g>
+      <g
+        key="1.2"
+      >
+        <line
+          className="line-chart-grid"
+          x1={0}
+          x2={30}
+          y1={14.400000000000002}
+          y2={14.400000000000002}
+        />
+      </g>
+      <g
+        key="1.4"
+      >
+        <line
+          className="line-chart-grid"
+          x1={0}
+          x2={30}
+          y1={12.800000000000002}
+          y2={12.800000000000002}
+        />
+      </g>
+      <g
+        key="1.6"
+      >
+        <line
+          className="line-chart-grid"
+          x1={0}
+          x2={30}
+          y1={11.2}
+          y2={11.2}
+        />
+      </g>
+      <g
+        key="1.8"
+      >
+        <line
+          className="line-chart-grid"
+          x1={0}
+          x2={30}
+          y1={9.600000000000001}
+          y2={9.600000000000001}
+        />
+      </g>
+      <g
+        key="2"
+      >
+        <line
+          className="line-chart-grid"
+          x1={0}
+          x2={30}
+          y1={8}
+          y2={8}
+        />
+      </g>
+      <g
+        key="2.2"
+      >
+        <line
+          className="line-chart-grid"
+          x1={0}
+          x2={30}
+          y1={6.399999999999999}
+          y2={6.399999999999999}
+        />
+      </g>
+      <g
+        key="2.4"
+      >
+        <line
+          className="line-chart-grid"
+          x1={0}
+          x2={30}
+          y1={4.800000000000002}
+          y2={4.800000000000002}
+        />
+      </g>
+      <g
+        key="2.6"
+      >
+        <line
+          className="line-chart-grid"
+          x1={0}
+          x2={30}
+          y1={3.1999999999999993}
+          y2={3.1999999999999993}
+        />
+      </g>
+      <g
+        key="2.8"
+      >
+        <line
+          className="line-chart-grid"
+          x1={0}
+          x2={30}
+          y1={1.6000000000000023}
+          y2={1.6000000000000023}
+        />
+      </g>
+      <g
+        key="3"
+      >
+        <line
+          className="line-chart-grid"
+          x1={0}
+          x2={30}
+          y1={0}
+          y2={0}
+        />
+      </g>
+    </g>
+    <g
+      transform="translate(0, 20)"
+    >
+      <text
+        className="line-chart-tick"
+        key="0"
+        textAnchor="end"
+        transform="rotate(-35, 1.875, 24)"
+        x={1.875}
+        y={24}
+      >
+        October
+      </text>
+      <text
+        className="line-chart-tick"
+        key="1"
+        textAnchor="end"
+        transform="rotate(-35, 5.625, 24)"
+        x={5.625}
+        y={24}
+      >
+        06 AM
+      </text>
+      <text
+        className="line-chart-tick"
+        key="2"
+        textAnchor="end"
+        transform="rotate(-35, 9.375, 24)"
+        x={9.375}
+        y={24}
+      >
+        12 PM
+      </text>
+      <text
+        className="line-chart-tick"
+        key="3"
+        textAnchor="end"
+        transform="rotate(-35, 13.125, 24)"
+        x={13.125}
+        y={24}
+      >
+        06 PM
+      </text>
+      <text
+        className="line-chart-tick"
+        key="4"
+        textAnchor="end"
+        transform="rotate(-35, 16.875, 24)"
+        x={16.875}
+        y={24}
+      >
+        Wed 02
+      </text>
+      <text
+        className="line-chart-tick"
+        key="5"
+        textAnchor="end"
+        transform="rotate(-35, 20.625, 24)"
+        x={20.625}
+        y={24}
+      >
+        06 AM
+      </text>
+      <text
+        className="line-chart-tick"
+        key="6"
+        textAnchor="end"
+        transform="rotate(-35, 24.375, 24)"
+        x={24.375}
+        y={24}
+      >
+        12 PM
+      </text>
+      <text
+        className="line-chart-tick"
+        key="7"
+        textAnchor="end"
+        transform="rotate(-35, 28.125, 24)"
+        x={28.125}
+        y={24}
+      >
+        06 PM
+      </text>
+    </g>
+    <g>
+      <path
+        className="line-chart-path line-chart-path-0"
+        d="M0,16L15,8"
+        key="test-1"
+      />
+      <path
+        className="line-chart-path line-chart-path-1"
+        d="M30,0Z"
+        key="test-2"
+      />
+    </g>
+    <g>
+      <circle
+        className="line-chart-dot line-chart-dot-1"
+        cx={30}
+        cy={0}
+        key="test-20"
+        r="2"
+      />
+    </g>
+    <rect
+      className="chart-mouse-events-overlay"
+      height={24}
+      width={30}
+    />
+  </g>
+</svg>
+`;
+
+exports[`should render leak legend correctly 1`] = `
+<Fragment>
+  <rect
+    fill="#fbf3d5"
+    height={16}
+    width={15}
+    x={15}
+    y={-16}
+  />
+  <text
+    className="new-code-legend"
+    textAnchor="start"
+    x={19}
+    y={-4}
+  >
+    new code
+  </text>
+  <rect
+    className="leak-chart-rect"
+    fill="#fbf3d5"
+    height={24}
+    width={15}
+    x={15}
+    y={0}
+  />
+</Fragment>
+`;
diff --git a/server/sonar-ui-common/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap b/server/sonar-ui-common/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap
new file mode 100644 (file)
index 0000000..b0f69f9
--- /dev/null
@@ -0,0 +1,187 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display bubbles 1`] = `
+<Bubble
+  key="0"
+  r={45}
+  scale={1}
+  x={33.21428571428571}
+  y={70.07936507936509}
+>
+  <Tooltip>
+    <g>
+      <circle
+        className="bubble-chart-bubble"
+        r={45}
+        style={
+          Object {
+            "fill": undefined,
+            "stroke": undefined,
+          }
+        }
+        transform="translate(33.21428571428571, 70.07936507936509) scale(1)"
+      />
+    </g>
+  </Tooltip>
+</Bubble>
+`;
+
+exports[`should display bubbles 2`] = `
+<Bubble
+  key="1"
+  r={33.57142857142858}
+  scale={1}
+  x={66.42857142857142}
+  y={33.57142857142858}
+>
+  <Tooltip>
+    <g>
+      <circle
+        className="bubble-chart-bubble"
+        r={33.57142857142858}
+        style={
+          Object {
+            "fill": undefined,
+            "stroke": undefined,
+          }
+        }
+        transform="translate(66.42857142857142, 33.57142857142858) scale(1)"
+      />
+    </g>
+  </Tooltip>
+</Bubble>
+`;
+
+exports[`should render bubble links 1`] = `
+<Bubble
+  key="0"
+  link="foo"
+  r={45}
+  scale={1}
+  x={33.21428571428571}
+  y={70.07936507936509}
+>
+  <Tooltip>
+    <g>
+      <Link
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="foo"
+      >
+        <a
+          onClick={[Function]}
+          style={Object {}}
+        >
+          <circle
+            className="bubble-chart-bubble"
+            r={45}
+            style={
+              Object {
+                "fill": undefined,
+                "stroke": undefined,
+              }
+            }
+            transform="translate(33.21428571428571, 70.07936507936509) scale(1)"
+          />
+        </a>
+      </Link>
+    </g>
+  </Tooltip>
+</Bubble>
+`;
+
+exports[`should render bubble links 2`] = `
+<Bubble
+  key="1"
+  link="bar"
+  r={33.57142857142858}
+  scale={1}
+  x={66.42857142857142}
+  y={33.57142857142858}
+>
+  <Tooltip>
+    <g>
+      <Link
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="bar"
+      >
+        <a
+          onClick={[Function]}
+          style={Object {}}
+        >
+          <circle
+            className="bubble-chart-bubble"
+            r={33.57142857142858}
+            style={
+              Object {
+                "fill": undefined,
+                "stroke": undefined,
+              }
+            }
+            transform="translate(66.42857142857142, 33.57142857142858) scale(1)"
+          />
+        </a>
+      </Link>
+    </g>
+  </Tooltip>
+</Bubble>
+`;
+
+exports[`should render bubbles with click handlers 1`] = `
+<Bubble
+  data="foo"
+  key="0"
+  onClick={[MockFunction]}
+  r={45}
+  scale={1}
+  x={33.21428571428571}
+  y={70.07936507936509}
+>
+  <Tooltip>
+    <g>
+      <circle
+        className="bubble-chart-bubble"
+        onClick={[Function]}
+        r={45}
+        style={
+          Object {
+            "fill": undefined,
+            "stroke": undefined,
+          }
+        }
+        transform="translate(33.21428571428571, 70.07936507936509) scale(1)"
+      />
+    </g>
+  </Tooltip>
+</Bubble>
+`;
+
+exports[`should render bubbles with click handlers 2`] = `
+<Bubble
+  data="bar"
+  key="1"
+  onClick={[MockFunction]}
+  r={33.57142857142858}
+  scale={1}
+  x={66.42857142857142}
+  y={33.57142857142858}
+>
+  <Tooltip>
+    <g>
+      <circle
+        className="bubble-chart-bubble"
+        onClick={[Function]}
+        r={33.57142857142858}
+        style={
+          Object {
+            "fill": undefined,
+            "stroke": undefined,
+          }
+        }
+        transform="translate(66.42857142857142, 33.57142857142858) scale(1)"
+      />
+    </g>
+  </Tooltip>
+</Bubble>
+`;
diff --git a/server/sonar-ui-common/components/charts/__tests__/__snapshots__/ColorGradientLegend-test.tsx.snap b/server/sonar-ui-common/components/charts/__tests__/__snapshots__/ColorGradientLegend-test.tsx.snap
new file mode 100644 (file)
index 0000000..0bbe8ee
--- /dev/null
@@ -0,0 +1,220 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render properly 1`] = `
+<svg
+  className="measure-details-treemap-legend"
+  height={20}
+  width={200}
+>
+  <defs>
+    <linearGradient
+      id="gradient-legend"
+    >
+      <stop
+        key="0"
+        offset={0}
+        stopColor="#00aa00"
+      />
+      <stop
+        key="1"
+        offset={0.25}
+        stopColor="#b0d513"
+      />
+      <stop
+        key="2"
+        offset={0.5}
+        stopColor="#eabe06"
+      />
+      <stop
+        key="3"
+        offset={0.75}
+        stopColor="#ed7d20"
+      />
+      <stop
+        key="4"
+        offset={1}
+        stopColor="#d4333f"
+      />
+    </linearGradient>
+    <pattern
+      height="30"
+      id="stripes"
+      patternTransform="rotate(45 0 0)"
+      patternUnits="userSpaceOnUse"
+      width="30"
+    >
+      <line
+        style={
+          Object {
+            "stroke": "#b4b4b4",
+            "strokeWidth": 4,
+          }
+        }
+        x1={0}
+        x2={0}
+        y1="0"
+        y2="30"
+      />
+      <line
+        style={
+          Object {
+            "stroke": "#999",
+            "strokeWidth": 4,
+          }
+        }
+        x1={4}
+        x2={4}
+        y1="0"
+        y2="30"
+      />
+      <line
+        style={
+          Object {
+            "stroke": "#b4b4b4",
+            "strokeWidth": 4,
+          }
+        }
+        x1={8}
+        x2={8}
+        y1="0"
+        y2="30"
+      />
+      <line
+        style={
+          Object {
+            "stroke": "#999",
+            "strokeWidth": 4,
+          }
+        }
+        x1={12}
+        x2={12}
+        y1="0"
+        y2="30"
+      />
+      <line
+        style={
+          Object {
+            "stroke": "#b4b4b4",
+            "strokeWidth": 4,
+          }
+        }
+        x1={16}
+        x2={16}
+        y1="0"
+        y2="30"
+      />
+      <line
+        style={
+          Object {
+            "stroke": "#999",
+            "strokeWidth": 4,
+          }
+        }
+        x1={20}
+        x2={20}
+        y1="0"
+        y2="30"
+      />
+      <line
+        style={
+          Object {
+            "stroke": "#b4b4b4",
+            "strokeWidth": 4,
+          }
+        }
+        x1={24}
+        x2={24}
+        y1="0"
+        y2="30"
+      />
+      <line
+        style={
+          Object {
+            "stroke": "#999",
+            "strokeWidth": 4,
+          }
+        }
+        x1={28}
+        x2={28}
+        y1="0"
+        y2="30"
+      />
+    </pattern>
+  </defs>
+  <g
+    transform="translate(0, 12)"
+  >
+    <rect
+      fill="url(#gradient-legend)"
+      height={8}
+      width={176}
+      x={0}
+      y={0}
+    />
+    <text
+      className="gradient-legend-text"
+      dy="-2px"
+      key="0"
+      x={0}
+      y={0}
+    >
+      0
+    </text>
+    <text
+      className="gradient-legend-text"
+      dy="-2px"
+      key="1"
+      x={44}
+      y={0}
+    >
+      25
+    </text>
+    <text
+      className="gradient-legend-text"
+      dy="-2px"
+      key="2"
+      x={88}
+      y={0}
+    >
+      50
+    </text>
+    <text
+      className="gradient-legend-text"
+      dy="-2px"
+      key="3"
+      x={132}
+      y={0}
+    >
+      75
+    </text>
+    <text
+      className="gradient-legend-text"
+      dy="-2px"
+      key="4"
+      x={176}
+      y={0}
+    >
+      100
+    </text>
+  </g>
+  <g
+    transform="translate(176, 12)"
+  >
+    <rect
+      fill="url(#stripes)"
+      height={8}
+      width={20}
+      x={4}
+      y={0}
+    />
+    <text
+      className="gradient-legend-na"
+      dy="-2px"
+      x={14}
+      y={0}
+    >
+      N/A
+    </text>
+  </g>
+</svg>
+`;
diff --git a/server/sonar-ui-common/components/charts/__tests__/__snapshots__/DonutChart-test.tsx.snap b/server/sonar-ui-common/components/charts/__tests__/__snapshots__/DonutChart-test.tsx.snap
new file mode 100644 (file)
index 0000000..f4da702
--- /dev/null
@@ -0,0 +1,122 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<svg
+  className="donut-chart"
+  height={20}
+  width={20}
+>
+  <g
+    transform="translate(0, 0)"
+  >
+    <g
+      transform="translate(10, 10)"
+    >
+      <Sector
+        data={
+          Object {
+            "data": Object {
+              "fill": "#000000",
+              "value": 25,
+            },
+            "endAngle": 1.5707963267948968,
+            "index": 0,
+            "padAngle": 0,
+            "startAngle": 0,
+            "value": 25,
+          }
+        }
+        fill="#000000"
+        key="0"
+        radius={10}
+        thickness={2}
+      />
+      <Sector
+        data={
+          Object {
+            "data": Object {
+              "fill": "#ffffff",
+              "value": 75,
+            },
+            "endAngle": 6.283185307179586,
+            "index": 1,
+            "padAngle": 0,
+            "startAngle": 1.5707963267948968,
+            "value": 75,
+          }
+        }
+        fill="#ffffff"
+        key="1"
+        radius={10}
+        thickness={2}
+      />
+    </g>
+  </g>
+</svg>
+`;
+
+exports[`should render correctly 2`] = `
+<path
+  d="M6.123233995736766e-16,-10A10,10,0,0,1,10,2.220446049250313e-15L8,1.7763568394002505e-15A8,8,0,0,0,4.898587196589413e-16,-8Z"
+  style={
+    Object {
+      "fill": "#000000",
+    }
+  }
+/>
+`;
+
+exports[`should render correctly with padding and pad angle too 1`] = `
+<svg
+  className="donut-chart"
+  height={20}
+  width={20}
+>
+  <g
+    transform="translate(2, 2)"
+  >
+    <g
+      transform="translate(8, 8)"
+    >
+      <Sector
+        data={
+          Object {
+            "data": Object {
+              "fill": "#000000",
+              "value": 25,
+            },
+            "endAngle": 1.6207963267948966,
+            "index": 0,
+            "padAngle": 0.1,
+            "startAngle": 0,
+            "value": 25,
+          }
+        }
+        fill="#000000"
+        key="0"
+        radius={8}
+        thickness={2}
+      />
+      <Sector
+        data={
+          Object {
+            "data": Object {
+              "fill": "#ffffff",
+              "value": 75,
+            },
+            "endAngle": 6.283185307179585,
+            "index": 1,
+            "padAngle": 0.1,
+            "startAngle": 1.6207963267948966,
+            "value": 75,
+          }
+        }
+        fill="#ffffff"
+        key="1"
+        radius={8}
+        thickness={2}
+      />
+    </g>
+  </g>
+</svg>
+`;
diff --git a/server/sonar-ui-common/components/charts/__tests__/__snapshots__/Histogram-test.tsx.snap b/server/sonar-ui-common/components/charts/__tests__/__snapshots__/Histogram-test.tsx.snap
new file mode 100644 (file)
index 0000000..f52a4ea
--- /dev/null
@@ -0,0 +1,352 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<svg
+  className="bar-chart"
+  height={75}
+  width={100}
+>
+  <g
+    transform="translate(10, 10)"
+  >
+    <g>
+      <g
+        key="0"
+      >
+        <rect
+          className="bar-chart-bar"
+          height={10}
+          width={54}
+          x={0}
+          y={10}
+        />
+      </g>
+      <g
+        key="1"
+      >
+        <rect
+          className="bar-chart-bar"
+          height={10}
+          width={41}
+          x={0}
+          y={28}
+        />
+      </g>
+      <g
+        key="2"
+      >
+        <rect
+          className="bar-chart-bar"
+          height={10}
+          width={81}
+          x={0}
+          y={46}
+        />
+      </g>
+    </g>
+  </g>
+</svg>
+`;
+
+exports[`renders with yValues 1`] = `
+<svg
+  className="bar-chart"
+  height={75}
+  width={100}
+>
+  <g
+    transform="translate(10, 10)"
+  >
+    <g>
+      <g
+        key="0"
+      >
+        <rect
+          className="bar-chart-bar"
+          height={10}
+          width={54}
+          x={0}
+          y={10}
+        />
+        <Tooltip>
+          <text
+            className="bar-chart-tick histogram-value"
+            dx="1em"
+            dy="0.3em"
+            x={53.33333333333333}
+            y={15}
+          >
+            100.0
+          </text>
+        </Tooltip>
+      </g>
+      <g
+        key="1"
+      >
+        <rect
+          className="bar-chart-bar"
+          height={10}
+          width={41}
+          x={0}
+          y={28}
+        />
+        <Tooltip>
+          <text
+            className="bar-chart-tick histogram-value"
+            dx="1em"
+            dy="0.3em"
+            x={40}
+            y={33}
+          >
+            75.0
+          </text>
+        </Tooltip>
+      </g>
+      <g
+        key="2"
+      >
+        <rect
+          className="bar-chart-bar"
+          height={10}
+          width={81}
+          x={0}
+          y={46}
+        />
+        <Tooltip>
+          <text
+            className="bar-chart-tick histogram-value"
+            dx="1em"
+            dy="0.3em"
+            x={80}
+            y={51}
+          >
+            150.0
+          </text>
+        </Tooltip>
+      </g>
+    </g>
+  </g>
+</svg>
+`;
+
+exports[`renders with yValues and yTicks 1`] = `
+<svg
+  className="bar-chart"
+  height={75}
+  width={100}
+>
+  <g
+    transform="translate(10, 10)"
+  >
+    <g>
+      <g
+        key="0"
+      >
+        <rect
+          className="bar-chart-bar"
+          height={10}
+          width={54}
+          x={0}
+          y={10}
+        />
+        <Tooltip>
+          <text
+            className="bar-chart-tick histogram-value"
+            dx="1em"
+            dy="0.3em"
+            x={53.33333333333333}
+            y={15}
+          >
+            100.0
+          </text>
+        </Tooltip>
+        <text
+          className="bar-chart-tick histogram-tick"
+          dx="-1em"
+          dy="0.3em"
+          x={0}
+          y={15}
+        >
+          a
+        </text>
+      </g>
+      <g
+        key="1"
+      >
+        <rect
+          className="bar-chart-bar"
+          height={10}
+          width={41}
+          x={0}
+          y={28}
+        />
+        <Tooltip>
+          <text
+            className="bar-chart-tick histogram-value"
+            dx="1em"
+            dy="0.3em"
+            x={40}
+            y={33}
+          >
+            75.0
+          </text>
+        </Tooltip>
+        <text
+          className="bar-chart-tick histogram-tick"
+          dx="-1em"
+          dy="0.3em"
+          x={0}
+          y={33}
+        >
+          b
+        </text>
+      </g>
+      <g
+        key="2"
+      >
+        <rect
+          className="bar-chart-bar"
+          height={10}
+          width={81}
+          x={0}
+          y={46}
+        />
+        <Tooltip>
+          <text
+            className="bar-chart-tick histogram-value"
+            dx="1em"
+            dy="0.3em"
+            x={80}
+            y={51}
+          >
+            150.0
+          </text>
+        </Tooltip>
+        <text
+          className="bar-chart-tick histogram-tick"
+          dx="-1em"
+          dy="0.3em"
+          x={0}
+          y={51}
+        >
+          c
+        </text>
+      </g>
+    </g>
+  </g>
+</svg>
+`;
+
+exports[`renders with yValues, yTicks and yTooltips 1`] = `
+<svg
+  className="bar-chart"
+  height={75}
+  width={100}
+>
+  <g
+    transform="translate(10, 10)"
+  >
+    <g>
+      <g
+        key="0"
+      >
+        <rect
+          className="bar-chart-bar"
+          height={10}
+          width={54}
+          x={0}
+          y={10}
+        />
+        <Tooltip
+          overlay="a - 100"
+        >
+          <text
+            className="bar-chart-tick histogram-value"
+            dx="1em"
+            dy="0.3em"
+            x={53.33333333333333}
+            y={15}
+          >
+            100.0
+          </text>
+        </Tooltip>
+        <text
+          className="bar-chart-tick histogram-tick"
+          dx="-1em"
+          dy="0.3em"
+          x={0}
+          y={15}
+        >
+          a
+        </text>
+      </g>
+      <g
+        key="1"
+      >
+        <rect
+          className="bar-chart-bar"
+          height={10}
+          width={41}
+          x={0}
+          y={28}
+        />
+        <Tooltip
+          overlay="b - 75"
+        >
+          <text
+            className="bar-chart-tick histogram-value"
+            dx="1em"
+            dy="0.3em"
+            x={40}
+            y={33}
+          >
+            75.0
+          </text>
+        </Tooltip>
+        <text
+          className="bar-chart-tick histogram-tick"
+          dx="-1em"
+          dy="0.3em"
+          x={0}
+          y={33}
+        >
+          b
+        </text>
+      </g>
+      <g
+        key="2"
+      >
+        <rect
+          className="bar-chart-bar"
+          height={10}
+          width={81}
+          x={0}
+          y={46}
+        />
+        <Tooltip
+          overlay="c - 150"
+        >
+          <text
+            className="bar-chart-tick histogram-value"
+            dx="1em"
+            dy="0.3em"
+            x={80}
+            y={51}
+          >
+            150.0
+          </text>
+        </Tooltip>
+        <text
+          className="bar-chart-tick histogram-tick"
+          dx="-1em"
+          dy="0.3em"
+          x={0}
+          y={51}
+        >
+          c
+        </text>
+      </g>
+    </g>
+  </g>
+</svg>
+`;
diff --git a/server/sonar-ui-common/components/controls/ActionsDropdown.tsx b/server/sonar-ui-common/components/controls/ActionsDropdown.tsx
new file mode 100644 (file)
index 0000000..06f01d6
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import { LocationDescriptor } from 'history';
+import * as React from 'react';
+import { Link } from 'react-router';
+import { translate } from '../../helpers/l10n';
+import DropdownIcon from '../icons/DropdownIcon';
+import SettingsIcon from '../icons/SettingsIcon';
+import { PopupPlacement } from '../ui/popups';
+import { Button } from './buttons';
+import { ClipboardBase } from './clipboard';
+import Dropdown from './Dropdown';
+import Tooltip from './Tooltip';
+
+export interface ActionsDropdownProps {
+  className?: string;
+  children: React.ReactNode;
+  onOpen?: () => void;
+  overlayPlacement?: PopupPlacement;
+  small?: boolean;
+  toggleClassName?: string;
+}
+
+export default function ActionsDropdown(props: ActionsDropdownProps) {
+  const { children, className, overlayPlacement, small, toggleClassName } = props;
+  return (
+    <Dropdown
+      className={className}
+      onOpen={props.onOpen}
+      overlay={<ul className="menu">{children}</ul>}
+      overlayPlacement={overlayPlacement}>
+      <Button
+        className={classNames('dropdown-toggle', toggleClassName, {
+          'button-small': small,
+        })}>
+        <SettingsIcon size={small ? 12 : 14} />
+        <DropdownIcon className="little-spacer-left" />
+      </Button>
+    </Dropdown>
+  );
+}
+
+interface ItemProps {
+  className?: string;
+  children: React.ReactNode;
+  /** used to pass a string to copy to clipboard */
+  copyValue?: string;
+  destructive?: boolean;
+  /** used to pass a name of downloaded file */
+  download?: string;
+  id?: string;
+  onClick?: () => void;
+  to?: LocationDescriptor;
+}
+
+export class ActionsDropdownItem extends React.PureComponent<ItemProps> {
+  handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    if (this.props.onClick) {
+      this.props.onClick();
+    }
+  };
+
+  render() {
+    const className = classNames(this.props.className, { 'text-danger': this.props.destructive });
+
+    if (this.props.download && typeof this.props.to === 'string') {
+      return (
+        <li>
+          <a
+            className={className}
+            download={this.props.download}
+            href={this.props.to}
+            id={this.props.id}>
+            {this.props.children}
+          </a>
+        </li>
+      );
+    }
+
+    if (this.props.to) {
+      return (
+        <li>
+          <Link className={className} id={this.props.id} to={this.props.to}>
+            {this.props.children}
+          </Link>
+        </li>
+      );
+    }
+
+    if (this.props.copyValue) {
+      return (
+        <ClipboardBase>
+          {({ setCopyButton, copySuccess }) => (
+            <Tooltip overlay={translate('copied_action')} visible={copySuccess}>
+              <li data-clipboard-text={this.props.copyValue} ref={setCopyButton}>
+                <a className={className} href="#" id={this.props.id} onClick={this.handleClick}>
+                  {this.props.children}
+                </a>
+              </li>
+            </Tooltip>
+          )}
+        </ClipboardBase>
+      );
+    }
+
+    return (
+      <li>
+        <a className={className} href="#" id={this.props.id} onClick={this.handleClick}>
+          {this.props.children}
+        </a>
+      </li>
+    );
+  }
+}
+
+export function ActionsDropdownDivider() {
+  return <li className="divider" />;
+}
diff --git a/server/sonar-ui-common/components/controls/BackButton.tsx b/server/sonar-ui-common/components/controls/BackButton.tsx
new file mode 100644 (file)
index 0000000..3c747be
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import { ThemeConsumer } from '../theme';
+import Tooltip from './Tooltip';
+
+interface Props {
+  className?: string;
+  disabled?: boolean;
+  onClick: () => void;
+  tooltip?: string;
+}
+
+export default class BackButton extends React.PureComponent<Props> {
+  handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    if (!this.props.disabled) {
+      this.props.onClick();
+    }
+  };
+
+  renderIcon = () => (
+    <ThemeConsumer>
+      {(theme) => (
+        <svg height="24" viewBox="0 0 21 24" width="21">
+          <path
+            d="M3.845 12.9992l5.993 5.993.052.056c.049.061.093.122.129.191.082.159.121.339.111.518-.006.102-.028.203-.064.298-.149.39-.537.652-.954.644-.102-.002-.204-.019-.301-.052-.148-.05-.273-.135-.387-.241l-8.407-8.407 8.407-8.407.056-.052c.061-.048.121-.092.19-.128.116-.06.237-.091.366-.108.076-.004.075-.004.153-.003.155.015.3.052.437.129.088.051.169.115.239.19.246.266.33.656.214.999-.051.149-.135.273-.241.387l-5.983 5.984c5.287-.044 10.577-.206 15.859.013.073.009.091.009.163.027.187.047.359.15.49.292.075.081.136.175.18.276.044.101.072.209.081.319.032.391-.175.775-.521.962-.097.052-.202.089-.311.107-.073.012-.091.01-.165.013H3.845z"
+            fill={this.props.disabled ? theme.colors.disableGrayText : theme.colors.secondFontColor}
+          />
+        </svg>
+      )}
+    </ThemeConsumer>
+  );
+
+  render() {
+    const { tooltip = translate('issues.return_to_list') } = this.props;
+    return (
+      <Tooltip overlay={tooltip}>
+        <a
+          className={classNames(
+            'link-no-underline',
+            { 'cursor-not-allowed': this.props.disabled },
+            this.props.className
+          )}
+          href="#"
+          onClick={this.handleClick}>
+          {this.renderIcon()}
+        </a>
+      </Tooltip>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/BoxedGroupAccordion.tsx b/server/sonar-ui-common/components/controls/BoxedGroupAccordion.tsx
new file mode 100644 (file)
index 0000000..5677b22
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import OpenCloseIcon from '../icons/OpenCloseIcon';
+
+interface Props {
+  children: React.ReactNode;
+  className?: string;
+  data?: string;
+  onClick: (data?: string) => void;
+  open: boolean;
+  renderHeader?: () => React.ReactNode;
+  title: React.ReactNode;
+}
+
+interface State {
+  hoveringInner: boolean;
+}
+
+export default class BoxedGroupAccordion extends React.PureComponent<Props, State> {
+  state: State = { hoveringInner: false };
+
+  handleClick = () => {
+    this.props.onClick(this.props.data);
+  };
+
+  onDetailEnter = () => {
+    this.setState({ hoveringInner: true });
+  };
+
+  onDetailLeave = () => {
+    this.setState({ hoveringInner: false });
+  };
+
+  render() {
+    const { className, open, renderHeader, title } = this.props;
+    return (
+      <div
+        className={classNames('boxed-group boxed-group-accordion', className, {
+          'no-hover': this.state.hoveringInner,
+        })}>
+        <div className="boxed-group-header" onClick={this.handleClick} role="listitem">
+          <span className="boxed-group-accordion-title">
+            <OpenCloseIcon className="little-spacer-right" open={open} />
+            {title}
+          </span>
+          {renderHeader && renderHeader()}
+        </div>
+        {open && (
+          <div
+            className="boxed-group-inner"
+            onMouseEnter={this.onDetailEnter}
+            onMouseLeave={this.onDetailLeave}>
+            {this.props.children}
+          </div>
+        )}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/BoxedTabs.tsx b/server/sonar-ui-common/components/controls/BoxedTabs.tsx
new file mode 100644 (file)
index 0000000..043c62d
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { styled, themeColor, ThemedProps, themeSize } from '../theme';
+
+export interface BoxedTabsProps<K> {
+  className?: string;
+  onSelect: (key: K) => void;
+  selected?: K;
+  tabs: Array<{ key: K; label: React.ReactNode }>;
+}
+
+const TabContainer = styled.div`
+  display: flex;
+  flex-direction: row;
+`;
+
+const baseBorder = ({ theme }: ThemedProps) => `1px solid ${theme.colors.barBorderColor}`;
+
+const highlightHoverMixin = ({ theme }: ThemedProps) => `
+  &:hover {
+    background-color: ${theme.colors.barBackgroundColorHighlight};
+  }
+`;
+
+const StyledTab = styled.button<{ active: boolean }>`
+  position: relative;
+  background-color: ${(props) => (props.active ? 'white' : props.theme.colors.barBackgroundColor)};
+  border-top: ${baseBorder};
+  border-left: ${baseBorder};
+  border-right: none;
+  border-bottom: none;
+  margin-bottom: -1px;
+  min-width: 128px;
+  min-height: 56px;
+  ${(props) => !props.active && 'cursor: pointer;'}
+  outline: 0;
+  padding: calc(2 * ${themeSize('gridSize')});
+
+  ${(props) => (!props.active ? highlightHoverMixin : null)}
+
+  &:last-child {
+    border-right: ${baseBorder};
+  }
+`;
+
+const ActiveBorder = styled.div<{ active: boolean }>`
+  display: ${(props) => (props.active ? 'block' : 'none')};
+  background-color: ${themeColor('blue')};
+  height: 3px;
+  width: 100%;
+  position: absolute;
+  left: 0;
+  top: -1px;
+`;
+
+export default function BoxedTabs<K>(props: BoxedTabsProps<K>) {
+  const { className, tabs, selected } = props;
+
+  return (
+    <TabContainer className={className}>
+      {tabs.map(({ key, label }, i) => (
+        <StyledTab
+          active={selected === key}
+          key={i}
+          onClick={() => selected !== key && props.onSelect(key)}
+          type="button">
+          <ActiveBorder active={selected === key} />
+          {label}
+        </StyledTab>
+      ))}
+    </TabContainer>
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/Checkbox.css b/server/sonar-ui-common/components/controls/Checkbox.css
new file mode 100644 (file)
index 0000000..ab709bb
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+.icon-checkbox {
+  display: inline-block;
+  line-height: 1;
+  vertical-align: top;
+  padding: 1px 2px;
+  box-sizing: border-box;
+}
+
+a.icon-checkbox {
+  border-bottom: none;
+}
+
+.icon-checkbox:focus {
+  outline: none;
+}
+
+.icon-checkbox:before {
+  content: ' ';
+  display: inline-block;
+  width: 10px;
+  height: 10px;
+  border: 1px solid var(--darkBlue);
+  border-radius: 2px;
+  transition: border-color 0.2s ease, background-color 0.2s ease, background-image 0.2s ease,
+    box-shadow 0.4s ease;
+}
+
+.icon-checkbox:not(.icon-checkbox-disabled):focus:before,
+.link-checkbox:not(.disabled):focus:focus .icon-checkbox:before {
+  box-shadow: 0 0 0 3px rgba(35, 106, 151, 0.25);
+}
+
+.icon-checkbox-checked:before {
+  background-color: var(--blue);
+  background-image: url('data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2014%2014%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20stroke-linejoin%3D%22round%22%20stroke-miterlimit%3D%221.414%22%3E%3Cpath%20d%3D%22M12%204.665c0%20.172-.06.318-.18.438l-5.55%205.55c-.12.12-.266.18-.438.18s-.318-.06-.438-.18L2.18%207.438C2.06%207.317%202%207.17%202%207s.06-.318.18-.44l.878-.876c.12-.12.267-.18.44-.18.17%200%20.317.06.437.18l1.897%201.903%204.233-4.24c.12-.12.266-.18.438-.18s.32.06.44.18l.876.88c.12.12.18.265.18.438z%22%20fill%3D%22%23fff%22%20fill-rule%3D%22nonzero%22%2F%3E%3C%2Fsvg%3E');
+  border-color: var(--blue);
+}
+
+.icon-checkbox-checked.icon-checkbox-single:before {
+  background-image: url('data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2014%2014%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20stroke-linejoin%3D%22round%22%20stroke-miterlimit%3D%221.414%22%3E%3Cpath%20d%3D%22M10%204.698C10%204.312%209.688%204%209.302%204H4.698C4.312%204%204%204.312%204%204.698v4.604c0%20.386.312.698.698.698h4.604c.386%200%20.698-.312.698-.698V4.698z%22%20fill%3D%22%23fff%22%2F%3E%3C%2Fsvg%3E');
+}
+
+.icon-checkbox-disabled:before {
+  border: 1px solid var(--disableGrayText);
+  cursor: not-allowed;
+}
+
+.icon-checkbox-disabled.icon-checkbox-checked:before {
+  background-color: var(--disableGrayText);
+}
+
+.icon-checkbox-invisible {
+  visibility: hidden;
+}
+
+.link-checkbox {
+  color: inherit;
+  border-bottom: none;
+}
+
+.link-checkbox.disabled,
+.link-checkbox.disabled:hover,
+.link-checkbox.disabled label {
+  color: var(--secondFontColor);
+  cursor: not-allowed;
+}
+
+.link-checkbox:hover,
+.link-checkbox:active,
+.link-checkbox:focus {
+  color: inherit;
+}
diff --git a/server/sonar-ui-common/components/controls/Checkbox.tsx b/server/sonar-ui-common/components/controls/Checkbox.tsx
new file mode 100644 (file)
index 0000000..307dc1c
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import DeferredSpinner from '../ui/DeferredSpinner';
+import './Checkbox.css';
+
+interface Props {
+  checked: boolean;
+  disabled?: boolean;
+  children?: React.ReactNode;
+  className?: string;
+  id?: string;
+  loading?: boolean;
+  onCheck: (checked: boolean, id?: string) => void;
+  right?: boolean;
+  thirdState?: boolean;
+  title?: string;
+}
+
+export default class Checkbox extends React.PureComponent<Props> {
+  static defaultProps = {
+    thirdState: false,
+  };
+
+  handleClick = (event: React.SyntheticEvent<HTMLElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    if (!this.props.disabled) {
+      this.props.onCheck(!this.props.checked, this.props.id);
+    }
+  };
+
+  render() {
+    const { checked, children, disabled, id, loading, right, thirdState, title } = this.props;
+    const className = classNames('icon-checkbox', {
+      'icon-checkbox-checked': checked,
+      'icon-checkbox-single': thirdState,
+      'icon-checkbox-disabled': disabled,
+    });
+
+    if (children) {
+      return (
+        <a
+          aria-checked={checked}
+          className={classNames('link-checkbox', this.props.className, {
+            note: disabled,
+            disabled,
+          })}
+          href="#"
+          id={id}
+          onClick={this.handleClick}
+          role="checkbox"
+          title={title}>
+          {right && children}
+          <DeferredSpinner loading={Boolean(loading)}>
+            <i className={className} />
+          </DeferredSpinner>
+          {!right && children}
+        </a>
+      );
+    }
+
+    if (loading) {
+      return <DeferredSpinner />;
+    }
+
+    return (
+      <a
+        aria-checked={checked}
+        className={classNames(className, this.props.className)}
+        href="#"
+        id={id}
+        onClick={this.handleClick}
+        role="checkbox"
+        title={title}
+      />
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/ClickEventBoundary.tsx b/server/sonar-ui-common/components/controls/ClickEventBoundary.tsx
new file mode 100644 (file)
index 0000000..96a25e4
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+
+export interface ClickEventBoundaryProps {
+  children: React.ReactElement;
+}
+
+export default function ClickEventBoundary({ children }: ClickEventBoundaryProps) {
+  return React.cloneElement(children, {
+    onClick: (e: React.SyntheticEvent<MouseEvent>) => {
+      e.stopPropagation();
+      if (typeof children.props.onClick === 'function') {
+        children.props.onClick(e);
+      }
+    },
+  });
+}
diff --git a/server/sonar-ui-common/components/controls/ConfirmButton.tsx b/server/sonar-ui-common/components/controls/ConfirmButton.tsx
new file mode 100644 (file)
index 0000000..737b18e
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import ConfirmModal, { ConfirmModalProps } from './ConfirmModal';
+import ModalButton, { ChildrenProps, ModalProps } from './ModalButton';
+
+interface Props<T> extends ConfirmModalProps<T> {
+  children: (props: ChildrenProps) => React.ReactNode;
+  modalBody: React.ReactNode;
+  modalHeader: string;
+  modalHeaderDescription?: React.ReactNode;
+}
+
+interface State {
+  modal: boolean;
+}
+
+export default class ConfirmButton<T> extends React.PureComponent<Props<T>, State> {
+  renderConfirmModal = ({ onClose }: ModalProps) => {
+    const {
+      children,
+      modalBody,
+      modalHeader,
+      modalHeaderDescription,
+      ...confirmModalProps
+    } = this.props;
+    return (
+      <ConfirmModal
+        header={modalHeader}
+        headerDescription={modalHeaderDescription}
+        onClose={onClose}
+        {...confirmModalProps}>
+        {modalBody}
+      </ConfirmModal>
+    );
+  };
+
+  render() {
+    return <ModalButton modal={this.renderConfirmModal}>{this.props.children}</ModalButton>;
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/ConfirmModal.tsx b/server/sonar-ui-common/components/controls/ConfirmModal.tsx
new file mode 100644 (file)
index 0000000..295d4f8
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import DeferredSpinner from '../ui/DeferredSpinner';
+import { ResetButtonLink, SubmitButton } from './buttons';
+import ClickEventBoundary from './ClickEventBoundary';
+import { ModalProps } from './Modal';
+import SimpleModal, { ChildrenProps } from './SimpleModal';
+
+export interface ConfirmModalProps<T> extends ModalProps {
+  cancelButtonText?: string;
+  confirmButtonText: string;
+  confirmData?: T;
+  confirmDisable?: boolean;
+  isDestructive?: boolean;
+  onConfirm: (data?: T) => void | Promise<void | Response>;
+}
+
+interface Props<T> extends ConfirmModalProps<T> {
+  header: string;
+  headerDescription?: React.ReactNode;
+  onClose: () => void;
+}
+
+export default class ConfirmModal<T = string> extends React.PureComponent<Props<T>> {
+  mounted = false;
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  handleSubmit = () => {
+    const result = this.props.onConfirm(this.props.confirmData);
+    if (result) {
+      return result.then(this.props.onClose, () => {});
+    } else {
+      this.props.onClose();
+      return undefined;
+    }
+  };
+
+  renderModalContent = ({ onCloseClick, onFormSubmit, submitting }: ChildrenProps) => {
+    const {
+      children,
+      confirmButtonText,
+      confirmDisable,
+      header,
+      headerDescription,
+      isDestructive,
+      cancelButtonText = translate('cancel'),
+    } = this.props;
+    return (
+      <ClickEventBoundary>
+        <form onSubmit={onFormSubmit}>
+          <header className="modal-head">
+            <h2>{header}</h2>
+            {headerDescription}
+          </header>
+          <div className="modal-body">{children}</div>
+          <footer className="modal-foot">
+            <DeferredSpinner className="spacer-right" loading={submitting} />
+            <SubmitButton
+              autoFocus={true}
+              className={isDestructive ? 'button-red' : undefined}
+              disabled={submitting || confirmDisable}>
+              {confirmButtonText}
+            </SubmitButton>
+            <ResetButtonLink disabled={submitting} onClick={onCloseClick}>
+              {cancelButtonText}
+            </ResetButtonLink>
+          </footer>
+        </form>
+      </ClickEventBoundary>
+    );
+  };
+
+  render() {
+    const { header, onClose, noBackdrop, size } = this.props;
+    const modalProps = { header, onClose, noBackdrop, size };
+    return (
+      <SimpleModal onSubmit={this.handleSubmit} {...modalProps}>
+        {this.renderModalContent}
+      </SimpleModal>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/DocumentClickHandler.tsx b/server/sonar-ui-common/components/controls/DocumentClickHandler.tsx
new file mode 100644 (file)
index 0000000..a6bb044
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+
+interface Props {
+  children: React.ReactNode;
+  onClick: () => void;
+}
+
+export default class DocumentClickHandler extends React.Component<Props> {
+  componentDidMount() {
+    setTimeout(() => {
+      this.addClickHandler();
+    }, 0);
+  }
+
+  componentWillUnmount() {
+    this.removeClickHandler();
+  }
+
+  addClickHandler = () => {
+    document.addEventListener('click', this.handleDocumentClick);
+  };
+
+  removeClickHandler = () => {
+    document.removeEventListener('click', this.handleDocumentClick);
+  };
+
+  handleDocumentClick = () => {
+    this.props.onClick();
+  };
+
+  render() {
+    return this.props.children;
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/Dropdown.css b/server/sonar-ui-common/components/controls/Dropdown.css
new file mode 100644 (file)
index 0000000..783ed43
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.dropdown {
+  position: relative;
+  display: inline-block;
+  vertical-align: middle;
+}
+
+.dropdown-bottom-hint {
+  line-height: 16px;
+  margin-bottom: -5px;
+  padding: 5px 10px;
+  border-top: 1px solid var(--barBorderColor);
+  background-color: var(--barBackgroundColor);
+  color: var(--secondFontColor);
+  font-size: 11px;
+}
diff --git a/server/sonar-ui-common/components/controls/Dropdown.tsx b/server/sonar-ui-common/components/controls/Dropdown.tsx
new file mode 100644 (file)
index 0000000..a39be19
--- /dev/null
@@ -0,0 +1,160 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import { Popup, PopupPlacement } from '../ui/popups';
+import './Dropdown.css';
+import ScreenPositionFixer from './ScreenPositionFixer';
+import Toggler from './Toggler';
+
+interface OnClickCallback {
+  (event?: React.SyntheticEvent<HTMLElement>): void;
+}
+
+interface RenderProps {
+  closeDropdown: () => void;
+  onToggleClick: OnClickCallback;
+  open: boolean;
+}
+
+interface Props {
+  children:
+    | ((renderProps: RenderProps) => JSX.Element)
+    | React.ReactElement<{ onClick: OnClickCallback }>;
+  className?: string;
+  closeOnClick?: boolean;
+  closeOnClickOutside?: boolean;
+  onOpen?: () => void;
+  overlay: React.ReactNode;
+  overlayPlacement?: PopupPlacement;
+  noOverlayPadding?: boolean;
+  tagName?: string;
+}
+
+interface State {
+  open: boolean;
+}
+
+export default class Dropdown extends React.PureComponent<Props, State> {
+  state: State = { open: false };
+
+  componentDidUpdate(_: Props, prevState: State) {
+    if (!prevState.open && this.state.open && this.props.onOpen) {
+      this.props.onOpen();
+    }
+  }
+
+  closeDropdown = () => this.setState({ open: false });
+
+  handleToggleClick = (event?: React.SyntheticEvent<HTMLElement>) => {
+    if (event) {
+      event.preventDefault();
+      event.currentTarget.blur();
+    }
+    this.setState((state) => ({ open: !state.open }));
+  };
+
+  render() {
+    const a11yAttrs = {
+      'aria-expanded': String(this.state.open),
+      'aria-haspopup': 'true',
+    };
+
+    const child = React.isValidElement(this.props.children)
+      ? React.cloneElement(this.props.children, { onClick: this.handleToggleClick, ...a11yAttrs })
+      : this.props.children({
+          closeDropdown: this.closeDropdown,
+          onToggleClick: this.handleToggleClick,
+          open: this.state.open,
+        });
+
+    const { closeOnClick = true, closeOnClickOutside = false } = this.props;
+
+    const toggler = (
+      <Toggler
+        closeOnClick={closeOnClick}
+        closeOnClickOutside={closeOnClickOutside}
+        onRequestClose={this.closeDropdown}
+        open={this.state.open}
+        overlay={
+          <DropdownOverlay
+            noPadding={this.props.noOverlayPadding}
+            placement={this.props.overlayPlacement}>
+            {this.props.overlay}
+          </DropdownOverlay>
+        }>
+        {child}
+      </Toggler>
+    );
+
+    return React.createElement(
+      this.props.tagName || 'div',
+      { className: classNames('dropdown', this.props.className) },
+      toggler
+    );
+  }
+}
+
+interface OverlayProps {
+  className?: string;
+  children: React.ReactNode;
+  noPadding?: boolean;
+  placement?: PopupPlacement;
+}
+
+// TODO use the same styling for <Select />
+// TODO use the same styling for <DateInput />
+
+export class DropdownOverlay extends React.Component<OverlayProps> {
+  get placement() {
+    return this.props.placement || PopupPlacement.Bottom;
+  }
+
+  renderPopup = (leftFix?: number, topFix?: number) => (
+    <Popup
+      arrowStyle={
+        leftFix !== undefined && topFix !== undefined
+          ? { transform: `translate(${-leftFix}px, ${-topFix}px)` }
+          : undefined
+      }
+      className={this.props.className}
+      noPadding={this.props.noPadding}
+      placement={this.placement}
+      style={
+        leftFix !== undefined && topFix !== undefined
+          ? { marginLeft: `calc(50% + ${leftFix}px)` }
+          : undefined
+      }>
+      {this.props.children}
+    </Popup>
+  );
+
+  render() {
+    if (this.placement === PopupPlacement.Bottom) {
+      return (
+        <ScreenPositionFixer>
+          {({ leftFix, topFix }) => this.renderPopup(leftFix, topFix)}
+        </ScreenPositionFixer>
+      );
+    } else {
+      return this.renderPopup();
+    }
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/EscKeydownHandler.tsx b/server/sonar-ui-common/components/controls/EscKeydownHandler.tsx
new file mode 100644 (file)
index 0000000..b8c324c
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { KeyCodes } from '../../helpers/keycodes';
+
+interface Props {
+  children: React.ReactNode;
+  onKeydown: () => void;
+}
+
+export default class EscKeydownHandler extends React.Component<Props> {
+  componentDidMount() {
+    setTimeout(() => {
+      document.addEventListener('keydown', this.handleKeyDown, false);
+    }, 0);
+  }
+
+  componentWillUnmount() {
+    document.removeEventListener('keydown', this.handleKeyDown, false);
+  }
+
+  handleKeyDown = (event: KeyboardEvent) => {
+    if (event.keyCode === KeyCodes.Escape) {
+      this.props.onKeydown();
+    }
+  };
+
+  render() {
+    return this.props.children;
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/FavoriteButton.tsx b/server/sonar-ui-common/components/controls/FavoriteButton.tsx
new file mode 100644 (file)
index 0000000..1b6dbf2
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import FavoriteIcon from '../icons/FavoriteIcon';
+import { ButtonLink } from './buttons';
+import Tooltip from './Tooltip';
+
+export interface Props {
+  className?: string;
+  favorite: boolean;
+  qualifier: string;
+  toggleFavorite: () => void;
+}
+
+export default class FavoriteButton extends React.PureComponent<Props> {
+  render() {
+    const { className, favorite, qualifier, toggleFavorite } = this.props;
+    const tooltip = favorite
+      ? translate('favorite.current', qualifier)
+      : translate('favorite.check', qualifier);
+    const ariaLabel = translate('favorite.action', favorite ? 'remove' : 'add');
+
+    return (
+      <Tooltip overlay={tooltip}>
+        <ButtonLink
+          aria-label={ariaLabel}
+          className={classNames('favorite-link', 'link-no-underline', className)}
+          onClick={toggleFavorite}>
+          <FavoriteIcon favorite={favorite} />
+        </ButtonLink>
+      </Tooltip>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/GlobalMessages.tsx b/server/sonar-ui-common/components/controls/GlobalMessages.tsx
new file mode 100644 (file)
index 0000000..7c51ae5
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { keyframes } from '@emotion/core';
+import * as React from 'react';
+import { cutLongWords } from '../../helpers/path';
+import { styled, themeGet, themeSize } from '../theme';
+import { ClearButton } from './buttons';
+
+interface Message {
+  id: string;
+  level: 'ERROR' | 'SUCCESS';
+  message: string;
+}
+
+export interface GlobalMessagesProps {
+  closeGlobalMessage: (id: string) => void;
+  messages: Message[];
+}
+
+export default function GlobalMessages({ closeGlobalMessage, messages }: GlobalMessagesProps) {
+  if (messages.length === 0) {
+    return null;
+  }
+
+  return (
+    <MessagesContainer>
+      {messages.map((message) => (
+        <GlobalMessage closeGlobalMessage={closeGlobalMessage} key={message.id} message={message} />
+      ))}
+    </MessagesContainer>
+  );
+}
+
+const MessagesContainer = styled.div`
+  position: fixed;
+  z-index: ${themeGet('zIndexes', 'processContainerZIndex')};
+  top: 0;
+  left: 50%;
+  width: 350px;
+  margin-left: -175px;
+`;
+
+export class GlobalMessage extends React.PureComponent<{
+  closeGlobalMessage: (id: string) => void;
+  message: Message;
+}> {
+  handleClose = () => {
+    this.props.closeGlobalMessage(this.props.message.id);
+  };
+
+  render() {
+    const { message } = this.props;
+    return (
+      <Message
+        data-test={`global-message__${message.level}`}
+        level={message.level}
+        role={message.level === 'SUCCESS' ? 'status' : 'alert'}>
+        {cutLongWords(message.message)}
+        <CloseButton
+          className="button-small"
+          color="#fff"
+          level={message.level}
+          onClick={this.handleClose}
+        />
+      </Message>
+    );
+  }
+}
+
+const appearAnim = keyframes`
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+`;
+
+const Message = styled.div<Pick<Message, 'level'>>`
+  position: relative;
+  padding: 0 30px 0 10px;
+  line-height: ${themeSize('controlHeight')};
+  border-radius: 0 0 3px 3px;
+  box-sizing: border-box;
+  color: #ffffff;
+  background-color: ${({ level, theme }) =>
+    level === 'SUCCESS' ? theme.colors.green : theme.colors.red};
+  text-align: center;
+  opacity: 0;
+  animation: ${appearAnim} 0.2s ease forwards;
+
+  & + & {
+    margin-top: calc(${themeSize('gridSize')} / 2);
+    border-radius: 3px;
+  }
+`;
+
+const CloseButton = styled(ClearButton)<Pick<Message, 'level'>>`
+  position: absolute;
+  top: calc(${themeSize('gridSize')} / 4);
+  right: calc(${themeSize('gridSize')} / 4);
+
+  &:hover svg,
+  &:focus svg {
+    color: ${({ level, theme }) => (level === 'SUCCESS' ? theme.colors.green : theme.colors.red)};
+  }
+`;
diff --git a/server/sonar-ui-common/components/controls/HelpTooltip.css b/server/sonar-ui-common/components/controls/HelpTooltip.css
new file mode 100644 (file)
index 0000000..bafc621
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.help-tooltip {
+  display: inline-flex;
+  align-items: center;
+  vertical-align: middle;
+}
+
+.help-toolip-link {
+  display: block;
+  width: 12px;
+  height: 12px;
+  border: none;
+}
diff --git a/server/sonar-ui-common/components/controls/HelpTooltip.tsx b/server/sonar-ui-common/components/controls/HelpTooltip.tsx
new file mode 100644 (file)
index 0000000..edf4ec6
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import HelpIcon from '../icons/HelpIcon';
+import { IconProps } from '../icons/Icon';
+import { ThemeConsumer } from '../theme';
+import './HelpTooltip.css';
+import Tooltip, { Placement } from './Tooltip';
+
+interface Props extends Pick<IconProps, 'size'> {
+  className?: string;
+  children?: React.ReactNode;
+  onShow?: () => void;
+  overlay: React.ReactNode;
+  placement?: Placement;
+}
+
+export default function HelpTooltip({ size = 12, ...props }: Props) {
+  return (
+    <div className={classNames('help-tooltip', props.className)}>
+      <Tooltip
+        mouseLeaveDelay={0.25}
+        onShow={props.onShow}
+        overlay={props.overlay}
+        placement={props.placement}>
+        <span className="display-inline-flex-center">
+          {props.children || (
+            <ThemeConsumer>
+              {(theme) => <HelpIcon fill={theme.colors.gray71} size={size} />}
+            </ThemeConsumer>
+          )}
+        </span>
+      </Tooltip>
+    </div>
+  );
+}
+
+export function DarkHelpTooltip({ size = 12, ...props }: Omit<Props, 'children'>) {
+  return (
+    <HelpTooltip {...props}>
+      <ThemeConsumer>
+        {({ colors }) => (
+          <HelpIcon fill={colors.transparentBlack} fillInner={colors.white} size={size} />
+        )}
+      </ThemeConsumer>
+    </HelpTooltip>
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/IdentityProviderLink.css b/server/sonar-ui-common/components/controls/IdentityProviderLink.css
new file mode 100644 (file)
index 0000000..6aff2f3
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+a.identity-provider-link {
+  display: block;
+  width: auto;
+  line-height: 22px;
+  padding: var(--gridSize) calc(1.5 * var(--gridSize));
+  border: 1px solid rgba(0, 0, 0, 0.15);
+  border-radius: 2px;
+  box-sizing: border-box;
+  background-color: var(--darkBlue);
+  color: #fff;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+a.identity-provider-link.small {
+  line-height: 14px;
+  padding: calc(var(--gridSize) / 2) var(--gridSize);
+}
+
+a.identity-provider-link:hover,
+a.identity-provider-link:focus {
+  box-shadow: inset 0 0 0 100px rgba(255, 255, 255, 0.1);
+}
+
+a.identity-provider-link.dark-text {
+  color: var(--secondFontColor);
+}
+
+a.identity-provider-link.dark-text:hover,
+a.identity-provider-link.dark-text:focus {
+  box-shadow: inset 0 0 0 100px rgba(0, 0, 0, 0.1);
+}
+
+a.identity-provider-link > img {
+  padding-right: calc(1.5 * var(--gridSize));
+}
+
+a.identity-provider-link.small > img {
+  padding-right: var(--gridSize);
+}
+
+a.identity-provider-link > span::before {
+  content: '';
+  opacity: 0.4;
+  border-left: 1px var(--gray71) solid;
+  margin-right: calc(1.5 * var(--gridSize));
+}
diff --git a/server/sonar-ui-common/components/controls/IdentityProviderLink.tsx b/server/sonar-ui-common/components/controls/IdentityProviderLink.tsx
new file mode 100644 (file)
index 0000000..bfcb25f
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import { isDarkColor } from '../../helpers/colors';
+import { getBaseUrl } from '../../helpers/urls';
+import './IdentityProviderLink.css';
+
+interface Props {
+  backgroundColor: string;
+  children: React.ReactNode;
+  className?: string;
+  iconPath: string;
+  name: string;
+  onClick?: () => void;
+  small?: boolean;
+  url: string | undefined;
+}
+
+export default function IdentityProviderLink({
+  backgroundColor,
+  children,
+  className,
+  iconPath,
+  name,
+  onClick,
+  small,
+  url,
+}: Props) {
+  const size = small ? 14 : 20;
+
+  return (
+    <a
+      className={classNames(
+        'identity-provider-link',
+        { 'dark-text': !isDarkColor(backgroundColor), small },
+        className
+      )}
+      href={url}
+      onClick={onClick}
+      style={{ backgroundColor }}>
+      <img alt={name} height={size} src={getBaseUrl() + iconPath} width={size} />
+      {children}
+    </a>
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/InputValidationField.tsx b/server/sonar-ui-common/components/controls/InputValidationField.tsx
new file mode 100644 (file)
index 0000000..e02bace
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import ModalValidationField from './ModalValidationField';
+
+interface Props {
+  autoFocus?: boolean;
+  className?: string;
+  description?: string;
+  dirty: boolean;
+  disabled: boolean;
+  error: string | undefined;
+  id?: string;
+  label?: React.ReactNode;
+  name: string;
+  onBlur: (event: React.FocusEvent<any>) => void;
+  onChange: (event: React.ChangeEvent<any>) => void;
+  placeholder?: string;
+  touched: boolean | undefined;
+  type?: string;
+  value: string;
+}
+
+export default function InputValidationField({ className, ...props }: Props) {
+  const { description, dirty, error, label, touched, ...inputProps } = props;
+  const modalValidationProps = { description, dirty, error, label, touched };
+  return (
+    <ModalValidationField {...modalValidationProps}>
+      {({ className: validationClassName }) => (
+        <input className={classNames(className, validationClassName)} {...inputProps} />
+      )}
+    </ModalValidationField>
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/ListFooter.tsx b/server/sonar-ui-common/components/controls/ListFooter.tsx
new file mode 100644 (file)
index 0000000..0438791
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { formatMeasure } from '../../helpers/measures';
+import DeferredSpinner from '../ui/DeferredSpinner';
+import { Button } from './buttons';
+
+export interface ListFooterProps {
+  count: number;
+  className?: string;
+  loading?: boolean;
+  loadMore?: () => void;
+  needReload?: boolean;
+  reload?: () => void;
+  ready?: boolean;
+  total?: number;
+}
+
+export default function ListFooter(props: ListFooterProps) {
+  const { className, count, loading, needReload, total, ready = true } = props;
+  const hasMore = total && total > count;
+
+  let button;
+  if (needReload && props.reload) {
+    button = (
+      <Button className="spacer-left" data-test="reload" disabled={loading} onClick={props.reload}>
+        {translate('reload')}
+      </Button>
+    );
+  } else if (hasMore && props.loadMore) {
+    button = (
+      <Button
+        className="spacer-left"
+        disabled={loading}
+        data-test="show-more"
+        onClick={props.loadMore}>
+        {translate('show_more')}
+      </Button>
+    );
+  }
+
+  return (
+    <footer
+      className={classNames('spacer-top note text-center', { 'new-loading': !ready }, className)}>
+      {translateWithParameters(
+        'x_of_y_shown',
+        formatMeasure(count, 'INT', null),
+        formatMeasure(total, 'INT', null)
+      )}
+      {button}
+      {loading && <DeferredSpinner className="text-bottom spacer-left position-absolute" />}
+    </footer>
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/Modal.css b/server/sonar-ui-common/components/controls/Modal.css
new file mode 100644 (file)
index 0000000..a225712
--- /dev/null
@@ -0,0 +1,211 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.modal,
+.ReactModal__Content {
+  position: fixed;
+  z-index: var(--modalZIndex);
+  top: 0;
+  left: 50%;
+  margin-left: -270px;
+  width: 540px;
+  background-color: #fff;
+  opacity: 0;
+  transition: all 0.2s ease;
+  border-radius: 3px;
+}
+
+.modal:focus,
+.ReactModal__Content:focus {
+  outline: none;
+}
+
+.modal.in,
+.ReactModal__Content--after-open {
+  top: 15%;
+  opacity: 1;
+}
+
+.modal-small {
+  width: 450px;
+  margin-left: -225px;
+}
+
+.modal-medium {
+  width: 830px;
+  margin-left: -415px;
+}
+
+.modal-large {
+  width: calc(100% - 40px);
+  max-width: 1280px;
+  min-width: 1040px;
+  margin-left: 0;
+  transform: translateX(-50%);
+}
+
+.modal-overlay,
+.ReactModal__Overlay {
+  position: fixed;
+  z-index: var(--modalOverlayZIndex);
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background-color: rgba(0, 0, 0, 0.7);
+  opacity: 0;
+  transition: all 0.2s ease;
+}
+
+.modal-overlay.in,
+.ReactModal__Overlay--after-open {
+  opacity: 1;
+}
+
+.modal-no-backdrop {
+  background-color: transparent;
+}
+
+.modal-open,
+.ReactModal__Body--open {
+  overflow: hidden;
+  margin-right: var(--sbw);
+}
+
+.modal-head {
+  padding: calc(4 * var(--gridSize));
+  padding-bottom: 0;
+}
+
+.modal-head h1,
+.modal-head h2 {
+  margin: 0;
+  font-size: var(--bigFontSize);
+  font-weight: bold;
+  line-height: normal;
+  overflow-wrap: break-word;
+}
+
+.modal-body {
+  padding: var(--pagePadding) calc(4 * var(--gridSize));
+}
+
+.modal-container {
+  max-height: 60vh;
+  box-sizing: border-box;
+  overflow-y: auto;
+  border-top: 1px solid var(--barBorderColor);
+  margin-top: var(--pagePadding);
+  padding-right: calc(4 * var(--gridSize));
+}
+
+.modal-container > :last-child {
+  margin-bottom: var(--pagePadding);
+}
+
+.modal-field,
+.modal-validation-field {
+  clear: both;
+  display: block;
+  padding: 0;
+  margin-bottom: calc(var(--gridSize) * 2);
+}
+
+.modal-field label,
+.modal-validation-field label {
+  display: block;
+  font-weight: bold;
+  padding-bottom: calc(var(--gridSize) / 2);
+}
+
+.modal-field a.icon-checkbox,
+.modal-field input,
+.modal-field select,
+.modal-field textarea,
+.modal-field .Select {
+  margin-right: 5px;
+}
+
+.modal-field a.icon-checkbox {
+  height: 24px;
+}
+
+.modal-field input[type='radio'],
+.modal-field input[type='checkbox'] {
+  margin-top: 5px;
+  margin-bottom: 4px;
+}
+
+.modal-field > .icon-checkbox {
+  padding-top: 6px;
+  padding-right: 8px;
+}
+
+.modal-field input[type='text'],
+.modal-field input[type='email'],
+.modal-field input[type='password'],
+.modal-field textarea,
+.modal-field select,
+.modal-field .Select {
+  width: 100%;
+}
+
+.modal-validation-field input,
+.modal-validation-field textarea,
+.modal-validation-field .Select {
+  margin-right: var(--gridSize);
+  margin-bottom: 2px;
+  width: calc(100% - 3 * var(--gridSize));
+}
+
+.modal-field textarea,
+.modal-validation-field textarea {
+  max-width: 100%;
+  min-width: 100%;
+  max-height: 50vh;
+  min-height: var(--controlHeight);
+}
+.modal-validation-field input:not(.is-invalid),
+.modal-validation-field .Select:not(.is-invalid) {
+  margin-bottom: calc(var(--tinyControlHeight) + 2px);
+}
+
+.modal-field-description {
+  line-height: 1.4;
+  color: var(--secondFontColor);
+  font-size: var(--smallFontSize);
+  overflow: hidden;
+  text-overflow: ellipsis;
+  margin-top: 2px;
+}
+
+.modal-foot {
+  padding: var(--pagePadding) calc(4 * var(--gridSize));
+  border-top: 1px solid var(--barBorderColor);
+  background-color: var(--barBackgroundColor);
+  border-radius: 3px;
+  text-align: right;
+}
+
+.modal-foot button,
+.modal-foot .button,
+.modal-foot input[type='submit'],
+.modal-foot input[type='button'] {
+  margin-left: var(--gridSize);
+}
diff --git a/server/sonar-ui-common/components/controls/Modal.tsx b/server/sonar-ui-common/components/controls/Modal.tsx
new file mode 100644 (file)
index 0000000..91acdea
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import * as ReactModal from 'react-modal';
+import { getReactDomContainerSelector } from '../../helpers/init';
+import './Modal.css';
+
+ReactModal.setAppElement(getReactDomContainerSelector());
+
+export interface ModalProps {
+  children: React.ReactNode;
+  size?: 'small' | 'medium' | 'large';
+  noBackdrop?: boolean;
+}
+
+interface Props extends ModalProps {
+  /* String or object className to be applied to the modal content. */
+  className?: string;
+
+  /* String or object className to be applied to the overlay. */
+  overlayClassName?: string;
+
+  /* Function that will be run after the modal has opened. */
+  onAfterOpen?(): void;
+
+  /* Function that will be run after the modal has closed. */
+  onAfterClose?(): void;
+
+  /* Function that will be run when the modal is requested to be closed, prior to actually closing. */
+  onRequestClose?(event: React.MouseEvent | React.KeyboardEvent): void;
+
+  /* Boolean indicating if the modal should be focused after render */
+  shouldFocusAfterRender?: boolean;
+
+  /* Boolean indicating if the overlay should close the modal. Defaults to true. */
+  shouldCloseOnOverlayClick?: boolean;
+
+  /* Boolean indicating if pressing the esc key should close the modal */
+  shouldCloseOnEsc?: boolean;
+
+  /* String indicating how the content container should be announced to screenreaders. */
+  contentLabel: string;
+}
+
+export default function Modal(props: Props) {
+  return (
+    <ReactModal
+      className={classNames('modal', {
+        'modal-small': props.size === 'small',
+        'modal-medium': props.size === 'medium',
+        'modal-large': props.size === 'large',
+      })}
+      isOpen={true}
+      overlayClassName={classNames('modal-overlay', { 'modal-no-backdrop': props.noBackdrop })}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/ModalButton.tsx b/server/sonar-ui-common/components/controls/ModalButton.tsx
new file mode 100644 (file)
index 0000000..44e43f5
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+
+export interface ChildrenProps {
+  onClick: () => void;
+  onFormSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
+}
+
+export interface ModalProps {
+  onClose: () => void;
+}
+
+export interface Props {
+  children: (props: ChildrenProps) => React.ReactNode;
+  modal: (props: ModalProps) => React.ReactNode;
+}
+
+interface State {
+  modal: boolean;
+}
+
+export default class ModalButton extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { modal: false };
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  handleButtonClick = () => {
+    this.setState({ modal: true });
+  };
+
+  handleFormSubmit = (event?: React.FormEvent<HTMLFormElement>) => {
+    if (event) {
+      event.preventDefault();
+    }
+    this.setState({ modal: true });
+  };
+
+  handleCloseModal = () => {
+    if (this.mounted) {
+      this.setState({ modal: false });
+    }
+  };
+
+  render() {
+    return (
+      <>
+        {this.props.children({
+          onClick: this.handleButtonClick,
+          onFormSubmit: this.handleFormSubmit,
+        })}
+        {this.state.modal && this.props.modal({ onClose: this.handleCloseModal })}
+      </>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/ModalValidationField.tsx b/server/sonar-ui-common/components/controls/ModalValidationField.tsx
new file mode 100644 (file)
index 0000000..c5eeff1
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import AlertErrorIcon from '../icons/AlertErrorIcon';
+import AlertSuccessIcon from '../icons/AlertSuccessIcon';
+
+interface Props {
+  children: (props: { className?: string }) => React.ReactNode;
+  description?: string;
+  dirty: boolean;
+  error: string | undefined;
+  label?: React.ReactNode;
+  touched: boolean | undefined;
+}
+
+export default function ModalValidationField(props: Props) {
+  const { description, dirty, error } = props;
+
+  const isValid = dirty && props.touched && error === undefined;
+  const showError = dirty && props.touched && error !== undefined;
+  return (
+    <div className="modal-validation-field">
+      {props.label}
+      {props.children({ className: classNames({ 'is-invalid': showError, 'is-valid': isValid }) })}
+      {showError && <AlertErrorIcon className="little-spacer-top" />}
+      {isValid && <AlertSuccessIcon className="little-spacer-top" />}
+      {showError && <p className="text-danger">{error}</p>}
+      {description && <div className="modal-field-description">{description}</div>}
+    </div>
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/OutsideClickHandler.tsx b/server/sonar-ui-common/components/controls/OutsideClickHandler.tsx
new file mode 100644 (file)
index 0000000..c7c86ba
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { findDOMNode } from 'react-dom';
+
+interface Props {
+  children: React.ReactNode;
+  onClickOutside: () => void;
+}
+
+export default class OutsideClickHandler extends React.Component<Props> {
+  mounted = false;
+
+  componentDidMount() {
+    this.mounted = true;
+    setTimeout(() => {
+      this.addClickHandler();
+    }, 0);
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+    this.removeClickHandler();
+  }
+
+  addClickHandler = () => {
+    window.addEventListener('click', this.handleWindowClick);
+  };
+
+  removeClickHandler = () => {
+    window.removeEventListener('click', this.handleWindowClick);
+  };
+
+  handleWindowClick = (event: MouseEvent) => {
+    if (this.mounted) {
+      // eslint-disable-next-line react/no-find-dom-node
+      const node = findDOMNode(this);
+      if (!node || !node.contains(event.target as Node)) {
+        this.props.onClickOutside();
+      }
+    }
+  };
+
+  render() {
+    return this.props.children;
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/Radio.css b/server/sonar-ui-common/components/controls/Radio.css
new file mode 100644 (file)
index 0000000..172c89c
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+.icon-radio {
+  position: relative;
+  display: inline-block;
+  vertical-align: top;
+  width: 14px;
+  height: 14px;
+  margin: 1px;
+  border: 1px solid var(--gray80);
+  border-radius: 12px;
+  box-sizing: border-box;
+  transition: border-color 0.3s ease;
+  flex-shrink: 0;
+}
+
+.icon-radio:after {
+  position: absolute;
+  top: 2px;
+  left: 2px;
+  display: block;
+  width: 8px;
+  height: 8px;
+  border-radius: 8px;
+  background-color: var(--darkBlue);
+  content: '';
+  opacity: 0;
+  transition: opacity 0.3s ease;
+}
+
+.link-radio .icon-radio.is-checked:after {
+  opacity: 1;
+}
+
+.link-radio {
+  color: inherit;
+  border-bottom: none;
+}
+
+.link-radio:not(.disabled):hover,
+.link-radio:not(.disabled):active,
+.link-radio:not(.disabled):focus {
+  color: inherit;
+}
+
+.link-radio:not(.disabled):hover > .icon-radio {
+  border-color: var(--blue);
+}
+
+.link-radio.disabled,
+.link-radio.disabled:hover,
+.link-radio.disabled label {
+  color: var(--disableGrayText);
+  cursor: not-allowed;
+}
+
+.link-radio.disabled .icon-radio:after {
+  background-color: var(--disableGrayBg);
+}
diff --git a/server/sonar-ui-common/components/controls/Radio.tsx b/server/sonar-ui-common/components/controls/Radio.tsx
new file mode 100644 (file)
index 0000000..1095a30
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import './Radio.css';
+
+interface Props {
+  checked: boolean;
+  className?: string;
+  disabled?: boolean;
+  onCheck: (value: string) => void;
+  value: string;
+}
+
+export default class Radio extends React.PureComponent<Props> {
+  handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+
+    if (!this.props.disabled) {
+      this.props.onCheck(this.props.value);
+    }
+  };
+
+  render() {
+    const { className, checked, children, disabled } = this.props;
+
+    return (
+      <a
+        aria-checked={checked}
+        className={classNames('display-inline-flex-center link-radio', className, { disabled })}
+        href="#"
+        onClick={this.handleClick}
+        role="radio">
+        <i className={classNames('icon-radio', 'spacer-right', { 'is-checked': checked })} />
+        {children}
+      </a>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/RadioCard.css b/server/sonar-ui-common/components/controls/RadioCard.css
new file mode 100644 (file)
index 0000000..4afbef0
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.radio-card {
+  display: flex;
+  flex-direction: column;
+  width: 450px;
+  min-height: 210px;
+  background-color: #fff;
+  border: solid 1px var(--barBorderColor);
+  border-radius: 3px;
+  box-sizing: border-box;
+  margin-right: calc(2 * var(--gridSize));
+  transition: all 0.2s ease;
+}
+
+.radio-card.animated {
+  height: 0;
+  border-width: 0;
+  overflow: hidden;
+}
+
+.radio-card.animated.open {
+  height: 210px;
+  border-width: 1px;
+}
+
+.radio-card.highlight {
+  box-shadow: var(--defaultShadow);
+}
+
+.radio-card:last-child {
+  margin-right: 0;
+}
+
+.radio-card:focus {
+  outline: none;
+}
+
+.radio-card-vertical {
+  width: 100%;
+  min-height: auto;
+}
+
+.radio-card-actionable {
+  cursor: pointer;
+}
+
+.radio-card-actionable:not(.disabled):hover {
+  box-shadow: var(--defaultShadow);
+  transform: translateY(-2px);
+}
+
+.radio-card-actionable.selected {
+  border-color: var(--darkBlue);
+}
+
+/*
+ * Disabled transform property because it moves the element to a new stacking context
+ * creating z-index conflicts with other components.
+ * This is a problem with a vertical list of RadioCards where a select might be above another RadioCard
+ */
+.radio-card-actionable.radio-card-vertical:not(.disabled):hover {
+  box-shadow: none;
+  transform: none;
+}
+
+.radio-card-actionable.radio-card-vertical:not(.selected):not(.disabled):hover {
+  border-color: var(--lightBlue);
+}
+
+.radio-card-actionable.selected .radio-card-recommended {
+  border: solid 1px var(--darkBlue);
+  border-top: none;
+}
+
+.radio-card-actionable.disabled {
+  cursor: not-allowed;
+  background-color: var(--disableGrayBg);
+  border-color: var(--disableGrayBorder);
+}
+
+.radio-card-actionable.disabled h2,
+.radio-card-actionable.disabled ul {
+  color: var(--disableGrayText);
+}
+
+.radio-card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: calc(2 * var(--gridSize)) calc(2 * var(--gridSize)) 0;
+}
+
+.radio-card-body {
+  flex-grow: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  padding: 0 calc(2 * var(--gridSize)) calc(2 * var(--gridSize));
+}
+
+.radio-card-body .alert {
+  margin-bottom: 0;
+}
+
+.radio-card-recommended {
+  position: relative;
+  padding: 6px calc(var(--gridSize) * 2);
+  left: -1px;
+  bottom: -1px;
+  width: 450px;
+  color: #fff;
+  background-color: var(--blue);
+  border-radius: 0 0 3px 3px;
+  box-sizing: border-box;
+  font-size: var(--smallFontSize);
+}
diff --git a/server/sonar-ui-common/components/controls/RadioCard.tsx b/server/sonar-ui-common/components/controls/RadioCard.tsx
new file mode 100644 (file)
index 0000000..e650199
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { translate } from '../../helpers/l10n';
+import RecommendedIcon from '../icons/RecommendedIcon';
+import './Radio.css';
+import './RadioCard.css';
+
+export interface RadioCardProps {
+  className?: string;
+  disabled?: boolean;
+  onClick?: () => void;
+  selected?: boolean;
+}
+
+interface Props extends RadioCardProps {
+  children: React.ReactNode;
+  recommended?: string;
+  title: React.ReactNode;
+  titleInfo?: React.ReactNode;
+  vertical?: boolean;
+}
+
+export default function RadioCard(props: Props) {
+  const {
+    className,
+    disabled,
+    onClick,
+    recommended,
+    selected,
+    titleInfo,
+    vertical = false,
+  } = props;
+  const isActionable = Boolean(onClick);
+  return (
+    <div
+      aria-checked={selected}
+      className={classNames(
+        'radio-card',
+        {
+          'radio-card-actionable': isActionable,
+          'radio-card-vertical': vertical,
+          disabled,
+          selected,
+        },
+        className
+      )}
+      onClick={isActionable && !disabled ? onClick : undefined}
+      role="radio"
+      tabIndex={0}>
+      <h2 className="radio-card-header big-spacer-bottom">
+        <span className="display-flex-center link-radio">
+          {isActionable && (
+            <i className={classNames('icon-radio', 'spacer-right', { 'is-checked': selected })} />
+          )}
+          {props.title}
+        </span>
+        {titleInfo}
+      </h2>
+      <div className="radio-card-body">{props.children}</div>
+      {recommended && (
+        <div className="radio-card-recommended">
+          <RecommendedIcon className="spacer-right" />
+          <FormattedMessage
+            defaultMessage={recommended}
+            id={recommended}
+            values={{ recommended: <strong>{translate('recommended')}</strong> }}
+          />
+        </div>
+      )}
+    </div>
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/RadioToggle.css b/server/sonar-ui-common/components/controls/RadioToggle.css
new file mode 100644 (file)
index 0000000..747c4a4
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.radio-toggle {
+  display: inline-block;
+  vertical-align: middle;
+  font-size: 0;
+  white-space: nowrap;
+}
+
+.radio-toggle > li {
+  display: inline-block;
+  vertical-align: middle;
+  font-size: var(--smallFontSize);
+}
+
+.radio-toggle > li:first-child > label {
+  border-top-left-radius: 2px;
+  border-bottom-left-radius: 2px;
+}
+
+.radio-toggle > li:last-child > label {
+  border-top-right-radius: 2px;
+  border-bottom-right-radius: 2px;
+}
+
+.radio-toggle > li + li > label {
+  border-left: none;
+}
+
+.radio-toggle > li > label {
+  display: inline-block;
+  padding: 0 12px;
+  margin: 0;
+  border: 1px solid var(--darkBlue);
+  color: var(--darkBlue);
+  height: calc(var(--controlHeight) - 2px);
+  line-height: calc(var(--controlHeight) - 2px);
+  cursor: pointer;
+  font-weight: 600;
+}
+
+.radio-toggle input[type='radio'] {
+  display: none;
+}
+
+.radio-toggle input[type='radio']:checked + label {
+  background-color: var(--darkBlue);
+  color: #fff;
+}
+
+.radio-toggle input[type='radio']:disabled + label {
+  color: var(--disableGrayText);
+  border-color: var(--disableGrayBorder);
+  background: var(--disableGrayBg);
+  cursor: not-allowed;
+}
diff --git a/server/sonar-ui-common/components/controls/RadioToggle.tsx b/server/sonar-ui-common/components/controls/RadioToggle.tsx
new file mode 100644 (file)
index 0000000..1ec9ed3
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import './RadioToggle.css';
+import Tooltip from './Tooltip';
+
+type ToggleValueType = string | number | boolean;
+interface Option {
+  disabled?: boolean;
+  label: string;
+  tooltip?: string;
+  value: ToggleValueType;
+}
+
+interface Props {
+  className?: string;
+  name: string;
+  onCheck: (value: ToggleValueType) => void;
+  options: Option[];
+  value?: ToggleValueType;
+}
+
+export default class RadioToggle extends React.PureComponent<Props> {
+  static defaultProps = {
+    disabled: false,
+    value: null,
+  };
+
+  renderOption = (option: Option) => {
+    const checked = option.value === this.props.value;
+    const htmlId = this.props.name + '__' + option.value;
+    return (
+      <li key={option.value.toString()}>
+        <input
+          checked={checked}
+          disabled={option.disabled}
+          id={htmlId}
+          name={this.props.name}
+          onChange={() => this.props.onCheck(option.value)}
+          type="radio"
+        />
+        <Tooltip overlay={option.tooltip || undefined}>
+          <label htmlFor={htmlId}>{option.label}</label>
+        </Tooltip>
+      </li>
+    );
+  };
+
+  render() {
+    return (
+      <ul className={classNames('radio-toggle', this.props.className)}>
+        {this.props.options.map(this.renderOption)}
+      </ul>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/ReloadButton.tsx b/server/sonar-ui-common/components/controls/ReloadButton.tsx
new file mode 100644 (file)
index 0000000..69d3bb3
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import { ThemeConsumer } from '../theme';
+import Tooltip from './Tooltip';
+
+interface Props {
+  className?: string;
+  tooltip?: string;
+  onClick: () => void;
+}
+
+export default class ReloadButton extends React.PureComponent<Props> {
+  handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.props.onClick();
+  };
+
+  render() {
+    const { tooltip = translate('reload') } = this.props;
+    return (
+      <Tooltip overlay={tooltip}>
+        <a
+          className={classNames('link-no-underline', this.props.className)}
+          href="#"
+          onClick={this.handleClick}>
+          {
+            <ThemeConsumer>
+              {(theme) => (
+                <svg height="24" viewBox="0 0 18 24" width="18">
+                  <path
+                    d="M16.6454 8.1084c-.3-.5-.9-.7-1.4-.4-.5.3-.7.9-.4 1.4.9 1.6 1.1 3.4.6 5.1-.5 1.7-1.7 3.2-3.2 4-3.3 1.8-7.4.6-9.1-2.7-1.8-3.1-.8-6.9 2.1-8.8v3.3h2v-7h-7v2h3.9c-3.7 2.5-5 7.5-2.8 11.4 1.6 3 4.6 4.6 7.7 4.6 1.4 0 2.8-.3 4.2-1.1 2-1.1 3.5-3 4.2-5.2.6-2.2.3-4.6-.8-6.6z"
+                    fill={theme.colors.secondFontColor}
+                  />
+                </svg>
+              )}
+            </ThemeConsumer>
+          }
+        </a>
+      </Tooltip>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/ScreenPositionFixer.tsx b/server/sonar-ui-common/components/controls/ScreenPositionFixer.tsx
new file mode 100644 (file)
index 0000000..69c00f8
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { throttle } from 'lodash';
+import * as React from 'react';
+import { findDOMNode } from 'react-dom';
+import { Theme, withTheme } from '../theme';
+
+interface Props {
+  /**
+   * First time `children` are rendered with `undefined` fixes to measure the offset.
+   * Second time it renders with the computed fixes.
+   */
+  children: (props: Fixes) => React.ReactNode;
+
+  /**
+   * Use this flag to force re-positioning.
+   * Use cases:
+   *   - when you need to measure `children` size first
+   *   - when you load content asynchronously
+   */
+  ready?: boolean;
+  theme: Theme;
+}
+
+interface Fixes {
+  leftFix?: number;
+  topFix?: number;
+}
+
+export class ScreenPositionFixer extends React.Component<Props, Fixes> {
+  throttledPosition: () => void;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {};
+    this.throttledPosition = throttle(this.position, 50);
+  }
+
+  componentDidMount() {
+    this.addEventListeners();
+    this.position();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (!prevProps.ready && this.props.ready) {
+      this.position();
+    } else if (prevProps.ready && !this.props.ready) {
+      this.reset();
+    }
+  }
+
+  componentWillUnmount() {
+    this.removeEventListeners();
+  }
+
+  addEventListeners = () => {
+    window.addEventListener('resize', this.throttledPosition);
+  };
+
+  removeEventListeners = () => {
+    window.removeEventListener('resize', this.throttledPosition);
+  };
+
+  reset = () => {
+    this.setState({ leftFix: undefined, topFix: undefined });
+  };
+
+  position = () => {
+    const edgeMargin = 0.5 * this.props.theme.rawSizes.grid;
+
+    // eslint-disable-next-line react/no-find-dom-node
+    const node = findDOMNode(this);
+    if (node && node instanceof Element) {
+      const { width, height, left, top } = node.getBoundingClientRect();
+      const { clientHeight, clientWidth } = document.documentElement;
+
+      let leftFix = 0;
+      if (left < edgeMargin) {
+        leftFix = edgeMargin - left;
+      } else if (left + width > clientWidth - edgeMargin) {
+        leftFix = clientWidth - edgeMargin - left - width;
+      }
+
+      let topFix = 0;
+      if (top < edgeMargin) {
+        topFix = edgeMargin - top;
+      } else if (top + height > clientHeight - edgeMargin) {
+        topFix = clientHeight - edgeMargin - top - height;
+      }
+
+      this.setState({ leftFix, topFix });
+    }
+  };
+
+  render() {
+    return this.props.children(this.state);
+  }
+}
+
+export default withTheme(ScreenPositionFixer);
diff --git a/server/sonar-ui-common/components/controls/SearchBox.css b/server/sonar-ui-common/components/controls/SearchBox.css
new file mode 100644 (file)
index 0000000..412058a
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.search-box {
+  position: relative;
+  display: inline-block;
+  vertical-align: middle;
+  font-size: 0;
+  white-space: nowrap;
+}
+
+.search-box,
+.search-box-input {
+  width: 100%;
+  max-width: 300px;
+}
+
+.search-box-input {
+  /* for magnifier icon */
+  padding-left: var(--controlHeight) !important;
+  /* for clear button */
+  padding-right: var(--controlHeight) !important;
+  font-size: var(--baseFontSize);
+}
+
+.search-box-input::-webkit-search-decoration,
+.search-box-input::-webkit-search-cancel-button,
+.search-box-input::-webkit-search-results-button,
+.search-box-input::-webkit-search-results-decoration {
+  -webkit-appearance: none;
+  display: none;
+}
+
+.search-box-input::-ms-clear,
+.search-box-input::-ms-reveal {
+  display: none;
+  width: 0;
+  height: 0;
+}
+
+.search-box-note {
+  position: absolute;
+  top: 1px;
+  left: 40px;
+  right: var(--controlHeight);
+  line-height: var(--controlHeight);
+  color: var(--secondFontColor);
+  font-size: var(--smallFontSize);
+  text-align: right;
+  text-overflow: ellipsis;
+  overflow: hidden;
+  white-space: nowrap;
+  pointer-events: none;
+}
+
+.search-box-input:focus ~ .search-box-magnifier {
+  color: var(--blue);
+}
+
+.search-box-magnifier {
+  position: absolute;
+  top: 4px;
+  left: 4px;
+  color: var(--gray60);
+  transition: color 0.3s ease;
+}
+
+.search-box > .deferred-spinner {
+  position: absolute;
+  top: 4px;
+  left: 5px;
+}
+
+.search-box-clear {
+  position: absolute;
+  top: 4px;
+  right: 4px;
+}
+
+.search-box-input-note {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  line-height: 1;
+  color: var(--secondFontColor);
+  font-size: var(--smallFontSize);
+  white-space: nowrap;
+}
diff --git a/server/sonar-ui-common/components/controls/SearchBox.tsx b/server/sonar-ui-common/components/controls/SearchBox.tsx
new file mode 100644 (file)
index 0000000..69272bb
--- /dev/null
@@ -0,0 +1,176 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import { Cancelable, debounce } from 'lodash';
+import * as React from 'react';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import SearchIcon from '../icons/SearchIcon';
+import DeferredSpinner from '../ui/DeferredSpinner';
+import { ClearButton } from './buttons';
+import './SearchBox.css';
+
+interface Props {
+  autoFocus?: boolean;
+  className?: string;
+  id?: string;
+  innerRef?: (node: HTMLInputElement | null) => void;
+  loading?: boolean;
+  maxLength?: number;
+  minLength?: number;
+  onChange: (value: string) => void;
+  onClick?: React.MouseEventHandler<HTMLInputElement>;
+  onFocus?: React.FocusEventHandler<HTMLInputElement>;
+  onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
+  placeholder: string;
+  value?: string;
+}
+
+interface State {
+  value: string;
+}
+
+const DEFAULT_MAX_LENGTH = 100;
+
+export default class SearchBox extends React.PureComponent<Props, State> {
+  debouncedOnChange: ((query: string) => void) & Cancelable;
+  input?: HTMLInputElement | null;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = { value: props.value || '' };
+    this.debouncedOnChange = debounce(this.props.onChange, 250);
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (
+      // input is controlled
+      this.props.value !== undefined &&
+      // parent is aware of last change
+      // can happen when previous value was less than min length
+      this.state.value === prevProps.value &&
+      this.state.value !== this.props.value
+    ) {
+      this.setState({ value: this.props.value });
+    }
+  }
+
+  changeValue = (value: string, debounced = true) => {
+    const { minLength } = this.props;
+    if (value.length === 0) {
+      // immediately notify when value is empty
+      this.props.onChange('');
+      // and cancel scheduled callback
+      this.debouncedOnChange.cancel();
+    } else if (!minLength || minLength <= value.length) {
+      if (debounced) {
+        this.debouncedOnChange(value);
+      } else {
+        this.props.onChange(value);
+      }
+    }
+  };
+
+  handleInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
+    const { value } = event.currentTarget;
+    this.setState({ value });
+    this.changeValue(value);
+  };
+
+  handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
+    if (event.keyCode === 27) {
+      // escape
+      event.preventDefault();
+      this.handleResetClick();
+    }
+    if (this.props.onKeyDown) {
+      this.props.onKeyDown(event);
+    }
+  };
+
+  handleResetClick = () => {
+    this.changeValue('', false);
+    if (this.props.value === undefined || this.props.value === '') {
+      this.setState({ value: '' });
+    }
+    if (this.input) {
+      this.input.focus();
+    }
+  };
+
+  ref = (node: HTMLInputElement | null) => {
+    this.input = node;
+    if (this.props.innerRef) {
+      this.props.innerRef(node);
+    }
+  };
+
+  render() {
+    const { loading, minLength, maxLength = DEFAULT_MAX_LENGTH } = this.props;
+    const { value } = this.state;
+
+    const inputClassName = classNames('search-box-input', {
+      touched: value.length > 0 && (!minLength || minLength > value.length),
+    });
+
+    const tooShort = minLength !== undefined && value.length > 0 && value.length < minLength;
+
+    return (
+      <div
+        className={classNames('search-box', this.props.className)}
+        id={this.props.id}
+        title={tooShort ? translateWithParameters('select2.tooShort', minLength!) : ''}>
+        <input
+          aria-label={translate('search_verb')}
+          autoComplete="off"
+          autoFocus={this.props.autoFocus}
+          className={inputClassName}
+          maxLength={maxLength}
+          onChange={this.handleInputChange}
+          onClick={this.props.onClick}
+          onFocus={this.props.onFocus}
+          onKeyDown={this.handleInputKeyDown}
+          placeholder={this.props.placeholder}
+          ref={this.ref}
+          type="search"
+          value={value}
+        />
+
+        <DeferredSpinner loading={loading !== undefined ? loading : false}>
+          <SearchIcon className="search-box-magnifier" />
+        </DeferredSpinner>
+
+        {value && (
+          <ClearButton
+            aria-label={translate('clear')}
+            className="button-tiny search-box-clear"
+            iconProps={{ size: 12 }}
+            onClick={this.handleResetClick}
+          />
+        )}
+
+        {tooShort && (
+          <span className="search-box-note">
+            {translateWithParameters('select2.tooShort', minLength!)}
+          </span>
+        )}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/SearchSelect.tsx b/server/sonar-ui-common/components/controls/SearchSelect.tsx
new file mode 100644 (file)
index 0000000..c446f35
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { debounce } from 'lodash';
+import * as React from 'react';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import Select, { Creatable } from './Select';
+
+interface Props<T> {
+  autofocus?: boolean;
+  canCreate?: boolean;
+  className?: string;
+  clearable?: boolean;
+  defaultOptions?: T[];
+  minimumQueryLength?: number;
+  multi?: boolean;
+  onSearch: (query: string) => Promise<T[]>;
+  onSelect?: (option: T) => void;
+  onMultiSelect?: (options: T[]) => void;
+  promptTextCreator?: (label: string) => string;
+  renderOption?: (option: T) => JSX.Element;
+  resetOnBlur?: boolean;
+  value?: T | T[];
+}
+
+interface State<T> {
+  loading: boolean;
+  options: T[];
+  query: string;
+}
+
+export default class SearchSelect<T extends { value: string }> extends React.PureComponent<
+  Props<T>,
+  State<T>
+> {
+  mounted = false;
+
+  constructor(props: Props<T>) {
+    super(props);
+    this.state = { loading: false, options: props.defaultOptions || [], query: '' };
+    this.handleSearch = debounce(this.handleSearch, 250);
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  get autofocus() {
+    return this.props.autofocus !== undefined ? this.props.autofocus : true;
+  }
+
+  get minimumQueryLength() {
+    return this.props.minimumQueryLength !== undefined ? this.props.minimumQueryLength : 2;
+  }
+
+  get resetOnBlur() {
+    return this.props.resetOnBlur !== undefined ? this.props.resetOnBlur : true;
+  }
+
+  handleSearch = (query: string) => {
+    // Ignore the result if the query changed
+    const currentQuery = query;
+    this.props.onSearch(currentQuery).then(
+      (options) => {
+        if (this.mounted) {
+          this.setState((state) => ({
+            loading: false,
+            options: state.query === currentQuery ? options : state.options,
+          }));
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  handleChange = (option: T | T[]) => {
+    if (Array.isArray(option)) {
+      if (this.props.onMultiSelect) {
+        this.props.onMultiSelect(option);
+      }
+    } else if (this.props.onSelect) {
+      this.props.onSelect(option);
+    }
+  };
+
+  handleInputChange = (query: string) => {
+    if (query.length >= this.minimumQueryLength) {
+      this.setState({ loading: true, query });
+      this.handleSearch(query);
+    } else {
+      // `onInputChange` is called with an empty string after a user selects a value
+      // in this case we shouldn't reset `options`, because it also resets select value :(
+      const options = (query.length === 0 && this.props.defaultOptions) || [];
+      this.setState({ options, query });
+    }
+  };
+
+  // disable internal filtering
+  handleFilterOption = () => true;
+
+  render() {
+    const Component = this.props.canCreate ? Creatable : Select;
+    return (
+      <Component
+        autoFocus={this.autofocus}
+        className={this.props.className}
+        clearable={this.props.clearable}
+        escapeClearsValue={false}
+        filterOption={this.handleFilterOption}
+        isLoading={this.state.loading}
+        multi={this.props.multi}
+        noResultsText={
+          this.state.query.length < this.minimumQueryLength
+            ? translateWithParameters('select2.tooShort', this.minimumQueryLength)
+            : translate('select2.noMatches')
+        }
+        onBlurResetsInput={this.resetOnBlur}
+        onChange={this.handleChange}
+        onInputChange={this.handleInputChange}
+        optionRenderer={this.props.renderOption}
+        options={this.state.options}
+        placeholder={translate('search_verb')}
+        promptTextCreator={this.props.promptTextCreator}
+        searchable={true}
+        value={this.props.value}
+        valueRenderer={this.props.renderOption}
+      />
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/Select.css b/server/sonar-ui-common/components/controls/Select.css
new file mode 100644 (file)
index 0000000..db74eb8
--- /dev/null
@@ -0,0 +1,477 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.Select {
+  position: relative;
+  display: inline-block;
+  vertical-align: middle;
+  font-size: var(--smallFontSize);
+  text-align: left;
+}
+
+.Select,
+.Select div,
+.Select input,
+.Select span {
+  box-sizing: border-box;
+}
+
+.Select.is-disabled > .Select-control {
+  background-color: var(--disableGrayBg) !important;
+  border-color: var(--disableGrayBorder) !important;
+}
+
+.Select.is-disabled > .Select-control:hover {
+  box-shadow: none !important;
+}
+
+.Select.is-disabled .Select-arrow-zone {
+  cursor: not-allowed !important;
+  pointer-events: none !important;
+}
+
+.Select.is-disabled .Select-placeholder,
+.Select.is-disabled .Select-value {
+  color: var(--disableGrayText) !important;
+}
+
+.Select-control {
+  position: relative;
+  display: table;
+  width: 100%;
+  height: var(--controlHeight);
+  line-height: calc(var(--controlHeight) - 2px);
+  border: 1px solid var(--gray80);
+  border-collapse: separate;
+  border-radius: 2px;
+  background-color: #fff;
+  color: var(--baseFontColor);
+  cursor: default;
+  outline: none;
+  overflow: hidden;
+}
+
+.is-searchable.is-open > .Select-control {
+  cursor: text;
+}
+
+.is-open > .Select-control {
+  border-bottom-right-radius: 0;
+  border-bottom-left-radius: 0;
+  background: #fff;
+}
+
+.is-open > .Select-control > .Select-arrow {
+  border-color: transparent transparent #999;
+  border-width: 0 5px 5px;
+}
+
+.is-searchable.is-focused:not(.is-open) > .Select-control {
+  cursor: text;
+}
+
+.is-focused:not(.is-open) > .Select-control {
+  border-color: var(--blue);
+}
+
+.Select-placeholder {
+  color: var(--secondFontColor);
+}
+
+.Select-placeholder,
+:not(.Select--multi) > .Select-control .Select-value {
+  bottom: 0;
+  left: 0;
+  line-height: 23px;
+  padding-left: 8px;
+  padding-right: 24px;
+  position: absolute;
+  right: 0;
+  top: 0;
+  max-width: 100%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.Select-value [class^='icon-'] {
+  padding-top: 5px;
+}
+
+.Select-value svg,
+.Select-value img {
+  padding-top: 3px;
+}
+
+.Select-option svg,
+.Select-option img,
+.Select-option [class^='icon-'] {
+  padding-top: 2px;
+}
+
+.has-value:not(.Select--multi) > .Select-control > .Select-value .Select-value-label,
+.has-value.is-pseudo-focused:not(.Select--multi)
+  > .Select-control
+  > .Select-value
+  .Select-value-label {
+  color: var(--baseFontColor);
+}
+
+.has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label,
+.has-value.is-pseudo-focused:not(.Select--multi)
+  > .Select-control
+  > .Select-value
+  a.Select-value-label {
+  cursor: pointer;
+  text-decoration: none;
+}
+
+.has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:hover,
+.has-value.is-pseudo-focused:not(.Select--multi)
+  > .Select-control
+  > .Select-value
+  a.Select-value-label:hover,
+.has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:focus,
+.has-value.is-pseudo-focused:not(.Select--multi)
+  > .Select-control
+  > .Select-value
+  a.Select-value-label:focus {
+  color: #007eff;
+  outline: none;
+  text-decoration: underline;
+}
+
+.Select-input {
+  vertical-align: top;
+  height: 22px;
+  padding-left: 8px;
+  padding-right: 8px;
+  outline: none;
+}
+
+.Select-input > input {
+  background: none transparent;
+  border: 0 none;
+  cursor: default;
+  display: inline-block;
+  font-family: inherit;
+  font-size: var(--smallFontSize);
+  height: 22px;
+  margin: 0;
+  outline: none;
+  padding: 0;
+  box-shadow: none;
+  -webkit-appearance: none;
+}
+
+.is-focused .Select-input > input {
+  cursor: text;
+}
+
+.has-value.is-pseudo-focused .Select-input {
+  opacity: 0;
+}
+
+.Select-control:not(.is-searchable) > .Select-input {
+  outline: none;
+}
+
+.Select-loading-zone {
+  cursor: pointer;
+  display: table-cell;
+  position: relative;
+  text-align: center;
+  vertical-align: middle;
+  width: 16px;
+}
+
+.Select-loading {
+  -webkit-animation: Select-animation-spin 400ms infinite linear;
+  -o-animation: Select-animation-spin 400ms infinite linear;
+  animation: Select-animation-spin 400ms infinite linear;
+  width: 16px;
+  height: 16px;
+  box-sizing: border-box;
+  border-radius: 50%;
+  border: 2px solid #ccc;
+  border-right-color: var(--baseFontColor);
+  display: inline-block;
+  position: relative;
+  vertical-align: middle;
+}
+
+.Select-clear-zone {
+  -webkit-animation: Select-animation-fadeIn 200ms;
+  -o-animation: Select-animation-fadeIn 200ms;
+  animation: Select-animation-fadeIn 200ms;
+  color: #999;
+  cursor: pointer;
+  display: table-cell;
+  position: relative;
+  text-align: center;
+  vertical-align: middle;
+  width: 16px;
+  height: 16px;
+  padding-right: 4px;
+}
+
+.Select-clear-zone:hover .Select-clear {
+  background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIHZpZXdCb3g9IjAgMCAxNCAxNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIiBzdHlsZT0iZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEuNDE0MjE7Ij4gICAgPGcgdHJhbnNmb3JtPSJtYXRyaXgoMC4wMjM0Mzc1LDAsMCwwLjAyMzQzNzUsLTUuMDE1NjIsLTUuMDE1NjIpIj4gICAgICAgIDxwYXRoIGQ9Ik04MTAsMjc0TDU3Miw1MTJMODEwLDc1MEw3NTAsODEwTDUxMiw1NzJMMjc0LDgxMEwyMTQsNzUwTDQ1Miw1MTJMMjE0LDI3NEwyNzQsMjE0TDUxMiw0NTJMNzUwLDIxNEw4MTAsMjc0WiIgc3R5bGU9ImZpbGw6cmdiKDIzMSwyMCw1Nik7ZmlsbC1ydWxlOm5vbnplcm87Ii8+ICAgIDwvZz48L3N2Zz4=);
+}
+
+.Select-clear {
+  display: block;
+  width: 9px;
+  height: 9px;
+  background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIHZpZXdCb3g9IjAgMCAxNCAxNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIiBzdHlsZT0iZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEuNDE0MjE7Ij4gICAgPGcgdHJhbnNmb3JtPSJtYXRyaXgoMC4wMjM0Mzc1LDAsMCwwLjAyMzQzNzUsLTUuMDE1NjIsLTUuMDE1NjIpIj4gICAgICAgIDxwYXRoIGQ9Ik04MTAsMjc0TDU3Miw1MTJMODEwLDc1MEw3NTAsODEwTDUxMiw1NzJMMjc0LDgxMEwyMTQsNzUwTDQ1Miw1MTJMMjE0LDI3NEwyNzQsMjE0TDUxMiw0NTJMNzUwLDIxNEw4MTAsMjc0WiIgc3R5bGU9ImZpbGw6cmdiKDE1MywxNTMsMTUzKTtmaWxsLXJ1bGU6bm9uemVybzsiLz4gICAgPC9nPjwvc3ZnPg==);
+  background-size: 9px 9px;
+  text-indent: -9999px;
+}
+
+.Select--multi .Select-clear-zone {
+  width: 17px;
+}
+
+.Select-arrow-zone {
+  cursor: pointer;
+  display: table-cell;
+  position: relative;
+  text-align: center;
+  vertical-align: middle;
+  width: 20px;
+  padding-right: 5px;
+}
+
+.Select-arrow {
+  border-color: #999 transparent transparent;
+  border-style: solid;
+  border-width: 4px 4px 2px;
+  display: inline-block;
+  height: 0;
+  width: 0;
+}
+
+.is-open .Select-arrow,
+.Select-arrow-zone:hover > .Select-arrow {
+  border-top-color: #666;
+}
+
+@-webkit-keyframes Select-animation-fadeIn {
+  from {
+    opacity: 0;
+  }
+
+  to {
+    opacity: 1;
+  }
+}
+
+@keyframes Select-animation-fadeIn {
+  from {
+    opacity: 0;
+  }
+
+  to {
+    opacity: 1;
+  }
+}
+
+.Select-menu-outer {
+  border-bottom-right-radius: 4px;
+  border-bottom-left-radius: 4px;
+  background-color: #fff;
+  border: 1px solid #ccc;
+  border-top-color: var(--barBorderColor);
+  box-sizing: border-box;
+  margin-top: -1px;
+  max-height: 200px;
+  position: absolute;
+  top: 100%;
+  width: 100%;
+  z-index: var(--dropdownMenuZIndex);
+  -webkit-overflow-scrolling: touch;
+  box-shadow: var(--defaultShadow);
+}
+
+.Select-menu {
+  max-height: 198px;
+  padding: 5px 0;
+  overflow-y: auto;
+}
+
+.Select-option {
+  display: block;
+  line-height: 20px;
+  padding: 0 8px;
+  box-sizing: border-box;
+  color: var(--baseFontColor);
+  font-size: var(--smallFontSize);
+  cursor: pointer;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.Select-option:last-child {
+  border-bottom-right-radius: 2px;
+  border-bottom-left-radius: 2px;
+}
+
+.Select-option.is-focused {
+  background-color: var(--barBackgroundColor);
+}
+
+.Select-option.is-disabled {
+  cursor: default;
+  opacity: 0.4;
+  font-style: italic;
+}
+
+.Select-noresults {
+  box-sizing: border-box;
+  color: var(--gray60);
+  cursor: default;
+  display: block;
+  padding: 8px 10px;
+}
+
+.Select--multi .Select-value {
+  background-color: rgba(0, 126, 255, 0.08);
+  border-radius: 2px;
+  border: 1px solid rgba(0, 126, 255, 0.24);
+  color: var(--baseFontColor);
+  display: inline-block;
+  font-size: var(--smallFontSize);
+  line-height: 14px;
+  margin: 1px 4px 1px 1px;
+  vertical-align: top;
+}
+
+.Select-value-label {
+  font-size: var(--smallFontSize);
+}
+
+.is-searchable.is-open .Select-value-label {
+  opacity: 0.5;
+}
+
+.Select-big .Select-control {
+  padding-top: 4px;
+  padding-bottom: 4px;
+}
+
+.Select-big .Select-placeholder {
+  margin-top: 4px;
+  margin-bottom: 4px;
+}
+
+.Select-big .Select-value-label {
+  display: inline-block;
+  margin-top: 7px;
+  line-height: 16px;
+}
+
+.Select-big .Select-option {
+  padding: 7px 8px;
+  line-height: 16px;
+}
+
+.Select-big img,
+.Select-big svg {
+  padding-top: 0;
+}
+
+.Select--multi .Select-value-icon,
+.Select--multi .Select-value-label {
+  display: inline-block;
+  vertical-align: middle;
+}
+
+.Select--multi .Select-value-label {
+  display: inline-block;
+  max-width: 200px;
+  border-bottom-right-radius: 2px;
+  border-top-right-radius: 2px;
+  cursor: default;
+  padding: 2px 5px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.Select--multi a.Select-value-label {
+  color: #007eff;
+  cursor: pointer;
+  text-decoration: none;
+}
+
+.Select--multi a.Select-value-label:hover {
+  text-decoration: underline;
+}
+
+.Select--multi .Select-value-icon {
+  cursor: pointer;
+  border-bottom-left-radius: 2px;
+  border-top-left-radius: 2px;
+  border-right: 1px solid rgba(0, 126, 255, 0.24);
+  padding: 1px 5px;
+}
+
+.Select--multi .Select-value-icon:hover,
+.Select--multi .Select-value-icon:focus {
+  background-color: rgba(0, 113, 230, 0.08);
+  color: #0071e6;
+}
+
+.Select--multi .Select-value-icon:active {
+  background-color: rgba(0, 126, 255, 0.24);
+}
+
+.Select--multi.is-disabled .Select-value {
+  background-color: #fcfcfc;
+  border: 1px solid #e3e3e3;
+  color: var(--baseFontColor);
+}
+
+.Select--multi.is-disabled .Select-value-icon {
+  cursor: not-allowed;
+  border-right: 1px solid #e3e3e3;
+}
+
+.Select--multi.is-disabled .Select-value-icon:hover,
+.Select--multi.is-disabled .Select-value-icon:focus,
+.Select--multi.is-disabled .Select-value-icon:active {
+  background-color: #fcfcfc;
+}
+
+.Select-aria-only {
+  display: none;
+}
+
+@keyframes Select-animation-spin {
+  to {
+    transform: rotate(1turn);
+  }
+}
+
+@-webkit-keyframes Select-animation-spin {
+  to {
+    -webkit-transform: rotate(1turn);
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/Select.tsx b/server/sonar-ui-common/components/controls/Select.tsx
new file mode 100644 (file)
index 0000000..be4c37b
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import {
+  defaultFilterOptions as reactSelectDefaultFilterOptions,
+  ReactAsyncSelectProps,
+  ReactCreatableSelectProps,
+  ReactSelectProps,
+} from 'react-select';
+import { lazyLoadComponent } from '../lazyLoadComponent';
+import { ClearButton } from './buttons';
+import './Select.css';
+
+declare module 'react-select' {
+  export function defaultFilterOptions(...args: any[]): any;
+}
+
+const ReactSelectLib = import('react-select');
+const ReactSelect = lazyLoadComponent(() => ReactSelectLib);
+const ReactCreatable = lazyLoadComponent(() =>
+  ReactSelectLib.then((lib) => ({ default: lib.Creatable }))
+);
+const ReactAsync = lazyLoadComponent(() => ReactSelectLib.then((lib) => ({ default: lib.Async })));
+
+function renderInput() {
+  return <ClearButton className="button-tiny spacer-left text-middle" iconProps={{ size: 12 }} />;
+}
+
+interface WithInnerRef {
+  innerRef?: (element: React.Component) => void;
+}
+
+export default function Select({ innerRef, ...props }: WithInnerRef & ReactSelectProps) {
+  // TODO try to define good defaults, if any
+  // ReactSelect doesn't declare `clearRenderer` prop
+  const ReactSelectAny = ReactSelect as any;
+  // hide the "x" icon when select is empty
+  const clearable = props.clearable ? Boolean(props.value) : false;
+  return (
+    <ReactSelectAny {...props} clearable={clearable} clearRenderer={renderInput} ref={innerRef} />
+  );
+}
+
+export const defaultFilterOptions = reactSelectDefaultFilterOptions;
+
+export function Creatable(props: ReactCreatableSelectProps) {
+  // ReactSelect doesn't declare `clearRenderer` prop
+  const ReactCreatableAny = ReactCreatable as any;
+  return <ReactCreatableAny {...props} clearRenderer={renderInput} />;
+}
+
+// TODO figure out why `ref` prop is incompatible
+export function AsyncSelect(props: ReactAsyncSelectProps & { ref?: any }) {
+  return <ReactAsync {...props} />;
+}
diff --git a/server/sonar-ui-common/components/controls/SelectList.css b/server/sonar-ui-common/components/controls/SelectList.css
new file mode 100644 (file)
index 0000000..4a2fcc1
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+.select-list-container {
+  min-width: 500px;
+  box-sizing: border-box;
+}
+
+.select-list-control {
+  margin-bottom: 10px;
+  box-sizing: border-box;
+}
+
+.select-list-list-container {
+  border: 1px solid #bfbfbf;
+  box-sizing: border-box;
+  height: 400px;
+  overflow: auto;
+}
+
+.select-list-list-checkbox {
+  display: flex !important;
+  align-items: center;
+}
+
+.select-list-list-checkbox i {
+  display: inline-block;
+  vertical-align: middle;
+  margin-right: 10px;
+}
+
+.select-list-list-disabled {
+  cursor: not-allowed;
+}
+
+.select-list-list-disabled > a {
+  pointer-events: none;
+}
+
+.select-list-list-item {
+  display: inline-block;
+  vertical-align: middle;
+}
diff --git a/server/sonar-ui-common/components/controls/SelectList.tsx b/server/sonar-ui-common/components/controls/SelectList.tsx
new file mode 100644 (file)
index 0000000..9baa6d8
--- /dev/null
@@ -0,0 +1,189 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import ListFooter from './ListFooter';
+import RadioToggle from './RadioToggle';
+import SearchBox from './SearchBox';
+import './SelectList.css';
+import SelectListListContainer from './SelectListListContainer';
+
+export enum SelectListFilter {
+  All = 'all',
+  Selected = 'selected',
+  Unselected = 'deselected',
+}
+
+interface Props {
+  allowBulkSelection?: boolean;
+  elements: string[];
+  elementsTotalCount?: number;
+  disabledElements?: string[];
+  labelSelected?: string;
+  labelUnselected?: string;
+  labelAll?: string;
+  needToReload?: boolean;
+  onSearch: (searchParams: SelectListSearchParams) => Promise<void>;
+  onSelect: (element: string) => Promise<void>;
+  onUnselect: (element: string) => Promise<void>;
+  pageSize?: number;
+  readOnly?: boolean;
+  renderElement: (element: string) => React.ReactNode;
+  selectedElements: string[];
+  withPaging?: boolean;
+}
+
+export interface SelectListSearchParams {
+  filter: SelectListFilter;
+  page?: number;
+  pageSize?: number;
+  query: string;
+}
+
+interface State {
+  lastSearchParams: SelectListSearchParams;
+  loading: boolean;
+}
+
+const DEFAULT_PAGE_SIZE = 100;
+
+export default class SelectList extends React.PureComponent<Props, State> {
+  mounted = false;
+
+  constructor(props: Props) {
+    super(props);
+
+    this.state = {
+      lastSearchParams: {
+        filter: SelectListFilter.Selected,
+        page: 1,
+        pageSize: props.pageSize ? props.pageSize : DEFAULT_PAGE_SIZE,
+        query: '',
+      },
+      loading: false,
+    };
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+    this.search({});
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  stopLoading = () => {
+    if (this.mounted) {
+      this.setState({ loading: false });
+    }
+  };
+
+  getFilter = () =>
+    this.state.lastSearchParams.query === ''
+      ? this.state.lastSearchParams.filter
+      : SelectListFilter.All;
+
+  search = (searchParams: Partial<SelectListSearchParams>) =>
+    this.setState(
+      (prevState) => ({
+        loading: true,
+        lastSearchParams: { ...prevState.lastSearchParams, ...searchParams },
+      }),
+      () =>
+        this.props
+          .onSearch({
+            filter: this.getFilter(),
+            page: this.props.withPaging ? this.state.lastSearchParams.page : undefined,
+            pageSize: this.props.withPaging ? this.state.lastSearchParams.pageSize : undefined,
+            query: this.state.lastSearchParams.query,
+          })
+          .then(this.stopLoading)
+          .catch(this.stopLoading)
+    );
+
+  changeFilter = (filter: SelectListFilter) => this.search({ filter, page: 1 });
+
+  handleQueryChange = (query: string) => this.search({ page: 1, query });
+
+  onLoadMore = () =>
+    this.search({
+      page:
+        this.state.lastSearchParams.page != null ? this.state.lastSearchParams.page + 1 : undefined,
+    });
+
+  onReload = () => this.search({ page: 1 });
+
+  render() {
+    const {
+      labelSelected = translate('selected'),
+      labelUnselected = translate('unselected'),
+      labelAll = translate('all'),
+    } = this.props;
+    const { filter } = this.state.lastSearchParams;
+
+    const disabled = this.state.lastSearchParams.query !== '';
+
+    return (
+      <div className="select-list">
+        <div className="display-flex-center">
+          <RadioToggle
+            className="select-list-filter spacer-right"
+            name="filter"
+            onCheck={this.changeFilter}
+            options={[
+              { disabled, label: labelSelected, value: SelectListFilter.Selected },
+              { disabled, label: labelUnselected, value: SelectListFilter.Unselected },
+              { disabled, label: labelAll, value: SelectListFilter.All },
+            ]}
+            value={filter}
+          />
+          <SearchBox
+            autoFocus={true}
+            loading={this.state.loading}
+            onChange={this.handleQueryChange}
+            placeholder={translate('search_verb')}
+            value={this.state.lastSearchParams.query}
+          />
+        </div>
+        <SelectListListContainer
+          allowBulkSelection={this.props.allowBulkSelection}
+          disabledElements={this.props.disabledElements || []}
+          elements={this.props.elements}
+          filter={this.getFilter()}
+          onSelect={this.props.onSelect}
+          onUnselect={this.props.onUnselect}
+          readOnly={this.props.readOnly}
+          renderElement={this.props.renderElement}
+          selectedElements={this.props.selectedElements}
+        />
+        {!!this.props.elementsTotalCount && (
+          <ListFooter
+            count={this.props.elements.length}
+            loadMore={this.onLoadMore}
+            needReload={this.props.needToReload}
+            reload={this.onReload}
+            total={this.props.elementsTotalCount}
+          />
+        )}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/SelectListListContainer.tsx b/server/sonar-ui-common/components/controls/SelectListListContainer.tsx
new file mode 100644 (file)
index 0000000..f0bb64a
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import DeferredSpinner from '../ui/DeferredSpinner';
+import Checkbox from './Checkbox';
+import { SelectListFilter } from './SelectList';
+import SelectListListElement from './SelectListListElement';
+
+interface Props {
+  allowBulkSelection?: boolean;
+  elements: string[];
+  disabledElements: string[];
+  filter: SelectListFilter;
+  onSelect: (element: string) => Promise<void>;
+  onUnselect: (element: string) => Promise<void>;
+  readOnly?: boolean;
+  renderElement: (element: string) => React.ReactNode;
+  selectedElements: string[];
+}
+
+interface State {
+  loading: boolean;
+}
+
+export default class SelectListListContainer extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { loading: false };
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  stopLoading = () => {
+    if (this.mounted) {
+      this.setState({ loading: false });
+    }
+  };
+
+  isDisabled = (element: string): boolean => {
+    return this.props.readOnly || this.props.disabledElements.includes(element);
+  };
+
+  isSelected = (element: string): boolean => {
+    return this.props.selectedElements.includes(element);
+  };
+
+  handleBulkChange = (checked: boolean) => {
+    this.setState({ loading: true });
+    if (checked) {
+      Promise.all(this.props.elements.map((element) => this.props.onSelect(element)))
+        .then(this.stopLoading)
+        .catch(this.stopLoading);
+    } else {
+      Promise.all(this.props.selectedElements.map((element) => this.props.onUnselect(element)))
+        .then(this.stopLoading)
+        .catch(this.stopLoading);
+    }
+  };
+
+  renderBulkSelector() {
+    const { elements, readOnly, selectedElements } = this.props;
+    return (
+      <>
+        <li>
+          <Checkbox
+            checked={selectedElements.length > 0}
+            disabled={this.state.loading || readOnly}
+            onCheck={this.handleBulkChange}
+            thirdState={selectedElements.length > 0 && elements.length !== selectedElements.length}>
+            <span className="big-spacer-left">
+              {translate('bulk_change')}
+              <DeferredSpinner className="spacer-left" loading={this.state.loading} timeout={10} />
+            </span>
+          </Checkbox>
+        </li>
+        <li className="divider" />
+      </>
+    );
+  }
+
+  render() {
+    const { allowBulkSelection, elements, filter } = this.props;
+
+    return (
+      <div className={classNames('select-list-list-container spacer-top')}>
+        <ul className="menu">
+          {allowBulkSelection &&
+            elements.length > 0 &&
+            filter === SelectListFilter.All &&
+            this.renderBulkSelector()}
+          {elements.map((element) => (
+            <SelectListListElement
+              disabled={this.isDisabled(element)}
+              element={element}
+              key={element}
+              onSelect={this.props.onSelect}
+              onUnselect={this.props.onUnselect}
+              renderElement={this.props.renderElement}
+              selected={this.isSelected(element)}
+            />
+          ))}
+        </ul>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/SelectListListElement.tsx b/server/sonar-ui-common/components/controls/SelectListListElement.tsx
new file mode 100644 (file)
index 0000000..bae8dbf
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import Checkbox from './Checkbox';
+
+interface Props {
+  active?: boolean;
+  disabled?: boolean;
+  element: string;
+  onSelect: (element: string) => Promise<void>;
+  onUnselect: (element: string) => Promise<void>;
+  renderElement: (element: string) => React.ReactNode;
+  selected: boolean;
+}
+
+interface State {
+  loading: boolean;
+}
+
+export default class SelectListListElement extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { loading: false };
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  stopLoading = () => {
+    if (this.mounted) {
+      this.setState({ loading: false });
+    }
+  };
+
+  handleCheck = (checked: boolean) => {
+    this.setState({ loading: true });
+    const request = checked ? this.props.onSelect : this.props.onUnselect;
+    request(this.props.element).then(this.stopLoading, this.stopLoading);
+  };
+
+  render() {
+    return (
+      <li className={classNames({ 'select-list-list-disabled': this.props.disabled })}>
+        <Checkbox
+          checked={this.props.selected}
+          className={classNames('select-list-list-checkbox', { active: this.props.active })}
+          disabled={this.props.disabled}
+          loading={this.state.loading}
+          onCheck={this.handleCheck}>
+          <span className="little-spacer-left">{this.props.renderElement(this.props.element)}</span>
+        </Checkbox>
+      </li>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/SimpleModal.tsx b/server/sonar-ui-common/components/controls/SimpleModal.tsx
new file mode 100644 (file)
index 0000000..99d368b
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Modal, { ModalProps } from './Modal';
+
+export interface ChildrenProps {
+  onCloseClick: (event?: React.SyntheticEvent<HTMLElement>) => void;
+  onFormSubmit: (event: React.SyntheticEvent<HTMLFormElement>) => void;
+  onSubmitClick: (event?: React.SyntheticEvent<HTMLElement>) => void;
+  submitting: boolean;
+}
+
+interface Props extends ModalProps {
+  children: (props: ChildrenProps) => React.ReactNode;
+  header: string;
+  onClose: () => void;
+  onSubmit: () => void | Promise<void | Response>;
+}
+
+interface State {
+  submitting: boolean;
+}
+
+export default class SimpleModal extends React.Component<Props, State> {
+  mounted = false;
+  state: State = { submitting: false };
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  stopSubmitting = () => {
+    if (this.mounted) {
+      this.setState({ submitting: false });
+    }
+  };
+
+  handleCloseClick = (event?: React.SyntheticEvent<HTMLElement>) => {
+    if (event) {
+      event.preventDefault();
+      event.currentTarget.blur();
+    }
+    this.props.onClose();
+  };
+
+  handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+    event.preventDefault();
+    this.submit();
+  };
+
+  handleSubmitClick = (event?: React.SyntheticEvent<HTMLElement>) => {
+    if (event) {
+      event.preventDefault();
+      event.currentTarget.blur();
+    }
+    this.submit();
+  };
+
+  submit = () => {
+    const result = this.props.onSubmit();
+    if (result) {
+      this.setState({ submitting: true });
+      result.then(this.stopSubmitting, this.stopSubmitting);
+    }
+  };
+
+  render() {
+    const { children, header, onClose, onSubmit, ...modalProps } = this.props;
+    return (
+      <Modal contentLabel={header} onRequestClose={onClose} {...modalProps}>
+        {children({
+          onCloseClick: this.handleCloseClick,
+          onFormSubmit: this.handleFormSubmit,
+          onSubmitClick: this.handleSubmitClick,
+          submitting: this.state.submitting,
+        })}
+      </Modal>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/Tabs.css b/server/sonar-ui-common/components/controls/Tabs.css
new file mode 100644 (file)
index 0000000..13dc927
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.flex-tabs {
+  display: flex;
+  clear: left;
+  margin-bottom: calc(3 * var(--gridSize));
+  border-bottom: 1px solid var(--barBorderColor);
+  font-size: var(--mediumFontSize);
+}
+
+.flex-tabs > li > a {
+  position: relative;
+  display: block;
+  top: 1px;
+  height: 100%;
+  width: 100%;
+  box-sizing: border-box;
+  color: var(--secondFontColor);
+  font-weight: 600;
+  cursor: pointer;
+  padding-bottom: calc(1.5 * var(--gridSize));
+  border-bottom: 3px solid transparent;
+  transition: color 0.2s ease;
+}
+
+.flex-tabs > li ~ li {
+  margin-left: calc(4 * var(--gridSize));
+}
+
+.flex-tabs > li > a:hover {
+  color: var(--baseFontColor);
+}
+
+.flex-tabs > li > a.selected {
+  color: var(--blue);
+  border-bottom-color: var(--blue);
+}
+
+.flex-tabs > li > a.disabled {
+  color: var(--disableGrayText) !important;
+  cursor: not-allowed !important;
+  pointer-events: none !important;
+}
diff --git a/server/sonar-ui-common/components/controls/Tabs.tsx b/server/sonar-ui-common/components/controls/Tabs.tsx
new file mode 100644 (file)
index 0000000..7fb2ef7
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import './Tabs.css';
+
+interface Props<T extends string> {
+  onChange: (tab: T) => void;
+  selected?: T;
+  tabs: Array<{ disabled?: boolean; key: T; node: React.ReactNode }>;
+}
+
+export default function Tabs<T extends string>({ onChange, selected, tabs }: Props<T>) {
+  return (
+    <ul className="flex-tabs">
+      {tabs.map((tab) => (
+        <Tab
+          disabled={tab.disabled}
+          key={tab.key}
+          name={tab.key}
+          onSelect={onChange}
+          selected={selected === tab.key}>
+          {tab.node}
+        </Tab>
+      ))}
+    </ul>
+  );
+}
+
+interface TabProps<T> {
+  children: React.ReactNode;
+  disabled?: boolean;
+  name: T;
+  onSelect: (tab: T) => void;
+  selected: boolean;
+}
+
+export class Tab<T> extends React.PureComponent<TabProps<T>> {
+  handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.stopPropagation();
+    if (!this.props.disabled) {
+      this.props.onSelect(this.props.name);
+    }
+  };
+
+  render() {
+    const { children, disabled, name, selected } = this.props;
+    return (
+      <li>
+        <a
+          className={classNames('js-' + name, { disabled, selected })}
+          href="#"
+          onClick={this.handleClick}>
+          {children}
+        </a>
+      </li>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/Toggle.css b/server/sonar-ui-common/components/controls/Toggle.css
new file mode 100644 (file)
index 0000000..1d14e11
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.button.boolean-toggle {
+  display: inline-block;
+  vertical-align: middle;
+  width: 48px;
+  height: var(--controlHeight);
+  padding: 1px;
+  border: 1px solid var(--gray80);
+  border-radius: var(--controlHeight);
+  box-sizing: border-box;
+  background-color: #fff;
+  cursor: pointer;
+  transition: all 0.3s ease;
+}
+
+.button.boolean-toggle:hover {
+  background-color: #fff;
+}
+
+.button.boolean-toggle:focus {
+  border-color: var(--blue);
+  background-color: #f6f6f6;
+}
+
+.boolean-toggle-handle {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 20px;
+  height: 20px;
+  border: 1px solid var(--gray80);
+  border-radius: 22px;
+  box-sizing: border-box;
+  background-color: #f6f6f6;
+  transition: transform 0.3s cubic-bezier(0.87, -0.41, 0.19, 1.44), border 0.3s ease;
+}
+
+.boolean-toggle-handle > * {
+  opacity: 0;
+  transition: opacity 0.3s ease;
+}
+
+.button.boolean-toggle-on {
+  border-color: var(--darkBlue);
+  background-color: var(--darkBlue);
+  color: var(--darkBlue);
+}
+
+.button.boolean-toggle-on:hover {
+  background-color: var(--darkBlue);
+}
+
+.button.boolean-toggle-on:focus {
+  background-color: var(--darkBlue);
+}
+
+.button.boolean-toggle-on .boolean-toggle-handle {
+  border-color: #f6f6f6;
+  transform: translateX(var(--controlHeight));
+}
+
+.button.boolean-toggle-on .boolean-toggle-handle > * {
+  opacity: 1;
+}
diff --git a/server/sonar-ui-common/components/controls/Toggle.tsx b/server/sonar-ui-common/components/controls/Toggle.tsx
new file mode 100644 (file)
index 0000000..62a7570
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import CheckIcon from '../icons/CheckIcon';
+import { Button } from './buttons';
+import './Toggle.css';
+
+interface Props {
+  disabled?: boolean;
+  name?: string;
+  onChange?: (value: boolean) => void;
+  value: boolean | string;
+}
+
+export default class Toggle extends React.PureComponent<Props> {
+  getValue = () => {
+    const { value } = this.props;
+    return typeof value === 'string' ? value === 'true' : value;
+  };
+
+  handleClick = () => {
+    if (this.props.onChange) {
+      const value = this.getValue();
+      this.props.onChange(!value);
+    }
+  };
+
+  render() {
+    const { disabled, name } = this.props;
+    const value = this.getValue();
+    const className = classNames('boolean-toggle', { 'boolean-toggle-on': value });
+
+    return (
+      <Button className={className} disabled={disabled} name={name} onClick={this.handleClick}>
+        <div aria-label={translate(value ? 'on' : 'off')} className="boolean-toggle-handle">
+          <CheckIcon size={12} />
+        </div>
+      </Button>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/Toggler.tsx b/server/sonar-ui-common/components/controls/Toggler.tsx
new file mode 100644 (file)
index 0000000..2545716
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import DocumentClickHandler from './DocumentClickHandler';
+import EscKeydownHandler from './EscKeydownHandler';
+import OutsideClickHandler from './OutsideClickHandler';
+
+interface Props {
+  children?: React.ReactNode;
+  closeOnClick?: boolean;
+  closeOnClickOutside?: boolean;
+  closeOnEscape?: boolean;
+  onRequestClose: () => void;
+  open: boolean;
+  overlay: React.ReactNode;
+}
+
+export default class Toggler extends React.Component<Props> {
+  renderOverlay() {
+    const {
+      closeOnClick = false,
+      closeOnClickOutside = true,
+      closeOnEscape = true,
+      onRequestClose,
+      overlay,
+    } = this.props;
+
+    let renderedOverlay;
+    if (closeOnEscape) {
+      renderedOverlay = <EscKeydownHandler onKeydown={onRequestClose}>{overlay}</EscKeydownHandler>;
+    } else {
+      renderedOverlay = overlay;
+    }
+
+    if (closeOnClick) {
+      return (
+        <DocumentClickHandler onClick={onRequestClose}>{renderedOverlay}</DocumentClickHandler>
+      );
+    } else if (closeOnClickOutside) {
+      return (
+        <OutsideClickHandler onClickOutside={onRequestClose}>{renderedOverlay}</OutsideClickHandler>
+      );
+    } else {
+      return renderedOverlay;
+    }
+  }
+
+  render() {
+    return (
+      <>
+        {this.props.children}
+        {this.props.open && this.renderOverlay()}
+      </>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/Tooltip.css b/server/sonar-ui-common/components/controls/Tooltip.css
new file mode 100644 (file)
index 0000000..40eaf11
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.tooltip {
+  position: absolute;
+  z-index: var(--tooltipZIndex);
+  display: block;
+  height: auto;
+  box-sizing: border-box;
+  font-size: var(--baseFontSize);
+  font-weight: 300;
+  line-height: 1.5;
+  animation: fadeIn 0.3s forwards;
+}
+
+.tooltip.top {
+  padding: 5px 0;
+  margin-top: -3px;
+}
+
+.tooltip.right {
+  padding: 0 5px;
+  margin-left: 3px;
+}
+
+.tooltip.bottom {
+  padding: 5px 0;
+  margin-top: 3px;
+}
+
+.tooltip.left {
+  padding: 0 5px;
+  margin-left: -3px;
+}
+
+.tooltip-inner {
+  max-width: 300px;
+  text-align: left;
+  text-decoration: none;
+  border-radius: 4px;
+  overflow: hidden;
+  word-break: break-word;
+}
+
+.tooltip-inner {
+  padding: 12px 17px;
+  color: #fff;
+  background-color: #475760;
+  letter-spacing: 0.04em;
+}
+
+.tooltip-inner .alert {
+  margin-bottom: 5px;
+  border-radius: 4px;
+}
+
+.tooltip-inner a {
+  border-bottom-color: #8da6b3;
+  color: #a5d0ea;
+}
+
+.tooltip-inner hr {
+  background-color: #5d6d75;
+}
+
+.tooltip-arrow {
+  position: absolute;
+  width: 0;
+  height: 0;
+  border: solid transparent;
+}
+
+.tooltip.top .tooltip-arrow {
+  bottom: 0;
+  left: 50%;
+  border-width: 5px 5px 0;
+  transform: translateX(-5px);
+  border-top-color: #475760;
+}
+
+.tooltip.right .tooltip-arrow {
+  top: 50%;
+  left: 0;
+  transform: translateY(-5px);
+  border-width: 5px 5px 5px 0;
+  border-right-color: #475760;
+}
+
+.tooltip.left .tooltip-arrow {
+  top: 50%;
+  right: 0;
+  transform: translateY(-5px);
+  border-width: 5px 0 5px 5px;
+  border-left-color: #475760;
+}
+
+.tooltip.bottom .tooltip-arrow {
+  top: 0;
+  left: 50%;
+  transform: translateX(-5px);
+  border-width: 0 5px 5px;
+  border-bottom-color: #475760;
+}
+
+/* Workaround for react issue with onMouseLeave in disabled buttons: https://github.com/facebook/react/issues/4251 */
+.tooltip button[disabled] {
+  pointer-events: none;
+}
+
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+  }
+
+  to {
+    opacity: 1;
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/Tooltip.tsx b/server/sonar-ui-common/components/controls/Tooltip.tsx
new file mode 100644 (file)
index 0000000..8ac9f87
--- /dev/null
@@ -0,0 +1,407 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { throttle } from 'lodash';
+import * as React from 'react';
+import { createPortal, findDOMNode } from 'react-dom';
+import ThemeContext from '../theme';
+import ScreenPositionFixer from './ScreenPositionFixer';
+import './Tooltip.css';
+
+export type Placement = 'bottom' | 'right' | 'left' | 'top';
+
+export interface TooltipProps {
+  classNameSpace?: string;
+  children: React.ReactElement<{}>;
+  mouseEnterDelay?: number;
+  mouseLeaveDelay?: number;
+  onShow?: () => void;
+  onHide?: () => void;
+  overlay: React.ReactNode;
+  placement?: Placement;
+  visible?: boolean;
+}
+
+interface Measurements {
+  height: number;
+  left: number;
+  top: number;
+  width: number;
+}
+
+interface OwnState {
+  flipped: boolean;
+  placement?: Placement;
+  visible: boolean;
+}
+
+type State = OwnState & Partial<Measurements>;
+
+const FLIP_MAP: { [key in Placement]: Placement } = {
+  left: 'right',
+  right: 'left',
+  top: 'bottom',
+  bottom: 'top',
+};
+
+function isMeasured(state: State): state is OwnState & Measurements {
+  return state.height !== undefined;
+}
+
+export default function Tooltip(props: TooltipProps) {
+  // overlay is a ReactNode, so it can be `undefined` or `null`
+  // this allows to easily render a tooltip conditionally
+  // more generaly we avoid rendering empty tooltips
+  return props.overlay != null && props.overlay !== '' ? (
+    <TooltipInner {...props} />
+  ) : (
+    props.children
+  );
+}
+
+export class TooltipInner extends React.Component<TooltipProps, State> {
+  throttledPositionTooltip: () => void;
+  mouseEnterTimeout?: number;
+  mouseLeaveTimeout?: number;
+  tooltipNode?: HTMLElement | null;
+  mounted = false;
+  mouseIn = false;
+
+  static defaultProps = {
+    mouseEnterDelay: 0.1,
+  };
+
+  constructor(props: TooltipProps) {
+    super(props);
+    this.state = {
+      flipped: false,
+      placement: props.placement,
+      visible: props.visible !== undefined ? props.visible : false,
+    };
+    this.throttledPositionTooltip = throttle(this.positionTooltip, 10);
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+    if (this.props.visible === true) {
+      this.positionTooltip();
+      this.addEventListeners();
+    }
+  }
+
+  componentDidUpdate(prevProps: TooltipProps, prevState: State) {
+    if (this.props.placement !== prevProps.placement) {
+      this.setState({ placement: this.props.placement });
+      // Break. This will trigger a new componentDidUpdate() call, so the below
+      // positionTooltip() call will be correct. Otherwise, it might not use
+      // the new state.placement value.
+      return;
+    }
+
+    if (
+      // opens
+      (this.props.visible === true && !prevProps.visible) ||
+      (this.props.visible === undefined &&
+        this.state.visible === true &&
+        prevState.visible === false)
+    ) {
+      this.positionTooltip();
+      this.addEventListeners();
+    } else if (
+      // closes
+      (!this.props.visible && prevProps.visible === true) ||
+      (this.props.visible === undefined &&
+        this.state.visible === false &&
+        prevState.visible === true)
+    ) {
+      this.clearPosition();
+      this.removeEventListeners();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+    this.removeEventListeners();
+    this.clearTimeouts();
+  }
+
+  static contextType = ThemeContext;
+
+  addEventListeners = () => {
+    window.addEventListener('resize', this.throttledPositionTooltip);
+    window.addEventListener('scroll', this.throttledPositionTooltip);
+  };
+
+  removeEventListeners = () => {
+    window.removeEventListener('resize', this.throttledPositionTooltip);
+    window.removeEventListener('scroll', this.throttledPositionTooltip);
+  };
+
+  clearTimeouts = () => {
+    window.clearTimeout(this.mouseEnterTimeout);
+    window.clearTimeout(this.mouseLeaveTimeout);
+  };
+
+  isVisible = () => {
+    return this.props.visible !== undefined ? this.props.visible : this.state.visible;
+  };
+
+  getPlacement = (): Placement => {
+    return this.state.placement || 'bottom';
+  };
+
+  tooltipNodeRef = (node: HTMLElement | null) => {
+    this.tooltipNode = node;
+  };
+
+  adjustArrowPosition = (
+    placement: Placement,
+    { leftFix, topFix }: { leftFix: number; topFix: number }
+  ) => {
+    switch (placement) {
+      case 'left':
+      case 'right':
+        return { marginTop: -topFix };
+      default:
+        return { marginLeft: -leftFix };
+    }
+  };
+
+  positionTooltip = () => {
+    // `findDOMNode(this)` will search for the DOM node for the current component
+    // first it will find a React.Fragment (see `render`),
+    // so it will get the DOM node of the first child, i.e. DOM node of `this.props.children`
+    // docs: https://reactjs.org/docs/refs-and-the-dom.html#exposing-dom-refs-to-parent-components
+
+    // eslint-disable-next-line react/no-find-dom-node
+    const toggleNode = findDOMNode(this);
+
+    if (toggleNode && toggleNode instanceof Element && this.tooltipNode) {
+      const toggleRect = toggleNode.getBoundingClientRect();
+      const tooltipRect = this.tooltipNode.getBoundingClientRect();
+      const { width, height } = tooltipRect;
+
+      let left = 0;
+      let top = 0;
+
+      switch (this.getPlacement()) {
+        case 'bottom':
+          left = toggleRect.left + toggleRect.width / 2 - width / 2;
+          top = toggleRect.top + toggleRect.height;
+          break;
+        case 'top':
+          left = toggleRect.left + toggleRect.width / 2 - width / 2;
+          top = toggleRect.top - height;
+          break;
+        case 'right':
+          left = toggleRect.left + toggleRect.width;
+          top = toggleRect.top + toggleRect.height / 2 - height / 2;
+          break;
+        case 'left':
+          left = toggleRect.left - width;
+          top = toggleRect.top + toggleRect.height / 2 - height / 2;
+          break;
+      }
+
+      // save width and height (and later set in `render`) to avoid resizing the tooltip element,
+      // when it's placed close to the window edge
+      this.setState({
+        left: window.pageXOffset + left,
+        top: window.pageYOffset + top,
+        width,
+        height,
+      });
+    }
+  };
+
+  clearPosition = () => {
+    this.setState({
+      flipped: false,
+      left: undefined,
+      top: undefined,
+      width: undefined,
+      height: undefined,
+      placement: this.props.placement,
+    });
+  };
+
+  handleMouseEnter = () => {
+    this.mouseEnterTimeout = window.setTimeout(() => {
+      // for some reason even after the `this.mouseEnterTimeout` is cleared, it still triggers
+      // to workaround this issue, check that its value is not `undefined`
+      // (if it's `undefined`, it means the timer has been reset)
+      if (
+        this.mounted &&
+        this.props.visible === undefined &&
+        this.mouseEnterTimeout !== undefined
+      ) {
+        this.setState({ visible: true });
+      }
+    }, (this.props.mouseEnterDelay || 0) * 1000);
+
+    if (this.props.onShow) {
+      this.props.onShow();
+    }
+  };
+
+  handleMouseLeave = () => {
+    if (this.mouseEnterTimeout !== undefined) {
+      window.clearTimeout(this.mouseEnterTimeout);
+      this.mouseEnterTimeout = undefined;
+    }
+
+    if (!this.mouseIn) {
+      this.mouseLeaveTimeout = window.setTimeout(() => {
+        if (this.mounted && this.props.visible === undefined && !this.mouseIn) {
+          this.setState({ visible: false });
+        }
+      }, (this.props.mouseLeaveDelay || 0) * 1000);
+
+      if (this.props.onHide) {
+        this.props.onHide();
+      }
+    }
+  };
+
+  handleOverlayMouseEnter = () => {
+    this.mouseIn = true;
+  };
+
+  handleOverlayMouseLeave = () => {
+    this.mouseIn = false;
+    this.handleMouseLeave();
+  };
+
+  needsFlipping = (leftFix: number, topFix: number) => {
+    // We can live with a tooltip that's slightly positioned over the toggle
+    // node. Only trigger if it really starts overlapping, as the re-positioning
+    // is quite expensive, needing 2 re-renders.
+    const threshold = this.context.rawSizes.grid;
+    switch (this.getPlacement()) {
+      case 'left':
+      case 'right':
+        return Math.abs(leftFix) > threshold;
+      case 'top':
+      case 'bottom':
+        return Math.abs(topFix) > threshold;
+    }
+    return false;
+  };
+
+  renderActual = ({ leftFix = 0, topFix = 0 }) => {
+    if (
+      !this.state.flipped &&
+      (leftFix !== 0 || topFix !== 0) &&
+      this.needsFlipping(leftFix, topFix)
+    ) {
+      // Update state in a render function... Not a good idea, but we need to
+      // render in order to know if we need to flip... To prevent React from
+      // complaining, we update the state using a setTimeout() call.
+      setTimeout(() => {
+        this.setState(
+          ({ placement = 'bottom' }) => ({
+            flipped: true,
+            // Set height to undefined to force ScreenPositionFixer to
+            // re-compute our positioning.
+            height: undefined,
+            placement: FLIP_MAP[placement],
+          }),
+          () => {
+            if (this.state.visible) {
+              // Force a re-positioning, as "only" updating the state doesn't
+              // recompute the position, only re-renders with the previous
+              // position (which is no longer correct).
+              this.positionTooltip();
+            }
+          }
+        );
+      }, 1);
+      return null;
+    }
+
+    const { classNameSpace = 'tooltip' } = this.props;
+    const placement = this.getPlacement();
+    const style = isMeasured(this.state)
+      ? {
+          left: this.state.left + leftFix,
+          top: this.state.top + topFix,
+          width: this.state.width,
+          height: this.state.height,
+        }
+      : undefined;
+
+    return (
+      <div
+        className={`${classNameSpace} ${placement}`}
+        onMouseEnter={this.handleOverlayMouseEnter}
+        onMouseLeave={this.handleOverlayMouseLeave}
+        ref={this.tooltipNodeRef}
+        style={style}>
+        <div className={`${classNameSpace}-inner`}>{this.props.overlay}</div>
+        <div
+          className={`${classNameSpace}-arrow`}
+          style={
+            isMeasured(this.state)
+              ? this.adjustArrowPosition(placement, { leftFix, topFix })
+              : undefined
+          }
+        />
+      </div>
+    );
+  };
+
+  render() {
+    return (
+      <>
+        {React.cloneElement(this.props.children, {
+          onMouseEnter: this.handleMouseEnter,
+          onMouseLeave: this.handleMouseLeave,
+        })}
+        {this.isVisible() && (
+          <TooltipPortal>
+            <ScreenPositionFixer ready={isMeasured(this.state)}>
+              {this.renderActual}
+            </ScreenPositionFixer>
+          </TooltipPortal>
+        )}
+      </>
+    );
+  }
+}
+
+class TooltipPortal extends React.Component {
+  el: HTMLElement;
+
+  constructor(props: {}) {
+    super(props);
+    this.el = document.createElement('div');
+  }
+
+  componentDidMount() {
+    document.body.appendChild(this.el);
+  }
+
+  componentWillUnmount() {
+    document.body.removeChild(this.el);
+  }
+
+  render() {
+    return createPortal(this.props.children, this.el);
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/ValidationForm.tsx b/server/sonar-ui-common/components/controls/ValidationForm.tsx
new file mode 100644 (file)
index 0000000..0e2bcd0
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { Formik, FormikActions, FormikProps } from 'formik';
+import * as React from 'react';
+
+export type ChildrenProps<V> = T.Omit<FormikProps<V>, 'handleSubmit'>;
+
+interface Props<V> {
+  children: (props: ChildrenProps<V>) => React.ReactNode;
+  initialValues: V;
+  isInitialValid?: boolean;
+  onSubmit: (data: V) => Promise<void>;
+  validate: (data: V) => { [P in keyof V]?: string } | Promise<{ [P in keyof V]?: string }>;
+}
+
+export default class ValidationForm<V> extends React.Component<Props<V>> {
+  mounted = false;
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  handleSubmit = (data: V, { setSubmitting }: FormikActions<V>) => {
+    const result = this.props.onSubmit(data);
+    const stopSubmitting = () => {
+      if (this.mounted) {
+        setSubmitting(false);
+      }
+    };
+
+    if (result) {
+      result.then(stopSubmitting, stopSubmitting);
+    } else {
+      stopSubmitting();
+    }
+  };
+
+  render() {
+    return (
+      <Formik<V>
+        initialValues={this.props.initialValues}
+        isInitialValid={this.props.isInitialValid}
+        onSubmit={this.handleSubmit}
+        validate={this.props.validate}>
+        {({ handleSubmit, ...props }) => (
+          <form onSubmit={handleSubmit}>{this.props.children(props)}</form>
+        )}
+      </Formik>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/ValidationInput.tsx b/server/sonar-ui-common/components/controls/ValidationInput.tsx
new file mode 100644 (file)
index 0000000..a8010ae
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import AlertErrorIcon from '../icons/AlertErrorIcon';
+import AlertSuccessIcon from '../icons/AlertSuccessIcon';
+import MandatoryFieldMarker from '../ui/MandatoryFieldMarker';
+import HelpTooltip from './HelpTooltip';
+
+interface Props {
+  description?: React.ReactNode;
+  children: React.ReactNode;
+  className?: string;
+  error: string | undefined;
+  help?: string;
+  id: string;
+  isInvalid: boolean;
+  isValid: boolean;
+  label: React.ReactNode;
+  required?: boolean;
+}
+
+export default function ValidationInput(props: Props) {
+  const hasError = props.isInvalid && props.error !== undefined;
+  return (
+    <div className={props.className}>
+      <label htmlFor={props.id}>
+        <span className="text-middle">
+          <strong>{props.label}</strong>
+          {props.required && <MandatoryFieldMarker />}
+        </span>
+        {props.help && <HelpTooltip className="spacer-left" overlay={props.help} />}
+      </label>
+      <div className="little-spacer-top spacer-bottom">
+        {props.children}
+        {props.isInvalid && <AlertErrorIcon className="spacer-left text-middle" />}
+        {hasError && (
+          <span className="little-spacer-left text-danger text-middle">{props.error}</span>
+        )}
+        {props.isValid && <AlertSuccessIcon className="spacer-left text-middle" />}
+      </div>
+      {props.description && <div className="note abs-width-400">{props.description}</div>}
+    </div>
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/ValidationModal.tsx b/server/sonar-ui-common/components/controls/ValidationModal.tsx
new file mode 100644 (file)
index 0000000..cca7c0f
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import DeferredSpinner from '../ui/DeferredSpinner';
+import { ResetButtonLink, SubmitButton } from './buttons';
+import Modal, { ModalProps } from './Modal';
+import ValidationForm, { ChildrenProps } from './ValidationForm';
+
+interface Props<V> extends ModalProps {
+  children: (props: ChildrenProps<V>) => React.ReactNode;
+  confirmButtonText: string;
+  header: string;
+  initialValues: V;
+  isDestructive?: boolean;
+  isInitialValid?: boolean;
+  onClose: () => void;
+  onSubmit: (data: V) => Promise<void>;
+  validate: (data: V) => { [P in keyof V]?: string };
+}
+
+export default class ValidationModal<V> extends React.PureComponent<Props<V>> {
+  handleSubmit = (data: V) => {
+    return this.props.onSubmit(data).then(() => {
+      this.props.onClose();
+    });
+  };
+
+  render() {
+    return (
+      <Modal
+        contentLabel={this.props.header}
+        noBackdrop={this.props.noBackdrop}
+        onRequestClose={this.props.onClose}
+        size={this.props.size}>
+        <ValidationForm
+          initialValues={this.props.initialValues}
+          isInitialValid={this.props.isInitialValid}
+          onSubmit={this.handleSubmit}
+          validate={this.props.validate}>
+          {(props) => (
+            <>
+              <header className="modal-head">
+                <h2>{this.props.header}</h2>
+              </header>
+
+              <div className="modal-body">{this.props.children(props)}</div>
+
+              <footer className="modal-foot">
+                <DeferredSpinner className="spacer-right" loading={props.isSubmitting} />
+                <SubmitButton
+                  className={this.props.isDestructive ? 'button-red' : undefined}
+                  disabled={props.isSubmitting || !props.isValid || !props.dirty}>
+                  {this.props.confirmButtonText}
+                </SubmitButton>
+                <ResetButtonLink disabled={props.isSubmitting} onClick={this.props.onClose}>
+                  {translate('cancel')}
+                </ResetButtonLink>
+              </footer>
+            </>
+          )}
+        </ValidationForm>
+      </Modal>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/ActionsDropdown-test.tsx b/server/sonar-ui-common/components/controls/__tests__/ActionsDropdown-test.tsx
new file mode 100644 (file)
index 0000000..93c5a8d
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+/* eslint-disable sonarjs/no-duplicate-string */
+import { mount, shallow } from 'enzyme';
+import * as React from 'react';
+import { click } from '../../../helpers/testUtils';
+import { PopupPlacement } from '../../ui/popups';
+import ActionsDropdown, {
+  ActionsDropdownDivider,
+  ActionsDropdownItem,
+  ActionsDropdownProps,
+} from '../ActionsDropdown';
+
+describe('ActionsDropdown', () => {
+  it('should render correctly', () => {
+    expect(shallowRender()).toMatchSnapshot();
+    expect(shallowRender({ small: false })).toMatchSnapshot();
+  });
+
+  function shallowRender(props: Partial<ActionsDropdownProps> = {}) {
+    return shallow(
+      <ActionsDropdown
+        className="foo"
+        onOpen={jest.fn()}
+        overlayPlacement={PopupPlacement.Bottom}
+        small={true}
+        toggleClassName="bar"
+        {...props}>
+        <span>Hello world</span>
+      </ActionsDropdown>
+    );
+  }
+});
+
+describe('ActionsDropdownItem', () => {
+  it('should render correctly', () => {
+    expect(shallowRender()).toMatchSnapshot();
+    expect(shallowRender({ destructive: true, id: 'baz', to: 'path/name' })).toMatchSnapshot();
+    expect(shallowRender({ download: 'foo/bar', to: 'path/name' })).toMatchSnapshot();
+  });
+
+  it('should trigger click', () => {
+    const onClick = jest.fn();
+    const wrapper = shallowRender({ onClick });
+    click(wrapper.find('a'));
+    expect(onClick).toBeCalled();
+  });
+
+  it('should render correctly copy item', () => {
+    const wrapper = mountRender({ copyValue: 'my content to copy to clipboard' });
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  function shallowRender(props: Partial<ActionsDropdownItem['props']> = {}) {
+    return shallow(renderContent(props));
+  }
+
+  function mountRender(props: Partial<ActionsDropdownItem['props']> = {}) {
+    return mount(renderContent(props));
+  }
+
+  function renderContent(props: Partial<ActionsDropdownItem['props']> = {}) {
+    return (
+      <ActionsDropdownItem className="foo" {...props}>
+        <span>Hello world</span>
+      </ActionsDropdownItem>
+    );
+  }
+});
+
+describe('ActionsDropdownDivider', () => {
+  it('should render correctly', () => {
+    expect(shallow(<ActionsDropdownDivider />)).toMatchSnapshot();
+  });
+});
diff --git a/server/sonar-ui-common/components/controls/__tests__/BackButton-test.tsx b/server/sonar-ui-common/components/controls/__tests__/BackButton-test.tsx
new file mode 100644 (file)
index 0000000..77ad09c
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import testTheme from '../../../config/jest/testTheme';
+import { click } from '../../../helpers/testUtils';
+import { ThemeProvider } from '../../theme';
+import BackButton from '../BackButton';
+
+it('should render properly', () => {
+  const wrapper = shallowRender();
+  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.find('ContextConsumer').dive()).toMatchSnapshot();
+});
+
+it('should handle click', () => {
+  const onClick = jest.fn();
+  const wrapper = shallowRender({ onClick });
+  expect(wrapper).toMatchSnapshot();
+  click(wrapper.find('a'));
+  expect(onClick).toBeCalled();
+});
+
+function shallowRender(props: Partial<BackButton['props']> = {}) {
+  return shallow<BackButton>(<BackButton onClick={jest.fn()} {...props} />, {
+    wrappingComponent: ThemeProvider,
+    wrappingComponentProps: {
+      theme: testTheme,
+    },
+  });
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/BoxedGroupAccordion-test.tsx b/server/sonar-ui-common/components/controls/__tests__/BoxedGroupAccordion-test.tsx
new file mode 100644 (file)
index 0000000..7feece1
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { click } from '../../../helpers/testUtils';
+import BoxedGroupAccordion from '../BoxedGroupAccordion';
+
+it('should render correctly', () => {
+  expect(getWrapper()).toMatchSnapshot();
+});
+
+it('should show the inner content after a click', () => {
+  const onClick = jest.fn();
+  const wrapper = getWrapper({ onClick });
+  click(wrapper.find('.boxed-group-header'));
+
+  expect(onClick).lastCalledWith('foo');
+  wrapper.setProps({ open: true });
+
+  expect(wrapper.find('.boxed-group-inner').exists()).toBe(true);
+});
+
+function getWrapper(props = {}) {
+  return shallow(
+    <BoxedGroupAccordion
+      data="foo"
+      onClick={() => {}}
+      open={false}
+      renderHeader={() => <div>header content</div>}
+      title="Foo"
+      {...props}>
+      <div>inner content</div>
+    </BoxedGroupAccordion>
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/BoxedTabs-test.tsx b/server/sonar-ui-common/components/controls/__tests__/BoxedTabs-test.tsx
new file mode 100644 (file)
index 0000000..d597369
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { mount, shallow } from 'enzyme';
+import * as React from 'react';
+import BoxedTabs, { BoxedTabsProps } from '../BoxedTabs';
+
+it('should render correctly', () => {
+  expect(mountRender()).toMatchSnapshot();
+});
+
+it('should call onSelect when a tab is clicked', () => {
+  const onSelect = jest.fn();
+  const wrapper = shallowRender({ onSelect });
+
+  wrapper.find('Styled(button)').get(1).props.onClick();
+
+  expect(onSelect).toHaveBeenCalledWith('b');
+});
+
+function shallowRender(overrides: Partial<BoxedTabsProps<string>> = {}) {
+  return shallow(dom(overrides));
+}
+
+function mountRender(overrides: Partial<BoxedTabsProps<string>> = {}) {
+  return mount(dom(overrides));
+}
+
+function dom(overrides) {
+  return (
+    <BoxedTabs
+      className="boxed-tabs"
+      onSelect={jest.fn()}
+      selected="a"
+      tabs={[
+        { key: 'a', label: 'labela' },
+        { key: 'b', label: 'labelb' },
+        {
+          key: 'c',
+          label: (
+            <span>
+              Complex label <strong>!!!</strong>
+            </span>
+          ),
+        },
+      ]}
+      {...overrides}
+    />
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/Checkbox-test.tsx b/server/sonar-ui-common/components/controls/__tests__/Checkbox-test.tsx
new file mode 100644 (file)
index 0000000..e05e0c8
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { click } from '../../../helpers/testUtils';
+import Checkbox from '../Checkbox';
+
+it('should render', () => {
+  const checkbox = shallow(<Checkbox checked={true} onCheck={() => {}} title="Title value" />);
+  expect(checkbox).toMatchSnapshot();
+});
+
+it('should render unchecked', () => {
+  const checkbox = shallow(<Checkbox checked={false} onCheck={() => true} />);
+  expect(checkbox.is('.icon-checkbox-checked')).toBe(false);
+  expect(checkbox.prop('aria-checked')).toBe(false);
+});
+
+it('should render checked', () => {
+  const checkbox = shallow(<Checkbox checked={true} onCheck={() => true} />);
+  expect(checkbox.is('.icon-checkbox-checked')).toBe(true);
+  expect(checkbox.prop('aria-checked')).toBe(true);
+});
+
+it('should render disabled', () => {
+  const checkbox = shallow(<Checkbox checked={true} disabled={true} onCheck={() => true} />);
+  expect(checkbox.is('.icon-checkbox-disabled')).toBe(true);
+});
+
+it('should render unchecked third state', () => {
+  const checkbox = shallow(<Checkbox checked={false} onCheck={() => true} thirdState={true} />);
+  expect(checkbox.is('.icon-checkbox-single')).toBe(true);
+  expect(checkbox.is('.icon-checkbox-checked')).toBe(false);
+});
+
+it('should render checked third state', () => {
+  const checkbox = shallow(<Checkbox checked={true} onCheck={() => true} thirdState={true} />);
+  expect(checkbox.is('.icon-checkbox-single')).toBe(true);
+  expect(checkbox.is('.icon-checkbox-checked')).toBe(true);
+});
+
+it('should render with a spinner', () => {
+  const checkbox = shallow(<Checkbox checked={false} loading={true} onCheck={() => true} />);
+  expect(checkbox.find('DeferredSpinner').exists()).toBe(true);
+});
+
+it('should render children', () => {
+  const checkbox = shallow(
+    <Checkbox checked={false} onCheck={() => true}>
+      <span>foo</span>
+    </Checkbox>
+  );
+  expect(checkbox.hasClass('link-checkbox')).toBe(true);
+  expect(checkbox.find('span').exists()).toBe(true);
+});
+
+it('should render children with a spinner', () => {
+  const checkbox = shallow(
+    <Checkbox checked={false} loading={true} onCheck={() => true}>
+      <span>foo</span>
+    </Checkbox>
+  );
+  expect(checkbox.hasClass('link-checkbox')).toBe(true);
+  expect(checkbox.find('span').exists()).toBe(true);
+  expect(checkbox.find('DeferredSpinner').exists()).toBe(true);
+});
+
+it('should call onCheck', () => {
+  const onCheck = jest.fn();
+  const checkbox = shallow(<Checkbox checked={false} onCheck={onCheck} />);
+  click(checkbox);
+  expect(onCheck).toBeCalledWith(true, undefined);
+});
+
+it('should not call onCheck when disabled', () => {
+  const onCheck = jest.fn();
+  const checkbox = shallow(<Checkbox checked={false} disabled={true} onCheck={onCheck} />);
+  click(checkbox);
+  expect(onCheck).toHaveBeenCalledTimes(0);
+});
+
+it('should call onCheck with id as second parameter', () => {
+  const onCheck = jest.fn();
+  const checkbox = shallow(<Checkbox checked={false} id="foo" onCheck={onCheck} />);
+  click(checkbox);
+  expect(onCheck).toBeCalledWith(true, 'foo');
+});
+
+it('should apply custom class', () => {
+  const checkbox = shallow(
+    <Checkbox checked={true} className="customclass" onCheck={() => true} />
+  );
+  expect(checkbox.is('.customclass')).toBe(true);
+});
+
+it('should render the checkbox on the right', () => {
+  expect(shallow(<Checkbox checked={true} onCheck={() => true} right={true} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-ui-common/components/controls/__tests__/ClickEventBoundary-test.tsx b/server/sonar-ui-common/components/controls/__tests__/ClickEventBoundary-test.tsx
new file mode 100644 (file)
index 0000000..c20dc06
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { mount } from 'enzyme';
+import * as React from 'react';
+import ClickEventBoundary from '../ClickEventBoundary';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should correctly capture a click event', () => {
+  const parentOnClick = jest.fn();
+  const childOnClick = jest.fn();
+  const wrapper = shallowRender({ onClick: parentOnClick }, { onClick: childOnClick });
+  // Don't use our click() helper, so we make sure the bubbling works correctly.
+  wrapper.find('button').simulate('click');
+  expect(childOnClick).toBeCalled();
+  expect(parentOnClick).not.toBeCalled();
+});
+
+function shallowRender(parentProps = {}, childProps = {}) {
+  // We need to mount in order to support event bubbling.
+  return mount(
+    <div {...parentProps}>
+      <ClickEventBoundary>
+        <button type="button" {...childProps}>
+          Click me
+        </button>
+      </ClickEventBoundary>
+    </div>
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/ConfirmButton-test.tsx b/server/sonar-ui-common/components/controls/__tests__/ConfirmButton-test.tsx
new file mode 100644 (file)
index 0000000..abd1ae4
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import ConfirmButton from '../ConfirmButton';
+
+it('should display a modal button', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should display a confirm modal', () => {
+  expect(
+    shallowRender().find('ModalButton').prop<Function>('modal')({ onClose: jest.fn() })
+  ).toMatchSnapshot();
+});
+
+function shallowRender() {
+  return shallow(
+    <ConfirmButton
+      confirmButtonText="submit"
+      modalBody={<div />}
+      modalHeader="title"
+      onConfirm={jest.fn()}>
+      {() => 'Confirm button'}
+    </ConfirmButton>
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/ConfirmModal-test.tsx b/server/sonar-ui-common/components/controls/__tests__/ConfirmModal-test.tsx
new file mode 100644 (file)
index 0000000..15fd56c
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { submit, waitAndUpdate } from '../../../helpers/testUtils';
+import ConfirmModal from '../ConfirmModal';
+
+it('should render correctly', () => {
+  const wrapper = shallow(
+    <ConfirmModal
+      confirmButtonText="confirm"
+      confirmData="data"
+      header="title"
+      onClose={jest.fn()}
+      onConfirm={jest.fn()}>
+      <p>My confirm message</p>
+    </ConfirmModal>
+  );
+  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.find('SimpleModal').dive()).toMatchSnapshot();
+});
+
+it('should confirm and close after confirm', async () => {
+  const onClose = jest.fn();
+  const onConfirm = jest.fn(() => Promise.resolve());
+  const wrapper = shallow(
+    <ConfirmModal
+      confirmButtonText="confirm"
+      confirmData="data"
+      header="title"
+      onClose={onClose}
+      onConfirm={onConfirm}>
+      <p>My confirm message</p>
+    </ConfirmModal>
+  );
+  const modalContent = wrapper.find('SimpleModal').dive();
+  submit(modalContent.find('form'));
+  expect(onConfirm).toBeCalledWith('data');
+  expect(modalContent.find('footer')).toMatchSnapshot();
+
+  await waitAndUpdate(wrapper);
+  expect(onClose).toHaveBeenCalled();
+});
diff --git a/server/sonar-ui-common/components/controls/__tests__/Dropdown-test.tsx b/server/sonar-ui-common/components/controls/__tests__/Dropdown-test.tsx
new file mode 100644 (file)
index 0000000..0595d59
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { mount, shallow, ShallowWrapper } from 'enzyme';
+import * as React from 'react';
+import { click } from '../../../helpers/testUtils';
+import { Popup, PopupPlacement } from '../../ui/popups';
+import { Button } from '../buttons';
+import Dropdown, { DropdownOverlay } from '../Dropdown';
+import ScreenPositionFixer from '../ScreenPositionFixer';
+
+describe('Dropdown', () => {
+  it('renders', () => {
+    expect(
+      shallow(<Dropdown overlay={<div id="overlay" />}>{() => <div />}</Dropdown>)
+        .find('div')
+        .exists()
+    ).toBe(true);
+  });
+
+  it('toggles with element child', () => {
+    checkToggle(
+      shallow(
+        <Dropdown overlay={<div id="overlay" />}>
+          <Button />
+        </Dropdown>
+      )
+    );
+
+    checkToggle(
+      shallow(
+        <Dropdown overlay={<div id="overlay" />}>
+          <a href="#">click me!</a>
+        </Dropdown>
+      ),
+      'a'
+    );
+  });
+
+  it('toggles with render prop', () => {
+    checkToggle(
+      shallow(
+        <Dropdown overlay={<div id="overlay" />}>
+          {({ onToggleClick }) => <Button onClick={onToggleClick} />}
+        </Dropdown>
+      )
+    );
+  });
+
+  it('should call onOpen', () => {
+    const onOpen = jest.fn();
+    const wrapper = mount(
+      <Dropdown onOpen={onOpen} overlay={<div id="overlay" />}>
+        <Button />
+      </Dropdown>
+    );
+    expect(onOpen).not.toBeCalled();
+    click(wrapper.find('Button'));
+    expect(onOpen).toBeCalled();
+  });
+
+  function checkToggle(wrapper: ShallowWrapper, selector = 'Button') {
+    expect(wrapper.state()).toEqual({ open: false });
+
+    click(wrapper.find(selector));
+    expect(wrapper.state()).toEqual({ open: true });
+
+    click(wrapper.find(selector));
+    expect(wrapper.state()).toEqual({ open: false });
+  }
+});
+
+describe('DropdownOverlay', () => {
+  it('should render overlay with screen fixer', () => {
+    const wrapper = shallow(
+      <DropdownOverlay>
+        <div />
+      </DropdownOverlay>,
+      // disable ScreenPositionFixer positioning
+      { disableLifecycleMethods: true }
+    );
+
+    expect(wrapper.is(ScreenPositionFixer)).toBe(true);
+    expect(wrapper.dive().dive().dive().is(Popup)).toBe(true);
+  });
+
+  it('should render overlay without screen fixer', () => {
+    const wrapper = shallow(
+      <DropdownOverlay placement={PopupPlacement.BottomRight}>
+        <div />
+      </DropdownOverlay>
+    );
+    expect(wrapper.is('Popup')).toBe(true);
+  });
+});
diff --git a/server/sonar-ui-common/components/controls/__tests__/EscKeydownHandler-test.tsx b/server/sonar-ui-common/components/controls/__tests__/EscKeydownHandler-test.tsx
new file mode 100644 (file)
index 0000000..1264159
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { KeyCodes } from '../../../helpers/keycodes';
+import { keydown } from '../../../helpers/testUtils';
+import EscKeydownHandler from '../EscKeydownHandler';
+
+jest.useFakeTimers();
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should correctly trigger the keydown handler when hitting Esc', () => {
+  const onKeydown = jest.fn();
+  shallowRender({ onKeydown });
+  jest.runAllTimers();
+  keydown(KeyCodes.Escape);
+  expect(onKeydown).toBeCalled();
+});
+
+function shallowRender(props: Partial<EscKeydownHandler['props']> = {}) {
+  return shallow<EscKeydownHandler>(
+    <EscKeydownHandler onKeydown={jest.fn()} {...props}>
+      <span>Hi there</span>
+    </EscKeydownHandler>
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/FavoriteButton-test.tsx b/server/sonar-ui-common/components/controls/__tests__/FavoriteButton-test.tsx
new file mode 100644 (file)
index 0000000..61919eb
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { click } from '../../../helpers/testUtils';
+import FavoriteButton, { Props } from '../FavoriteButton';
+
+it('should render favorite', () => {
+  const favorite = renderFavoriteBase({ favorite: true });
+  expect(favorite).toMatchSnapshot();
+});
+
+it('should render not favorite', () => {
+  const favorite = renderFavoriteBase({ favorite: false });
+  expect(favorite).toMatchSnapshot();
+});
+
+it('should update properly', () => {
+  const favorite = renderFavoriteBase({ favorite: false });
+  expect(favorite).toMatchSnapshot();
+
+  favorite.setProps({ favorite: true });
+  expect(favorite).toMatchSnapshot();
+});
+
+it('should toggle favorite', () => {
+  const toggleFavorite = jest.fn();
+  const favorite = renderFavoriteBase({ toggleFavorite });
+  click(favorite.find('ButtonLink'));
+  expect(toggleFavorite).toBeCalled();
+});
+
+function renderFavoriteBase(props: Partial<Props> = {}) {
+  return shallow(
+    <FavoriteButton favorite={true} qualifier="TRK" toggleFavorite={jest.fn()} {...props} />
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/GlobalMessages-test.tsx b/server/sonar-ui-common/components/controls/__tests__/GlobalMessages-test.tsx
new file mode 100644 (file)
index 0000000..3dc4eda
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import { matchers } from 'jest-emotion';
+import * as React from 'react';
+import testTheme from '../../../config/jest/testTheme';
+import GlobalMessages, { GlobalMessagesProps } from '../GlobalMessages';
+
+expect.extend(matchers);
+
+it('should not render when no message', () => {
+  expect(shallowRender({ messages: [] }).type()).toBeNull();
+});
+
+it('should render correctly with a message', () => {
+  const wrapper = shallowRender();
+  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.find('GlobalMessage').first().dive()).toMatchSnapshot();
+  expect(wrapper.find('GlobalMessage').last().dive()).toMatchSnapshot();
+});
+
+it('should render with correct css', () => {
+  const wrapper = shallowRender();
+  expect(wrapper.render()).toMatchSnapshot();
+  expect(wrapper.find('GlobalMessage').first().render()).toHaveStyleRule(
+    'background-color',
+    testTheme.colors.red
+  );
+
+  expect(wrapper.find('GlobalMessage').last().render()).toHaveStyleRule(
+    'background-color',
+    testTheme.colors.green
+  );
+});
+
+function shallowRender(props: Partial<GlobalMessagesProps> = {}) {
+  return shallow(
+    <GlobalMessages
+      closeGlobalMessage={jest.fn()}
+      messages={[
+        { id: '1', level: 'ERROR', message: 'Test' },
+        { id: '2', level: 'SUCCESS', message: 'Test 2' },
+      ]}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/HelpTooltip-test.tsx b/server/sonar-ui-common/components/controls/__tests__/HelpTooltip-test.tsx
new file mode 100644 (file)
index 0000000..ecd7a50
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import testTheme from '../../../config/jest/testTheme';
+import { ThemeProvider } from '../../theme';
+import HelpTooltip, { DarkHelpTooltip } from '../HelpTooltip';
+
+it('should render properly', () => {
+  const wrapper = shallow(<HelpTooltip overlay={<div className="my-overlay" />} />, {
+    wrappingComponent: ThemeProvider,
+    wrappingComponentProps: {
+      theme: testTheme,
+    },
+  });
+  expect(wrapper).toMatchSnapshot('default');
+  expect(wrapper.find('ContextConsumer').dive()).toMatchSnapshot('default icon');
+
+  wrapper.setProps({ size: 18 });
+  expect(wrapper.find('ContextConsumer').dive().prop('size')).toBe(18);
+});
+
+it('should render dark helptooltip properly', () => {
+  const wrapper = shallow(<DarkHelpTooltip overlay={<div className="my-overlay" />} size={14} />, {
+    wrappingComponent: ThemeProvider,
+    wrappingComponentProps: {
+      theme: testTheme,
+    },
+  });
+  expect(wrapper).toMatchSnapshot('dark');
+  expect(wrapper.find('ContextConsumer').dive()).toMatchSnapshot('dark icon');
+
+  wrapper.setProps({ size: undefined });
+  expect(wrapper.find('ContextConsumer').dive().prop('size')).toBe(12);
+});
diff --git a/server/sonar-ui-common/components/controls/__tests__/IdentityProviderLink-test.tsx b/server/sonar-ui-common/components/controls/__tests__/IdentityProviderLink-test.tsx
new file mode 100644 (file)
index 0000000..9a8b0a4
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import IdentityProviderLink from '../IdentityProviderLink';
+
+const identityProvider = {
+  backgroundColor: '#000',
+  iconPath: '/some/path',
+  key: 'foo',
+  name: 'Foo',
+};
+
+it('should render correctly', () => {
+  expect(
+    shallow(
+      <IdentityProviderLink
+        backgroundColor={identityProvider.backgroundColor}
+        iconPath={identityProvider.iconPath}
+        name={identityProvider.name}
+        url="/url/foo/bar">
+        Link text
+      </IdentityProviderLink>
+    )
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-ui-common/components/controls/__tests__/InputValidationField-test.tsx b/server/sonar-ui-common/components/controls/__tests__/InputValidationField-test.tsx
new file mode 100644 (file)
index 0000000..363e3d7
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import InputValidationField from '../InputValidationField';
+
+it('should render correctly', () => {
+  expect(getWrapper()).toMatchSnapshot();
+});
+
+function getWrapper(props = {}) {
+  return shallow(
+    <InputValidationField
+      description="Field description"
+      dirty={true}
+      disabled={false}
+      error="Bad formatting"
+      label="Foo field"
+      name="field"
+      onBlur={jest.fn()}
+      onChange={jest.fn()}
+      touched={true}
+      value="foo"
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/ListFooter-test.tsx b/server/sonar-ui-common/components/controls/__tests__/ListFooter-test.tsx
new file mode 100644 (file)
index 0000000..6ea69c9
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { click } from '../../../helpers/testUtils';
+import { Button } from '../buttons';
+import ListFooter, { ListFooterProps } from '../ListFooter';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('default');
+  expect(shallowRender({ loading: true })).toMatchSnapshot('loading');
+  expect(shallowRender({ needReload: true, reload: jest.fn() })).toMatchSnapshot('reload');
+  expect(shallowRender({ loading: true, needReload: true, reload: jest.fn() })).toMatchSnapshot(
+    'reload, loading'
+  );
+  expect(shallowRender({ loadMore: undefined })).toMatchSnapshot(
+    'empty if no loadMore nor reload props'
+  );
+  expect(shallowRender({ count: 5 })).toMatchSnapshot('empty if everything is loaded');
+});
+
+it('should properly call loadMore', () => {
+  const loadMore = jest.fn();
+  const wrapper = shallowRender({ loadMore });
+  click(wrapper.find(Button));
+  expect(loadMore).toBeCalled();
+});
+
+it('should properly call reload', () => {
+  const reload = jest.fn();
+  const wrapper = shallowRender({ needReload: true, reload });
+  click(wrapper.find(Button));
+  expect(reload).toBeCalled();
+});
+
+function shallowRender(props: Partial<ListFooterProps> = {}) {
+  return shallow<ListFooterProps>(
+    <ListFooter count={3} loadMore={jest.fn()} total={5} {...props} />
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/ModalButton-test.tsx b/server/sonar-ui-common/components/controls/__tests__/ModalButton-test.tsx
new file mode 100644 (file)
index 0000000..91756df
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { click } from '../../../helpers/testUtils';
+import ModalButton from '../ModalButton';
+
+it('should open/close modal', () => {
+  const wrapper = shallow(
+    <ModalButton modal={({ onClose }) => <button id="js-close" onClick={onClose} type="button" />}>
+      {({ onClick }) => <button id="js-open" onClick={onClick} type="button" />}
+    </ModalButton>
+  );
+
+  expect(wrapper.find('#js-open').exists()).toBe(true);
+  expect(wrapper.find('#js-close').exists()).toBe(false);
+  click(wrapper.find('#js-open'));
+  expect(wrapper.find('#js-close').exists()).toBe(true);
+  click(wrapper.find('#js-close'));
+  expect(wrapper.find('#js-close').exists()).toBe(false);
+});
diff --git a/server/sonar-ui-common/components/controls/__tests__/ModalValidationField-test.tsx b/server/sonar-ui-common/components/controls/__tests__/ModalValidationField-test.tsx
new file mode 100644 (file)
index 0000000..1f5cbb5
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import ModalValidationField from '../ModalValidationField';
+
+it('should display the field without any error/validation', () => {
+  expect(getWrapper({ description: 'Describe Foo.', touched: false })).toMatchSnapshot();
+  expect(getWrapper({ dirty: false })).toMatchSnapshot();
+});
+
+it('should display the field as valid', () => {
+  expect(getWrapper({ error: undefined })).toMatchSnapshot();
+});
+
+it('should display the field with an error', () => {
+  expect(getWrapper()).toMatchSnapshot();
+});
+
+function getWrapper(props = {}) {
+  return shallow(
+    <ModalValidationField
+      dirty={true}
+      error="Is required"
+      label={<label>Foo</label>}
+      touched={true}
+      {...props}>
+      {({ className }) => <input className={className} type="text" />}
+    </ModalValidationField>
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/Radio-test.tsx b/server/sonar-ui-common/components/controls/__tests__/Radio-test.tsx
new file mode 100644 (file)
index 0000000..feb1a6e
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { click } from '../../../helpers/testUtils';
+import Radio from '../Radio';
+
+it('should render properly', () => {
+  const wrapper = shallowRender();
+  expect(wrapper).toMatchSnapshot('not checked');
+
+  wrapper.setProps({ checked: true });
+  expect(wrapper).toMatchSnapshot('checked');
+});
+
+it('should invoke callback on click', () => {
+  const onCheck = jest.fn();
+  const value = 'value';
+  const wrapper = shallowRender({ onCheck, value });
+
+  click(wrapper);
+  expect(onCheck).toHaveBeenCalled();
+});
+
+it('should not invoke callback on click when disabled', () => {
+  const onCheck = jest.fn();
+  const wrapper = shallowRender({ disabled: true, onCheck });
+
+  click(wrapper);
+  expect(onCheck).not.toHaveBeenCalled();
+});
+
+function shallowRender(props?: Partial<Radio['props']>) {
+  return shallow<Radio>(<Radio checked={false} onCheck={jest.fn()} value="value" {...props} />);
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/RadioCard-test.tsx b/server/sonar-ui-common/components/controls/__tests__/RadioCard-test.tsx
new file mode 100644 (file)
index 0000000..27ed8f6
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { click } from '../../../helpers/testUtils';
+import RadioCard from '../RadioCard';
+
+it('should render correctly', () => {
+  expect(
+    shallow(
+      <RadioCard recommended="Recommended for you" title="Radio Card" titleInfo="info">
+        <div>content</div>
+      </RadioCard>
+    )
+  ).toMatchSnapshot();
+
+  expect(
+    shallow(
+      <RadioCard
+        recommended="Recommended for you"
+        title="Radio Card Vertical"
+        titleInfo="info"
+        vertical={true}>
+        <div>content</div>
+      </RadioCard>
+    )
+  ).toMatchSnapshot();
+});
+
+it('should be actionable', () => {
+  const onClick = jest.fn();
+  const wrapper = shallow(
+    <RadioCard onClick={onClick} title="Radio Card">
+      <div>content</div>
+    </RadioCard>
+  );
+
+  expect(wrapper).toMatchSnapshot();
+  click(wrapper);
+  wrapper.setProps({ selected: true, titleInfo: 'info' });
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-ui-common/components/controls/__tests__/RadioToggle-test.tsx b/server/sonar-ui-common/components/controls/__tests__/RadioToggle-test.tsx
new file mode 100644 (file)
index 0000000..6d5aa2e
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { change } from '../../../helpers/testUtils';
+import RadioToggle from '../RadioToggle';
+
+it('renders', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('calls onCheck', () => {
+  const onCheck = jest.fn();
+  const wrapper = shallowRender({ onCheck });
+  change(wrapper.find('input[id="sample__two"]'), '');
+  expect(onCheck).toBeCalledWith('two');
+});
+
+it('handles numeric values', () => {
+  const onCheck = jest.fn();
+  const wrapper = shallowRender({
+    onCheck,
+    options: [
+      { value: 1, label: 'first', tooltip: 'foo' },
+      { value: 2, label: 'second', tooltip: 'bar' },
+    ],
+    value: 1,
+  });
+  change(wrapper.find('input[id="sample__2"]'), '');
+  expect(onCheck).toBeCalledWith(2);
+});
+
+it('handles boolean values', () => {
+  const onCheck = jest.fn();
+  const wrapper = shallowRender({
+    onCheck,
+    options: [
+      { value: true, label: 'yes', tooltip: 'foo' },
+      { value: false, label: 'no', tooltip: 'bar' },
+    ],
+    value: true,
+  });
+  change(wrapper.find('input[id="sample__false"]'), '');
+  expect(onCheck).toBeCalledWith(false);
+});
+
+it('initialize value', () => {
+  const onCheck = jest.fn();
+  const wrapper = shallowRender({
+    onCheck,
+    options: [
+      { value: 1, label: 'first', tooltip: 'foo' },
+      { value: 2, label: 'second', tooltip: 'bar', disabled: true },
+    ],
+    value: 2,
+  });
+  expect(wrapper.find('input[checked=true]').prop('id')).toBe('sample__2');
+});
+
+it('accepts advanced options fields', () => {
+  expect(
+    shallowRender({
+      options: [
+        { value: 'one', label: 'first', tooltip: 'foo' },
+        { value: 'two', label: 'second', tooltip: 'bar', disabled: true },
+      ],
+    })
+  ).toMatchSnapshot();
+});
+
+function shallowRender(props?: Partial<RadioToggle['props']>) {
+  const options = [
+    { value: 'one', label: 'first' },
+    { value: 'two', label: 'second' },
+  ];
+  return shallow(<RadioToggle name="sample" onCheck={() => true} options={options} {...props} />);
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/ReloadButton-test.tsx b/server/sonar-ui-common/components/controls/__tests__/ReloadButton-test.tsx
new file mode 100644 (file)
index 0000000..f6a9285
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import testTheme from '../../../config/jest/testTheme';
+import { click } from '../../../helpers/testUtils';
+import { ThemeProvider } from '../../theme';
+import ReloadButton from '../ReloadButton';
+
+it('should render properly', () => {
+  const wrapper = shallowRender();
+  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.find('ContextConsumer').dive()).toMatchSnapshot();
+});
+
+it('should handle click', () => {
+  const onClick = jest.fn();
+  const wrapper = shallowRender({ onClick });
+  expect(wrapper).toMatchSnapshot();
+  click(wrapper.find('a'));
+  expect(onClick).toBeCalled();
+});
+
+function shallowRender(props: Partial<ReloadButton['props']> = {}) {
+  return shallow<ReloadButton>(<ReloadButton onClick={jest.fn()} {...props} />, {
+    wrappingComponent: ThemeProvider,
+    wrappingComponentProps: {
+      theme: testTheme,
+    },
+  });
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/ScreenPositionFixer-test.tsx b/server/sonar-ui-common/components/controls/__tests__/ScreenPositionFixer-test.tsx
new file mode 100644 (file)
index 0000000..aae3a9e
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { mount } from 'enzyme';
+import * as React from 'react';
+import { resizeWindowTo, setNodeRect } from '../../../helpers/testUtils';
+import ScreenPositionFixer from '../ScreenPositionFixer';
+
+jest.mock('lodash', () => {
+  const lodash = require.requireActual('lodash');
+  lodash.throttle = (fn: any) => () => fn();
+  return lodash;
+});
+
+jest.mock('react-dom', () => ({
+  findDOMNode: jest.fn(),
+}));
+
+beforeEach(() => {
+  setNodeRect({ left: 50, top: 50 });
+  resizeWindowTo(1000, 1000);
+});
+
+it('should fix position', () => {
+  const children = jest.fn(() => <div />);
+  mountRender({ children });
+
+  setNodeRect({ left: 50, top: 50 });
+  resizeWindowTo(75, 1000);
+  expect(children).toHaveBeenLastCalledWith({ leftFix: -29, topFix: 0 });
+
+  resizeWindowTo(1000, 75);
+  expect(children).toHaveBeenLastCalledWith({ leftFix: 0, topFix: -29 });
+
+  setNodeRect({ left: -10, top: 50 });
+  resizeWindowTo(1000, 1000);
+  expect(children).toHaveBeenLastCalledWith({ leftFix: 14, topFix: 0 });
+
+  setNodeRect({ left: 50, top: -10 });
+  resizeWindowTo();
+  expect(children).toHaveBeenLastCalledWith({ leftFix: 0, topFix: 14 });
+});
+
+it('should render two times', () => {
+  const children = jest.fn(() => <div />);
+  mountRender({ children });
+  expect(children).toHaveBeenCalledTimes(2);
+  expect(children).toHaveBeenCalledWith({});
+  expect(children).toHaveBeenLastCalledWith({ leftFix: 0, topFix: 0 });
+});
+
+it('should re-position when `ready` turns to `true`', () => {
+  const children = jest.fn(() => <div />);
+  const wrapper = mountRender({ children, ready: false });
+  expect(children).toHaveBeenCalledTimes(2);
+  wrapper.setProps({ ready: true });
+  // 2 + 1 (props change) + 1 (new measurement)
+  expect(children).toHaveBeenCalledTimes(4);
+});
+
+it('should re-position when window is resized', () => {
+  const children = jest.fn(() => <div />);
+  const wrapper = mountRender({ children });
+  expect(children).toHaveBeenCalledTimes(2);
+
+  resizeWindowTo();
+  // 2 + 1 (new measurement)
+  expect(children).toHaveBeenCalledTimes(3);
+
+  wrapper.unmount();
+  resizeWindowTo();
+  expect(children).toHaveBeenCalledTimes(3);
+});
+
+function mountRender(props: ScreenPositionFixer['props']) {
+  return mount(<ScreenPositionFixer {...props} />);
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/SearchBox-test.tsx b/server/sonar-ui-common/components/controls/__tests__/SearchBox-test.tsx
new file mode 100644 (file)
index 0000000..aaed9ed
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { mount, shallow } from 'enzyme';
+import * as React from 'react';
+import { change, click } from '../../../helpers/testUtils';
+import SearchBox from '../SearchBox';
+
+jest.mock('lodash', () => {
+  const lodash = jest.requireActual('lodash');
+  const debounce = (fn: Function) => {
+    const debounced: any = (...args: any[]) => fn(...args);
+    debounced.cancel = jest.fn();
+    return debounced;
+  };
+  return Object.assign({}, lodash, { debounce });
+});
+
+it('renders', () => {
+  const wrapper = shallow(
+    <SearchBox
+      maxLength={150}
+      minLength={2}
+      onChange={jest.fn()}
+      placeholder="placeholder"
+      value="foo"
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('warns when input is too short', () => {
+  const wrapper = shallow(
+    <SearchBox minLength={2} onChange={jest.fn()} placeholder="placeholder" value="f" />
+  );
+  expect(wrapper.find('.search-box-note').exists()).toBe(true);
+});
+
+it('shows clear button only when there is a value', () => {
+  const wrapper = shallow(<SearchBox onChange={jest.fn()} placeholder="placeholder" value="f" />);
+  expect(wrapper.find('.search-box-clear').exists()).toBe(true);
+  wrapper.setProps({ value: '' });
+  expect(wrapper.find('.search-box-clear').exists()).toBe(false);
+});
+
+it('attaches ref', () => {
+  const ref = jest.fn();
+  mount(<SearchBox innerRef={ref} onChange={jest.fn()} placeholder="placeholder" value="f" />);
+  expect(ref).toBeCalled();
+  expect(ref.mock.calls[0][0]).toBeInstanceOf(HTMLInputElement);
+});
+
+it('resets', () => {
+  const onChange = jest.fn();
+  const wrapper = shallow(<SearchBox onChange={onChange} placeholder="placeholder" value="f" />);
+  click(wrapper.find('.search-box-clear'));
+  expect(onChange).toBeCalledWith('');
+});
+
+it('changes', () => {
+  const onChange = jest.fn();
+  const wrapper = shallow(<SearchBox onChange={onChange} placeholder="placeholder" value="f" />);
+  change(wrapper.find('.search-box-input'), 'foo');
+  expect(onChange).toBeCalledWith('foo');
+});
+
+it('does not change when value is too short', () => {
+  const onChange = jest.fn();
+  const wrapper = shallow(
+    <SearchBox minLength={3} onChange={onChange} placeholder="placeholder" value="" />
+  );
+  change(wrapper.find('.search-box-input'), 'fo');
+  expect(onChange).not.toBeCalled();
+});
diff --git a/server/sonar-ui-common/components/controls/__tests__/SearchSelect-test.tsx b/server/sonar-ui-common/components/controls/__tests__/SearchSelect-test.tsx
new file mode 100644 (file)
index 0000000..71ca3e7
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import SearchSelect from '../SearchSelect';
+
+jest.mock('lodash', () => {
+  const lodash = require.requireActual('lodash');
+  lodash.debounce = jest.fn((fn) => fn);
+  return lodash;
+});
+
+it('should render Select', () => {
+  expect(shallow(<SearchSelect onSearch={jest.fn()} onSelect={jest.fn()} />)).toMatchSnapshot();
+});
+
+it('should call onSelect', () => {
+  const onSelect = jest.fn();
+  const wrapper = shallow(<SearchSelect onSearch={jest.fn()} onSelect={onSelect} />);
+  wrapper.prop('onChange')({ value: 'foo' });
+  expect(onSelect).lastCalledWith({ value: 'foo' });
+});
+
+it('should call onSearch', () => {
+  const onSearch = jest.fn().mockReturnValue(Promise.resolve([]));
+  const wrapper = shallow(
+    <SearchSelect minimumQueryLength={2} onSearch={onSearch} onSelect={jest.fn()} />
+  );
+  wrapper.prop('onInputChange')('f');
+  expect(onSearch).not.toHaveBeenCalled();
+  wrapper.prop('onInputChange')('foo');
+  expect(onSearch).lastCalledWith('foo');
+});
diff --git a/server/sonar-ui-common/components/controls/__tests__/SelectList-test.tsx b/server/sonar-ui-common/components/controls/__tests__/SelectList-test.tsx
new file mode 100644 (file)
index 0000000..e4a4ed2
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { waitAndUpdate } from '../../../helpers/testUtils';
+import SelectList, { SelectListFilter } from '../SelectList';
+
+const elements = ['foo', 'bar', 'baz'];
+const selectedElements = [elements[0]];
+const disabledElements = [elements[1]];
+
+it('should display properly with basics features', async () => {
+  const wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+  expect(wrapper.instance().mounted).toBe(true);
+
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.instance().componentWillUnmount();
+  expect(wrapper.instance().mounted).toBe(false);
+});
+
+it('should display properly with advanced features', async () => {
+  const wrapper = shallowRender({
+    allowBulkSelection: true,
+    elementsTotalCount: 125,
+    pageSize: 10,
+    readOnly: true,
+    withPaging: true,
+  });
+  await waitAndUpdate(wrapper);
+
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should display a loader when searching', async () => {
+  const wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().loading).toBe(false);
+
+  wrapper.instance().search({});
+  expect(wrapper.state().loading).toBe(true);
+  expect(wrapper).toMatchSnapshot();
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().loading).toBe(false);
+});
+
+it('should cancel filter selection when search is active', async () => {
+  const spy = jest.fn().mockResolvedValue({});
+  const wrapper = shallowRender({ onSearch: spy });
+  wrapper.instance().changeFilter(SelectListFilter.Unselected);
+  await waitAndUpdate(wrapper);
+
+  expect(spy).toHaveBeenCalledWith({
+    query: '',
+    filter: SelectListFilter.Unselected,
+    page: undefined,
+    pageSize: undefined,
+  });
+  expect(wrapper).toMatchSnapshot();
+
+  const query = 'test';
+  wrapper.instance().handleQueryChange(query);
+  expect(spy).toHaveBeenCalledWith({
+    query,
+    filter: SelectListFilter.All,
+    page: undefined,
+    pageSize: undefined,
+  });
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+
+  wrapper.instance().handleQueryChange('');
+  expect(spy).toHaveBeenCalledWith({
+    query: '',
+    filter: SelectListFilter.Unselected,
+    page: undefined,
+    pageSize: undefined,
+  });
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should display pagination element properly and call search method with correct parameters', () => {
+  const spy = jest.fn().mockResolvedValue({});
+  const wrapper = shallowRender({ elementsTotalCount: 100, onSearch: spy, withPaging: true });
+  expect(wrapper).toMatchSnapshot();
+  expect(spy).toHaveBeenCalledWith({
+    query: '',
+    filter: SelectListFilter.Selected,
+    page: 1,
+    pageSize: 100,
+  }); // Basic default call
+
+  wrapper.instance().onLoadMore();
+  expect(spy).toHaveBeenCalledWith({
+    query: '',
+    filter: SelectListFilter.Selected,
+    page: 2,
+    pageSize: 100,
+  }); // Load more call
+
+  wrapper.instance().onReload();
+  expect(spy).toHaveBeenCalledWith({
+    query: '',
+    filter: SelectListFilter.Selected,
+    page: 1,
+    pageSize: 100,
+  }); // Reload call
+
+  wrapper.setProps({ needToReload: true });
+  expect(wrapper).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<SelectList['props']> = {}) {
+  return shallow<SelectList>(
+    <SelectList
+      disabledElements={disabledElements}
+      elements={elements}
+      onSearch={jest.fn(() => Promise.resolve())}
+      onSelect={jest.fn(() => Promise.resolve())}
+      onUnselect={jest.fn(() => Promise.resolve())}
+      renderElement={(foo: string) => foo}
+      selectedElements={selectedElements}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/SelectListListContainer-test.tsx b/server/sonar-ui-common/components/controls/__tests__/SelectListListContainer-test.tsx
new file mode 100644 (file)
index 0000000..61d53f4
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { SelectListFilter } from '../SelectList';
+import SelectListListContainer from '../SelectListListContainer';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<SelectListListContainer['props']> = {}) {
+  return shallow(
+    <SelectListListContainer
+      allowBulkSelection={true}
+      disabledElements={[]}
+      elements={['foo', 'bar', 'baz']}
+      filter={SelectListFilter.All}
+      onSelect={jest.fn(() => Promise.resolve())}
+      onUnselect={jest.fn(() => Promise.resolve())}
+      readOnly={false}
+      renderElement={(foo: string) => foo}
+      selectedElements={['foo']}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/SelectListListElement-test.tsx b/server/sonar-ui-common/components/controls/__tests__/SelectListListElement-test.tsx
new file mode 100644 (file)
index 0000000..82a1a40
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { waitAndUpdate } from '../../../helpers/testUtils';
+import SelectListListElement from '../SelectListListElement';
+
+const listElement = (
+  <SelectListListElement
+    element="foo"
+    key="foo"
+    onSelect={jest.fn(() => Promise.resolve())}
+    onUnselect={jest.fn(() => Promise.resolve())}
+    renderElement={(foo: string) => foo}
+    selected={false}
+  />
+);
+
+it('should display a loader when checking', async () => {
+  const wrapper = shallow<SelectListListElement>(listElement);
+  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.state().loading).toBe(false);
+
+  (wrapper.instance() as SelectListListElement).handleCheck(true);
+  expect(wrapper.state().loading).toBe(true);
+  expect(wrapper).toMatchSnapshot();
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().loading).toBe(false);
+});
diff --git a/server/sonar-ui-common/components/controls/__tests__/SimpleModal-test.tsx b/server/sonar-ui-common/components/controls/__tests__/SimpleModal-test.tsx
new file mode 100644 (file)
index 0000000..6b52714
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { click, waitAndUpdate } from '../../../helpers/testUtils';
+import { Button } from '../buttons';
+import SimpleModal, { ChildrenProps } from '../SimpleModal';
+
+it('renders', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('closes', () => {
+  const onClose = jest.fn();
+  const children = ({ onCloseClick }: ChildrenProps) => (
+    <Button onClick={onCloseClick}>close</Button>
+  );
+  const wrapper = shallowRender({ children, onClose });
+  click(wrapper.find('Button'));
+  expect(onClose).toBeCalled();
+});
+
+it('submits', async () => {
+  const onSubmit = jest.fn(() => Promise.resolve());
+  const children = ({ onSubmitClick, submitting }: ChildrenProps) => (
+    <Button disabled={submitting} onClick={onSubmitClick}>
+      close
+    </Button>
+  );
+  const wrapper = shallowRender({ children, onSubmit });
+  wrapper.instance().mounted = true;
+  expect(wrapper).toMatchSnapshot();
+
+  click(wrapper.find('Button'));
+  expect(onSubmit).toBeCalled();
+  expect(wrapper).toMatchSnapshot();
+
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+});
+
+function shallowRender({ children = () => <div />, ...props }: Partial<SimpleModal['props']> = {}) {
+  return shallow<SimpleModal>(
+    <SimpleModal header="" onClose={jest.fn()} onSubmit={jest.fn()} {...props}>
+      {children}
+    </SimpleModal>
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/Tabs-test.tsx b/server/sonar-ui-common/components/controls/__tests__/Tabs-test.tsx
new file mode 100644 (file)
index 0000000..7db674a
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { click } from '../../../helpers/testUtils';
+import Tabs, { Tab } from '../Tabs';
+
+it('should render correctly', () => {
+  const wrapper = shallow(
+    <Tabs
+      onChange={jest.fn()}
+      selected="bar"
+      tabs={[
+        { key: 'foo', node: 'Foo' },
+        { key: 'bar', node: 'Bar' },
+      ]}
+    />
+  );
+
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should switch tabs', () => {
+  const onChange = jest.fn();
+  const wrapper = shallow(
+    <Tabs
+      onChange={onChange}
+      selected="bar"
+      tabs={[
+        { key: 'foo', node: 'Foo' },
+        { key: 'bar', node: 'Bar' },
+      ]}
+    />
+  );
+
+  click(shallow(wrapper.find('Tab').get(0)).find('.js-foo'));
+  expect(onChange).toBeCalledWith('foo');
+  click(shallow(wrapper.find('Tab').get(1)).find('.js-bar'));
+  expect(onChange).toBeCalledWith('bar');
+});
+
+it('should render single tab correctly', () => {
+  const onSelect = jest.fn();
+  const wrapper = shallow(
+    <Tab name="foo" onSelect={onSelect} selected={true}>
+      <span>Foo</span>
+    </Tab>
+  );
+  expect(wrapper).toMatchSnapshot();
+  click(wrapper.find('a'));
+  expect(onSelect).toBeCalledWith('foo');
+});
+
+it('should disable single tab', () => {
+  const onSelect = jest.fn();
+  const wrapper = shallow(
+    <Tab disabled={true} name="foo" onSelect={onSelect} selected={true}>
+      <span>Foo</span>
+    </Tab>
+  );
+  expect(wrapper).toMatchSnapshot();
+  click(wrapper.find('a'));
+  expect(onSelect).not.toBeCalled();
+});
diff --git a/server/sonar-ui-common/components/controls/__tests__/Toggle-test.tsx b/server/sonar-ui-common/components/controls/__tests__/Toggle-test.tsx
new file mode 100644 (file)
index 0000000..79fc605
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { click } from '../../../helpers/testUtils';
+import { Button } from '../buttons';
+import Toggle from '../Toggle';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('on');
+  expect(shallowRender({ value: false })).toMatchSnapshot('off');
+  expect(shallowRender({ disabled: true })).toMatchSnapshot('disabled');
+});
+
+it('should call onChange when clicked', () => {
+  const onChange = jest.fn();
+  const wrapper = shallowRender({ disabled: false, onChange, value: true });
+  click(wrapper.find(Button));
+  expect(onChange).toBeCalledWith(false);
+});
+
+function shallowRender(props?: Partial<Toggle['props']>) {
+  return shallow(
+    <Toggle disabled={true} name="toggle-name" onChange={jest.fn()} value={true} {...props} />
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/Toggler-test.tsx b/server/sonar-ui-common/components/controls/__tests__/Toggler-test.tsx
new file mode 100644 (file)
index 0000000..dfcbe6b
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import Toggler from '../Toggler';
+
+it('should render only children', () => {
+  expect(shallowRender({ open: false })).toMatchSnapshot();
+});
+
+it('should render children and overlay', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should render when closeOnClick=true', () => {
+  expect(shallowRender({ closeOnClick: true })).toMatchSnapshot();
+});
+
+it('should not render click wrappers', () => {
+  expect(
+    shallowRender({ closeOnClick: false, closeOnClickOutside: false, closeOnEscape: false })
+  ).toMatchSnapshot();
+});
+
+function shallowRender(props?: Partial<Toggler['props']>) {
+  return shallow(
+    <Toggler onRequestClose={jest.fn()} open={true} overlay={<div id="overlay" />} {...props}>
+      <div id="toggle" />
+    </Toggler>
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/Tooltip-test.tsx b/server/sonar-ui-common/components/controls/__tests__/Tooltip-test.tsx
new file mode 100644 (file)
index 0000000..d490fd5
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import Tooltip, { TooltipInner } from '../Tooltip';
+
+jest.useFakeTimers();
+jest.mock('react-dom', () => {
+  const actual = require.requireActual('react-dom');
+  return Object.assign({}, actual, {
+    findDOMNode: () => undefined,
+  });
+});
+
+it('should render', () => {
+  expect(
+    shallow(
+      <TooltipInner overlay={<span id="overlay" />} visible={false}>
+        <div id="tooltip" />
+      </TooltipInner>
+    )
+  ).toMatchSnapshot();
+  expect(
+    shallow(
+      <TooltipInner overlay={<span id="overlay" />} visible={true}>
+        <div id="tooltip" />
+      </TooltipInner>,
+      { disableLifecycleMethods: true }
+    )
+  ).toMatchSnapshot();
+});
+
+it('should open & close', () => {
+  const onShow = jest.fn();
+  const onHide = jest.fn();
+  const wrapper = shallow(
+    <TooltipInner onHide={onHide} onShow={onShow} overlay={<span id="overlay" />}>
+      <div id="tooltip" />
+    </TooltipInner>
+  );
+  wrapper.find('#tooltip').simulate('mouseenter');
+  jest.runOnlyPendingTimers();
+  wrapper.update();
+  expect(wrapper.find('TooltipPortal').exists()).toBe(true);
+  expect(onShow).toBeCalled();
+
+  wrapper.find('#tooltip').simulate('mouseleave');
+  jest.runOnlyPendingTimers();
+  wrapper.update();
+  expect(wrapper.find('TooltipPortal').exists()).toBe(false);
+  expect(onHide).toBeCalled();
+});
+
+it('should not open when mouse goes away quickly', () => {
+  const onShow = jest.fn();
+  const onHide = jest.fn();
+  const wrapper = shallow(
+    <TooltipInner onHide={onHide} onShow={onShow} overlay={<span id="overlay" />}>
+      <div id="tooltip" />
+    </TooltipInner>
+  );
+
+  wrapper.find('#tooltip').simulate('mouseenter');
+  wrapper.find('#tooltip').simulate('mouseleave');
+  jest.runOnlyPendingTimers();
+  wrapper.update();
+
+  expect(wrapper.find('TooltipPortal').exists()).toBe(false);
+});
+
+it('should not render tooltip without overlay', () => {
+  const wrapper = shallow(
+    <Tooltip overlay={undefined}>
+      <div id="tooltip" />
+    </Tooltip>
+  );
+  expect(wrapper.type()).toBe('div');
+});
+
+it('should not render empty tooltips', () => {
+  expect(
+    shallow(
+      <Tooltip overlay={undefined} visible={true}>
+        <div id="tooltip" />
+      </Tooltip>
+    )
+  ).toMatchSnapshot();
+  expect(
+    shallow(
+      <Tooltip overlay="" visible={true}>
+        <div id="tooltip" />
+      </Tooltip>
+    )
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-ui-common/components/controls/__tests__/ValidationForm-test.tsx b/server/sonar-ui-common/components/controls/__tests__/ValidationForm-test.tsx
new file mode 100644 (file)
index 0000000..295967f
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import ValidationForm from '../ValidationForm';
+
+it('should render and submit', async () => {
+  const render = jest.fn();
+  const onSubmit = jest.fn();
+  const setSubmitting = jest.fn();
+  const wrapper = shallow(
+    <ValidationForm initialValues={{ foo: 'bar' }} onSubmit={onSubmit} validate={jest.fn()}>
+      {render}
+    </ValidationForm>
+  );
+  expect(wrapper).toMatchSnapshot();
+  wrapper.dive();
+  expect(render).toBeCalledWith(
+    expect.objectContaining({ dirty: false, errors: {}, values: { foo: 'bar' } })
+  );
+
+  wrapper.prop<Function>('onSubmit')({ foo: 'bar' }, { setSubmitting });
+  expect(setSubmitting).toBeCalledWith(false);
+
+  onSubmit.mockResolvedValue(undefined).mockClear();
+  setSubmitting.mockClear();
+  wrapper.prop<Function>('onSubmit')({ foo: 'bar' }, { setSubmitting });
+  await new Promise(setImmediate);
+  expect(setSubmitting).toBeCalledWith(false);
+});
diff --git a/server/sonar-ui-common/components/controls/__tests__/ValidationInput-test.tsx b/server/sonar-ui-common/components/controls/__tests__/ValidationInput-test.tsx
new file mode 100644 (file)
index 0000000..7169780
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import ValidationInput from '../ValidationInput';
+
+it('should render', () => {
+  expect(
+    shallow(
+      <ValidationInput
+        description="My description"
+        error={undefined}
+        help="Help message"
+        id="field-id"
+        isInvalid={false}
+        isValid={false}
+        label="Field label"
+        required={true}>
+        <div />
+      </ValidationInput>
+    )
+  ).toMatchSnapshot();
+});
+
+it('should render with error', () => {
+  expect(
+    shallow(
+      <ValidationInput
+        description={<div>My description</div>}
+        error="Field error message"
+        id="field-id"
+        isInvalid={true}
+        isValid={false}
+        label="Field label">
+        <div />
+      </ValidationInput>
+    )
+  ).toMatchSnapshot();
+});
+
+it('should render when valid', () => {
+  expect(
+    shallow(
+      <ValidationInput
+        description="My description"
+        error={undefined}
+        id="field-id"
+        isInvalid={false}
+        isValid={true}
+        label="Field label"
+        required={true}>
+        <div />
+      </ValidationInput>
+    )
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-ui-common/components/controls/__tests__/ValidationModal-test.tsx b/server/sonar-ui-common/components/controls/__tests__/ValidationModal-test.tsx
new file mode 100644 (file)
index 0000000..810b864
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { waitAndUpdate } from '../../../helpers/testUtils';
+import ValidationForm from '../ValidationForm';
+import ValidationModal from '../ValidationModal';
+
+it('should render correctly', () => {
+  const wrapper = shallowRender();
+  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.find(ValidationForm).dive().dive()).toMatchSnapshot();
+});
+
+it('should handle submit', async () => {
+  const data = { field: 'foo' };
+  const onSubmit = jest.fn().mockResolvedValue({});
+  const onClose = jest.fn();
+  const wrapper = shallowRender({ onClose, onSubmit });
+
+  wrapper.instance().handleSubmit(data);
+  expect(onSubmit).toBeCalledWith(data);
+
+  await waitAndUpdate(wrapper);
+  expect(onClose).toBeCalled();
+});
+
+function shallowRender(props: Partial<ValidationModal<{ field: string }>['props']> = {}) {
+  return shallow<ValidationModal<{ field: string }>>(
+    <ValidationModal<{ field: string }>
+      confirmButtonText="confirm"
+      header="title"
+      initialValues={{ field: 'foo' }}
+      isDestructive={true}
+      isInitialValid={true}
+      onClose={jest.fn()}
+      onSubmit={jest.fn()}
+      validate={jest.fn()}
+      {...props}>
+      {(props) => (
+        <input
+          name="field"
+          onBlur={props.handleBlur}
+          onChange={props.handleChange}
+          type="text"
+          value={props.values.field}
+        />
+      )}
+    </ValidationModal>
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ActionsDropdown-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ActionsDropdown-test.tsx.snap
new file mode 100644 (file)
index 0000000..4bd6a2e
--- /dev/null
@@ -0,0 +1,143 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ActionsDropdown should render correctly 1`] = `
+<Dropdown
+  className="foo"
+  onOpen={[MockFunction]}
+  overlay={
+    <ul
+      className="menu"
+    >
+      <span>
+        Hello world
+      </span>
+    </ul>
+  }
+  overlayPlacement="bottom"
+>
+  <Button
+    className="dropdown-toggle bar button-small"
+  >
+    <SettingsIcon
+      size={12}
+    />
+    <DropdownIcon
+      className="little-spacer-left"
+    />
+  </Button>
+</Dropdown>
+`;
+
+exports[`ActionsDropdown should render correctly 2`] = `
+<Dropdown
+  className="foo"
+  onOpen={[MockFunction]}
+  overlay={
+    <ul
+      className="menu"
+    >
+      <span>
+        Hello world
+      </span>
+    </ul>
+  }
+  overlayPlacement="bottom"
+>
+  <Button
+    className="dropdown-toggle bar"
+  >
+    <SettingsIcon
+      size={14}
+    />
+    <DropdownIcon
+      className="little-spacer-left"
+    />
+  </Button>
+</Dropdown>
+`;
+
+exports[`ActionsDropdownDivider should render correctly 1`] = `
+<li
+  className="divider"
+/>
+`;
+
+exports[`ActionsDropdownItem should render correctly 1`] = `
+<li>
+  <a
+    className="foo"
+    href="#"
+    onClick={[Function]}
+  >
+    <span>
+      Hello world
+    </span>
+  </a>
+</li>
+`;
+
+exports[`ActionsDropdownItem should render correctly 2`] = `
+<li>
+  <Link
+    className="foo text-danger"
+    id="baz"
+    onlyActiveOnIndex={false}
+    style={Object {}}
+    to="path/name"
+  >
+    <span>
+      Hello world
+    </span>
+  </Link>
+</li>
+`;
+
+exports[`ActionsDropdownItem should render correctly 3`] = `
+<li>
+  <a
+    className="foo"
+    download="foo/bar"
+    href="path/name"
+  >
+    <span>
+      Hello world
+    </span>
+  </a>
+</li>
+`;
+
+exports[`ActionsDropdownItem should render correctly copy item 1`] = `
+<ActionsDropdownItem
+  className="foo"
+  copyValue="my content to copy to clipboard"
+>
+  <ClipboardBase>
+    <Tooltip
+      overlay="copied_action"
+      visible={false}
+    >
+      <TooltipInner
+        mouseEnterDelay={0.1}
+        overlay="copied_action"
+        visible={false}
+      >
+        <li
+          data-clipboard-text="my content to copy to clipboard"
+          onMouseEnter={[Function]}
+          onMouseLeave={[Function]}
+        >
+          <a
+            className="foo"
+            href="#"
+            onClick={[Function]}
+          >
+            <span>
+              Hello world
+            </span>
+          </a>
+        </li>
+      </TooltipInner>
+    </Tooltip>
+  </ClipboardBase>
+</ActionsDropdownItem>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/BackButton-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/BackButton-test.tsx.snap
new file mode 100644 (file)
index 0000000..57a5f2c
--- /dev/null
@@ -0,0 +1,46 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should handle click 1`] = `
+<Tooltip
+  overlay="issues.return_to_list"
+>
+  <a
+    className="link-no-underline"
+    href="#"
+    onClick={[Function]}
+  >
+    <ContextConsumer>
+      <Component />
+    </ContextConsumer>
+  </a>
+</Tooltip>
+`;
+
+exports[`should render properly 1`] = `
+<Tooltip
+  overlay="issues.return_to_list"
+>
+  <a
+    className="link-no-underline"
+    href="#"
+    onClick={[Function]}
+  >
+    <ContextConsumer>
+      <Component />
+    </ContextConsumer>
+  </a>
+</Tooltip>
+`;
+
+exports[`should render properly 2`] = `
+<svg
+  height="24"
+  viewBox="0 0 21 24"
+  width="21"
+>
+  <path
+    d="M3.845 12.9992l5.993 5.993.052.056c.049.061.093.122.129.191.082.159.121.339.111.518-.006.102-.028.203-.064.298-.149.39-.537.652-.954.644-.102-.002-.204-.019-.301-.052-.148-.05-.273-.135-.387-.241l-8.407-8.407 8.407-8.407.056-.052c.061-.048.121-.092.19-.128.116-.06.237-.091.366-.108.076-.004.075-.004.153-.003.155.015.3.052.437.129.088.051.169.115.239.19.246.266.33.656.214.999-.051.149-.135.273-.241.387l-5.983 5.984c5.287-.044 10.577-.206 15.859.013.073.009.091.009.163.027.187.047.359.15.49.292.075.081.136.175.18.276.044.101.072.209.081.319.032.391-.175.775-.521.962-.097.052-.202.089-.311.107-.073.012-.091.01-.165.013H3.845z"
+    fill="#777"
+  />
+</svg>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/BoxedGroupAccordion-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/BoxedGroupAccordion-test.tsx.snap
new file mode 100644 (file)
index 0000000..0c7b74b
--- /dev/null
@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+  className="boxed-group boxed-group-accordion"
+>
+  <div
+    className="boxed-group-header"
+    onClick={[Function]}
+    role="listitem"
+  >
+    <span
+      className="boxed-group-accordion-title"
+    >
+      <OpenCloseIcon
+        className="little-spacer-right"
+        open={false}
+      />
+      Foo
+    </span>
+    <div>
+      header content
+    </div>
+  </div>
+</div>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/BoxedTabs-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/BoxedTabs-test.tsx.snap
new file mode 100644 (file)
index 0000000..22d8f76
--- /dev/null
@@ -0,0 +1,178 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+.emotion-6 {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-flex-direction: row;
+  -ms-flex-direction: row;
+  flex-direction: row;
+}
+
+.emotion-1 {
+  position: relative;
+  background-color: white;
+  border-top: 1px solid #e6e6e6;
+  border-left: 1px solid #e6e6e6;
+  border-right: none;
+  border-bottom: none;
+  margin-bottom: -1px;
+  min-width: 128px;
+  min-height: 56px;
+  outline: 0;
+  padding: calc(2 * 8px);
+}
+
+.emotion-1:last-child {
+  border-right: 1px solid #e6e6e6;
+}
+
+.emotion-0 {
+  display: block;
+  background-color: #4b9fd5;
+  height: 3px;
+  width: 100%;
+  position: absolute;
+  left: 0;
+  top: -1px;
+}
+
+.emotion-3 {
+  position: relative;
+  background-color: #f3f3f3;
+  border-top: 1px solid #e6e6e6;
+  border-left: 1px solid #e6e6e6;
+  border-right: none;
+  border-bottom: none;
+  margin-bottom: -1px;
+  min-width: 128px;
+  min-height: 56px;
+  cursor: pointer;
+  outline: 0;
+  padding: calc(2 * 8px);
+}
+
+.emotion-3:hover {
+  background-color: #f8f8f8;
+}
+
+.emotion-3:last-child {
+  border-right: 1px solid #e6e6e6;
+}
+
+.emotion-2 {
+  display: none;
+  background-color: #4b9fd5;
+  height: 3px;
+  width: 100%;
+  position: absolute;
+  left: 0;
+  top: -1px;
+}
+
+<BoxedTabs
+  className="boxed-tabs"
+  onSelect={[MockFunction]}
+  selected="a"
+  tabs={
+    Array [
+      Object {
+        "key": "a",
+        "label": "labela",
+      },
+      Object {
+        "key": "b",
+        "label": "labelb",
+      },
+      Object {
+        "key": "c",
+        "label": <span>
+          Complex label 
+          <strong>
+            !!!
+          </strong>
+        </span>,
+      },
+    ]
+  }
+>
+  <Styled(div)
+    className="boxed-tabs"
+  >
+    <div
+      className="boxed-tabs emotion-6"
+    >
+      <Styled(button)
+        active={true}
+        key="0"
+        onClick={[Function]}
+        type="button"
+      >
+        <button
+          className="emotion-1"
+          onClick={[Function]}
+          type="button"
+        >
+          <Styled(div)
+            active={true}
+          >
+            <div
+              className="emotion-0"
+            />
+          </Styled(div)>
+          labela
+        </button>
+      </Styled(button)>
+      <Styled(button)
+        active={false}
+        key="1"
+        onClick={[Function]}
+        type="button"
+      >
+        <button
+          className="emotion-3"
+          onClick={[Function]}
+          type="button"
+        >
+          <Styled(div)
+            active={false}
+          >
+            <div
+              className="emotion-2"
+            />
+          </Styled(div)>
+          labelb
+        </button>
+      </Styled(button)>
+      <Styled(button)
+        active={false}
+        key="2"
+        onClick={[Function]}
+        type="button"
+      >
+        <button
+          className="emotion-3"
+          onClick={[Function]}
+          type="button"
+        >
+          <Styled(div)
+            active={false}
+          >
+            <div
+              className="emotion-2"
+            />
+          </Styled(div)>
+          <span>
+            Complex label 
+            <strong>
+              !!!
+            </strong>
+          </span>
+        </button>
+      </Styled(button)>
+    </div>
+  </Styled(div)>
+</BoxedTabs>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/Checkbox-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/Checkbox-test.tsx.snap
new file mode 100644 (file)
index 0000000..69c0c60
--- /dev/null
@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<a
+  aria-checked={true}
+  className="icon-checkbox icon-checkbox-checked"
+  href="#"
+  onClick={[Function]}
+  role="checkbox"
+  title="Title value"
+/>
+`;
+
+exports[`should render the checkbox on the right 1`] = `
+<a
+  aria-checked={true}
+  className="icon-checkbox icon-checkbox-checked"
+  href="#"
+  onClick={[Function]}
+  role="checkbox"
+/>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ClickEventBoundary-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ClickEventBoundary-test.tsx.snap
new file mode 100644 (file)
index 0000000..62eac27
--- /dev/null
@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div>
+  <ClickEventBoundary>
+    <button
+      onClick={[Function]}
+      type="button"
+    >
+      Click me
+    </button>
+  </ClickEventBoundary>
+</div>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ConfirmButton-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ConfirmButton-test.tsx.snap
new file mode 100644 (file)
index 0000000..f94c382
--- /dev/null
@@ -0,0 +1,20 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display a confirm modal 1`] = `
+<ConfirmModal
+  confirmButtonText="submit"
+  header="title"
+  onClose={[MockFunction]}
+  onConfirm={[MockFunction]}
+>
+  <div />
+</ConfirmModal>
+`;
+
+exports[`should display a modal button 1`] = `
+<ModalButton
+  modal={[Function]}
+>
+  <Component />
+</ModalButton>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ConfirmModal-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ConfirmModal-test.tsx.snap
new file mode 100644 (file)
index 0000000..f1367ae
--- /dev/null
@@ -0,0 +1,81 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should confirm and close after confirm 1`] = `
+<footer
+  className="modal-foot"
+>
+  <DeferredSpinner
+    className="spacer-right"
+    loading={true}
+  />
+  <SubmitButton
+    autoFocus={true}
+    disabled={true}
+  >
+    confirm
+  </SubmitButton>
+  <ResetButtonLink
+    disabled={true}
+    onClick={[Function]}
+  >
+    cancel
+  </ResetButtonLink>
+</footer>
+`;
+
+exports[`should render correctly 1`] = `
+<SimpleModal
+  header="title"
+  onClose={[MockFunction]}
+  onSubmit={[Function]}
+>
+  <Component />
+</SimpleModal>
+`;
+
+exports[`should render correctly 2`] = `
+<Modal
+  contentLabel="title"
+  onRequestClose={[MockFunction]}
+>
+  <ClickEventBoundary>
+    <form
+      onSubmit={[Function]}
+    >
+      <header
+        className="modal-head"
+      >
+        <h2>
+          title
+        </h2>
+      </header>
+      <div
+        className="modal-body"
+      >
+        <p>
+          My confirm message
+        </p>
+      </div>
+      <footer
+        className="modal-foot"
+      >
+        <DeferredSpinner
+          className="spacer-right"
+          loading={false}
+        />
+        <SubmitButton
+          autoFocus={true}
+        >
+          confirm
+        </SubmitButton>
+        <ResetButtonLink
+          disabled={false}
+          onClick={[Function]}
+        >
+          cancel
+        </ResetButtonLink>
+      </footer>
+    </form>
+  </ClickEventBoundary>
+</Modal>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/EscKeydownHandler-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/EscKeydownHandler-test.tsx.snap
new file mode 100644 (file)
index 0000000..239d6bf
--- /dev/null
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<span>
+  Hi there
+</span>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/FavoriteButton-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/FavoriteButton-test.tsx.snap
new file mode 100644 (file)
index 0000000..f66e2dd
--- /dev/null
@@ -0,0 +1,65 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render favorite 1`] = `
+<Tooltip
+  overlay="favorite.current.TRK"
+>
+  <ButtonLink
+    aria-label="favorite.action.remove"
+    className="favorite-link link-no-underline"
+    onClick={[MockFunction]}
+  >
+    <FavoriteIcon
+      favorite={true}
+    />
+  </ButtonLink>
+</Tooltip>
+`;
+
+exports[`should render not favorite 1`] = `
+<Tooltip
+  overlay="favorite.check.TRK"
+>
+  <ButtonLink
+    aria-label="favorite.action.add"
+    className="favorite-link link-no-underline"
+    onClick={[MockFunction]}
+  >
+    <FavoriteIcon
+      favorite={false}
+    />
+  </ButtonLink>
+</Tooltip>
+`;
+
+exports[`should update properly 1`] = `
+<Tooltip
+  overlay="favorite.check.TRK"
+>
+  <ButtonLink
+    aria-label="favorite.action.add"
+    className="favorite-link link-no-underline"
+    onClick={[MockFunction]}
+  >
+    <FavoriteIcon
+      favorite={false}
+    />
+  </ButtonLink>
+</Tooltip>
+`;
+
+exports[`should update properly 2`] = `
+<Tooltip
+  overlay="favorite.current.TRK"
+>
+  <ButtonLink
+    aria-label="favorite.action.remove"
+    className="favorite-link link-no-underline"
+    onClick={[MockFunction]}
+  >
+    <FavoriteIcon
+      favorite={true}
+    />
+  </ButtonLink>
+</Tooltip>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/GlobalMessages-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/GlobalMessages-test.tsx.snap
new file mode 100644 (file)
index 0000000..fdeaf6c
--- /dev/null
@@ -0,0 +1,212 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly with a message 1`] = `
+<Styled(div)>
+  <GlobalMessage
+    closeGlobalMessage={[MockFunction]}
+    key="1"
+    message={
+      Object {
+        "id": "1",
+        "level": "ERROR",
+        "message": "Test",
+      }
+    }
+  />
+  <GlobalMessage
+    closeGlobalMessage={[MockFunction]}
+    key="2"
+    message={
+      Object {
+        "id": "2",
+        "level": "SUCCESS",
+        "message": "Test 2",
+      }
+    }
+  />
+</Styled(div)>
+`;
+
+exports[`should render correctly with a message 2`] = `
+<Styled(div)
+  data-test="global-message__ERROR"
+  level="ERROR"
+  role="alert"
+>
+  Test
+  <Styled(ClearButton)
+    className="button-small"
+    color="#fff"
+    level="ERROR"
+    onClick={[Function]}
+  />
+</Styled(div)>
+`;
+
+exports[`should render correctly with a message 3`] = `
+<Styled(div)
+  data-test="global-message__SUCCESS"
+  level="SUCCESS"
+  role="status"
+>
+  Test 2
+  <Styled(ClearButton)
+    className="button-small"
+    color="#fff"
+    level="SUCCESS"
+    onClick={[Function]}
+  />
+</Styled(div)>
+`;
+
+exports[`should render with correct css 1`] = `
+@keyframes animation-0 {
+  from {
+    opacity: 0;
+  }
+
+  to {
+    opacity: 1;
+  }
+}
+
+@keyframes animation-0 {
+  from {
+    opacity: 0;
+  }
+
+  to {
+    opacity: 1;
+  }
+}
+
+.emotion-4 {
+  position: fixed;
+  z-index: 7000;
+  top: 0;
+  left: 50%;
+  width: 350px;
+  margin-left: -175px;
+}
+
+.emotion-1 {
+  position: relative;
+  padding: 0 30px 0 10px;
+  line-height: 24px;
+  border-radius: 0 0 3px 3px;
+  box-sizing: border-box;
+  color: #ffffff;
+  background-color: #d4333f;
+  text-align: center;
+  opacity: 0;
+  -webkit-animation: animation-0 0.2s ease forwards;
+  animation: animation-0 0.2s ease forwards;
+}
+
+.emotion-1 + .emotion-1 {
+  margin-top: calc(8px / 2);
+  border-radius: 3px;
+}
+
+.emotion-0 {
+  position: absolute;
+  top: calc(8px / 4);
+  right: calc(8px / 4);
+}
+
+.emotion-0:hover svg,
+.emotion-0:focus svg {
+  color: #d4333f;
+}
+
+.emotion-3 {
+  position: relative;
+  padding: 0 30px 0 10px;
+  line-height: 24px;
+  border-radius: 0 0 3px 3px;
+  box-sizing: border-box;
+  color: #ffffff;
+  background-color: #00aa00;
+  text-align: center;
+  opacity: 0;
+  -webkit-animation: animation-0 0.2s ease forwards;
+  animation: animation-0 0.2s ease forwards;
+}
+
+.emotion-3 + .emotion-3 {
+  margin-top: calc(8px / 2);
+  border-radius: 3px;
+}
+
+.emotion-2 {
+  position: absolute;
+  top: calc(8px / 4);
+  right: calc(8px / 4);
+}
+
+.emotion-2:hover svg,
+.emotion-2:focus svg {
+  color: #00aa00;
+}
+
+<div
+  class="emotion-4"
+>
+  <div
+    class="emotion-1"
+    data-test="global-message__ERROR"
+    role="alert"
+  >
+    Test
+    <button
+      class="button button-small emotion-0 button-icon"
+      level="ERROR"
+      style="color:#fff"
+      type="button"
+    >
+      <svg
+        height="16"
+        space="preserve"
+        style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421"
+        version="1.1"
+        viewBox="0 0 16 16"
+        width="16"
+        xlink="http://www.w3.org/1999/xlink"
+      >
+        <path
+          d="M14 4.242L11.758 2l-3.76 3.76L4.242 2 2 4.242l3.756 3.756L2 11.758 4.242 14l3.756-3.76 3.76 3.76L14 11.758l-3.76-3.76L14 4.242z"
+          style="fill:currentColor"
+        />
+      </svg>
+    </button>
+  </div>
+  <div
+    class="emotion-3"
+    data-test="global-message__SUCCESS"
+    role="status"
+  >
+    Test 2
+    <button
+      class="button button-small emotion-2 button-icon"
+      level="SUCCESS"
+      style="color:#fff"
+      type="button"
+    >
+      <svg
+        height="16"
+        space="preserve"
+        style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421"
+        version="1.1"
+        viewBox="0 0 16 16"
+        width="16"
+        xlink="http://www.w3.org/1999/xlink"
+      >
+        <path
+          d="M14 4.242L11.758 2l-3.76 3.76L4.242 2 2 4.242l3.756 3.756L2 11.758 4.242 14l3.756-3.76 3.76 3.76L14 11.758l-3.76-3.76L14 4.242z"
+          style="fill:currentColor"
+        />
+      </svg>
+    </button>
+  </div>
+</div>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/HelpTooltip-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/HelpTooltip-test.tsx.snap
new file mode 100644 (file)
index 0000000..abf28df
--- /dev/null
@@ -0,0 +1,53 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render dark helptooltip properly: dark 1`] = `
+<HelpTooltip
+  overlay={
+    <div
+      className="my-overlay"
+    />
+  }
+>
+  <ContextConsumer>
+    <Component />
+  </ContextConsumer>
+</HelpTooltip>
+`;
+
+exports[`should render dark helptooltip properly: dark icon 1`] = `
+<HelpIcon
+  fill="rgba(0, 0, 0, 0.25)"
+  fillInner="#ffffff"
+  size={14}
+/>
+`;
+
+exports[`should render properly: default 1`] = `
+<div
+  className="help-tooltip"
+>
+  <Tooltip
+    mouseLeaveDelay={0.25}
+    overlay={
+      <div
+        className="my-overlay"
+      />
+    }
+  >
+    <span
+      className="display-inline-flex-center"
+    >
+      <ContextConsumer>
+        <Component />
+      </ContextConsumer>
+    </span>
+  </Tooltip>
+</div>
+`;
+
+exports[`should render properly: default icon 1`] = `
+<HelpIcon
+  fill="#b4b4b4"
+  size={12}
+/>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/IdentityProviderLink-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/IdentityProviderLink-test.tsx.snap
new file mode 100644 (file)
index 0000000..d3a38a5
--- /dev/null
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<a
+  className="identity-provider-link"
+  href="/url/foo/bar"
+  style={
+    Object {
+      "backgroundColor": "#000",
+    }
+  }
+>
+  <img
+    alt="Foo"
+    height={20}
+    src="/some/path"
+    width={20}
+  />
+  Link text
+</a>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/InputValidationField-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/InputValidationField-test.tsx.snap
new file mode 100644 (file)
index 0000000..16f3e1c
--- /dev/null
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<ModalValidationField
+  description="Field description"
+  dirty={true}
+  error="Bad formatting"
+  label="Foo field"
+  touched={true}
+>
+  <Component />
+</ModalValidationField>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ListFooter-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ListFooter-test.tsx.snap
new file mode 100644 (file)
index 0000000..2d55aa0
--- /dev/null
@@ -0,0 +1,85 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: default 1`] = `
+<footer
+  className="spacer-top note text-center"
+>
+  x_of_y_shown.3.5
+  <Button
+    className="spacer-left"
+    data-test="show-more"
+    onClick={[MockFunction]}
+  >
+    show_more
+  </Button>
+</footer>
+`;
+
+exports[`should render correctly: empty if everything is loaded 1`] = `
+<footer
+  className="spacer-top note text-center"
+>
+  x_of_y_shown.5.5
+</footer>
+`;
+
+exports[`should render correctly: empty if no loadMore nor reload props 1`] = `
+<footer
+  className="spacer-top note text-center"
+>
+  x_of_y_shown.3.5
+</footer>
+`;
+
+exports[`should render correctly: loading 1`] = `
+<footer
+  className="spacer-top note text-center"
+>
+  x_of_y_shown.3.5
+  <Button
+    className="spacer-left"
+    data-test="show-more"
+    disabled={true}
+    onClick={[MockFunction]}
+  >
+    show_more
+  </Button>
+  <DeferredSpinner
+    className="text-bottom spacer-left position-absolute"
+  />
+</footer>
+`;
+
+exports[`should render correctly: reload 1`] = `
+<footer
+  className="spacer-top note text-center"
+>
+  x_of_y_shown.3.5
+  <Button
+    className="spacer-left"
+    data-test="reload"
+    onClick={[MockFunction]}
+  >
+    reload
+  </Button>
+</footer>
+`;
+
+exports[`should render correctly: reload, loading 1`] = `
+<footer
+  className="spacer-top note text-center"
+>
+  x_of_y_shown.3.5
+  <Button
+    className="spacer-left"
+    data-test="reload"
+    disabled={true}
+    onClick={[MockFunction]}
+  >
+    reload
+  </Button>
+  <DeferredSpinner
+    className="text-bottom spacer-left position-absolute"
+  />
+</footer>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ModalValidationField-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ModalValidationField-test.tsx.snap
new file mode 100644 (file)
index 0000000..4b4e605
--- /dev/null
@@ -0,0 +1,73 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display the field as valid 1`] = `
+<div
+  className="modal-validation-field"
+>
+  <label>
+    Foo
+  </label>
+  <input
+    className="is-valid"
+    type="text"
+  />
+  <AlertSuccessIcon
+    className="little-spacer-top"
+  />
+</div>
+`;
+
+exports[`should display the field with an error 1`] = `
+<div
+  className="modal-validation-field"
+>
+  <label>
+    Foo
+  </label>
+  <input
+    className="is-invalid"
+    type="text"
+  />
+  <AlertErrorIcon
+    className="little-spacer-top"
+  />
+  <p
+    className="text-danger"
+  >
+    Is required
+  </p>
+</div>
+`;
+
+exports[`should display the field without any error/validation 1`] = `
+<div
+  className="modal-validation-field"
+>
+  <label>
+    Foo
+  </label>
+  <input
+    className=""
+    type="text"
+  />
+  <div
+    className="modal-field-description"
+  >
+    Describe Foo.
+  </div>
+</div>
+`;
+
+exports[`should display the field without any error/validation 2`] = `
+<div
+  className="modal-validation-field"
+>
+  <label>
+    Foo
+  </label>
+  <input
+    className=""
+    type="text"
+  />
+</div>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/Radio-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/Radio-test.tsx.snap
new file mode 100644 (file)
index 0000000..8649fba
--- /dev/null
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render properly: checked 1`] = `
+<a
+  aria-checked={true}
+  className="display-inline-flex-center link-radio"
+  href="#"
+  onClick={[Function]}
+  role="radio"
+>
+  <i
+    className="icon-radio spacer-right is-checked"
+  />
+</a>
+`;
+
+exports[`should render properly: not checked 1`] = `
+<a
+  aria-checked={false}
+  className="display-inline-flex-center link-radio"
+  href="#"
+  onClick={[Function]}
+  role="radio"
+>
+  <i
+    className="icon-radio spacer-right"
+  />
+</a>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/RadioCard-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/RadioCard-test.tsx.snap
new file mode 100644 (file)
index 0000000..e58f9d7
--- /dev/null
@@ -0,0 +1,172 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should be actionable 1`] = `
+<div
+  className="radio-card radio-card-actionable"
+  onClick={[MockFunction]}
+  role="radio"
+  tabIndex={0}
+>
+  <h2
+    className="radio-card-header big-spacer-bottom"
+  >
+    <span
+      className="display-flex-center link-radio"
+    >
+      <i
+        className="icon-radio spacer-right"
+      />
+      Radio Card
+    </span>
+  </h2>
+  <div
+    className="radio-card-body"
+  >
+    <div>
+      content
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should be actionable 2`] = `
+<div
+  aria-checked={true}
+  className="radio-card radio-card-actionable selected"
+  onClick={
+    [MockFunction] {
+      "calls": Array [
+        Array [
+          Object {
+            "currentTarget": Object {
+              "blur": [Function],
+            },
+            "preventDefault": [Function],
+            "stopPropagation": [Function],
+            "target": Object {
+              "blur": [Function],
+            },
+          },
+        ],
+      ],
+      "results": Array [
+        Object {
+          "type": "return",
+          "value": undefined,
+        },
+      ],
+    }
+  }
+  role="radio"
+  tabIndex={0}
+>
+  <h2
+    className="radio-card-header big-spacer-bottom"
+  >
+    <span
+      className="display-flex-center link-radio"
+    >
+      <i
+        className="icon-radio spacer-right is-checked"
+      />
+      Radio Card
+    </span>
+    info
+  </h2>
+  <div
+    className="radio-card-body"
+  >
+    <div>
+      content
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render correctly 1`] = `
+<div
+  className="radio-card"
+  role="radio"
+  tabIndex={0}
+>
+  <h2
+    className="radio-card-header big-spacer-bottom"
+  >
+    <span
+      className="display-flex-center link-radio"
+    >
+      Radio Card
+    </span>
+    info
+  </h2>
+  <div
+    className="radio-card-body"
+  >
+    <div>
+      content
+    </div>
+  </div>
+  <div
+    className="radio-card-recommended"
+  >
+    <RecommendedIcon
+      className="spacer-right"
+    />
+    <FormattedMessage
+      defaultMessage="Recommended for you"
+      id="Recommended for you"
+      values={
+        Object {
+          "recommended": <strong>
+            recommended
+          </strong>,
+        }
+      }
+    />
+  </div>
+</div>
+`;
+
+exports[`should render correctly 2`] = `
+<div
+  className="radio-card radio-card-vertical"
+  role="radio"
+  tabIndex={0}
+>
+  <h2
+    className="radio-card-header big-spacer-bottom"
+  >
+    <span
+      className="display-flex-center link-radio"
+    >
+      Radio Card Vertical
+    </span>
+    info
+  </h2>
+  <div
+    className="radio-card-body"
+  >
+    <div>
+      content
+    </div>
+  </div>
+  <div
+    className="radio-card-recommended"
+  >
+    <RecommendedIcon
+      className="spacer-right"
+    />
+    <FormattedMessage
+      defaultMessage="Recommended for you"
+      id="Recommended for you"
+      values={
+        Object {
+          "recommended": <strong>
+            recommended
+          </strong>,
+        }
+      }
+    />
+  </div>
+</div>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/RadioToggle-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/RadioToggle-test.tsx.snap
new file mode 100644 (file)
index 0000000..791d312
--- /dev/null
@@ -0,0 +1,92 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`accepts advanced options fields 1`] = `
+<ul
+  className="radio-toggle"
+>
+  <li
+    key="one"
+  >
+    <input
+      checked={false}
+      id="sample__one"
+      name="sample"
+      onChange={[Function]}
+      type="radio"
+    />
+    <Tooltip
+      overlay="foo"
+    >
+      <label
+        htmlFor="sample__one"
+      >
+        first
+      </label>
+    </Tooltip>
+  </li>
+  <li
+    key="two"
+  >
+    <input
+      checked={false}
+      disabled={true}
+      id="sample__two"
+      name="sample"
+      onChange={[Function]}
+      type="radio"
+    />
+    <Tooltip
+      overlay="bar"
+    >
+      <label
+        htmlFor="sample__two"
+      >
+        second
+      </label>
+    </Tooltip>
+  </li>
+</ul>
+`;
+
+exports[`renders 1`] = `
+<ul
+  className="radio-toggle"
+>
+  <li
+    key="one"
+  >
+    <input
+      checked={false}
+      id="sample__one"
+      name="sample"
+      onChange={[Function]}
+      type="radio"
+    />
+    <Tooltip>
+      <label
+        htmlFor="sample__one"
+      >
+        first
+      </label>
+    </Tooltip>
+  </li>
+  <li
+    key="two"
+  >
+    <input
+      checked={false}
+      id="sample__two"
+      name="sample"
+      onChange={[Function]}
+      type="radio"
+    />
+    <Tooltip>
+      <label
+        htmlFor="sample__two"
+      >
+        second
+      </label>
+    </Tooltip>
+  </li>
+</ul>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ReloadButton-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ReloadButton-test.tsx.snap
new file mode 100644 (file)
index 0000000..9510ca6
--- /dev/null
@@ -0,0 +1,46 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should handle click 1`] = `
+<Tooltip
+  overlay="reload"
+>
+  <a
+    className="link-no-underline"
+    href="#"
+    onClick={[Function]}
+  >
+    <ContextConsumer>
+      <Component />
+    </ContextConsumer>
+  </a>
+</Tooltip>
+`;
+
+exports[`should render properly 1`] = `
+<Tooltip
+  overlay="reload"
+>
+  <a
+    className="link-no-underline"
+    href="#"
+    onClick={[Function]}
+  >
+    <ContextConsumer>
+      <Component />
+    </ContextConsumer>
+  </a>
+</Tooltip>
+`;
+
+exports[`should render properly 2`] = `
+<svg
+  height="24"
+  viewBox="0 0 18 24"
+  width="18"
+>
+  <path
+    d="M16.6454 8.1084c-.3-.5-.9-.7-1.4-.4-.5.3-.7.9-.4 1.4.9 1.6 1.1 3.4.6 5.1-.5 1.7-1.7 3.2-3.2 4-3.3 1.8-7.4.6-9.1-2.7-1.8-3.1-.8-6.9 2.1-8.8v3.3h2v-7h-7v2h3.9c-3.7 2.5-5 7.5-2.8 11.4 1.6 3 4.6 4.6 7.7 4.6 1.4 0 2.8-.3 4.2-1.1 2-1.1 3.5-3 4.2-5.2.6-2.2.3-4.6-.8-6.6z"
+    fill="#777"
+  />
+</svg>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/SearchBox-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/SearchBox-test.tsx.snap
new file mode 100644 (file)
index 0000000..4ed69eb
--- /dev/null
@@ -0,0 +1,37 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<div
+  className="search-box"
+  title=""
+>
+  <input
+    aria-label="search_verb"
+    autoComplete="off"
+    className="search-box-input"
+    maxLength={150}
+    onChange={[Function]}
+    onKeyDown={[Function]}
+    placeholder="placeholder"
+    type="search"
+    value="foo"
+  />
+  <DeferredSpinner
+    loading={false}
+  >
+    <SearchIcon
+      className="search-box-magnifier"
+    />
+  </DeferredSpinner>
+  <ClearButton
+    aria-label="clear"
+    className="button-tiny search-box-clear"
+    iconProps={
+      Object {
+        "size": 12,
+      }
+    }
+    onClick={[Function]}
+  />
+</div>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/SearchSelect-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/SearchSelect-test.tsx.snap
new file mode 100644 (file)
index 0000000..792343f
--- /dev/null
@@ -0,0 +1,17 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render Select 1`] = `
+<Select
+  autoFocus={true}
+  escapeClearsValue={false}
+  filterOption={[Function]}
+  isLoading={false}
+  noResultsText="select2.tooShort.2"
+  onBlurResetsInput={true}
+  onChange={[Function]}
+  onInputChange={[Function]}
+  options={Array []}
+  placeholder="search_verb"
+  searchable={true}
+/>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/SelectList-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/SelectList-test.tsx.snap
new file mode 100644 (file)
index 0000000..14d46bb
--- /dev/null
@@ -0,0 +1,558 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should cancel filter selection when search is active 1`] = `
+<div
+  className="select-list"
+>
+  <div
+    className="display-flex-center"
+  >
+    <RadioToggle
+      className="select-list-filter spacer-right"
+      disabled={false}
+      name="filter"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "disabled": false,
+            "label": "selected",
+            "value": "selected",
+          },
+          Object {
+            "disabled": false,
+            "label": "unselected",
+            "value": "deselected",
+          },
+          Object {
+            "disabled": false,
+            "label": "all",
+            "value": "all",
+          },
+        ]
+      }
+      value="deselected"
+    />
+    <SearchBox
+      autoFocus={true}
+      loading={false}
+      onChange={[Function]}
+      placeholder="search_verb"
+      value=""
+    />
+  </div>
+  <SelectListListContainer
+    disabledElements={
+      Array [
+        "bar",
+      ]
+    }
+    elements={
+      Array [
+        "foo",
+        "bar",
+        "baz",
+      ]
+    }
+    filter="deselected"
+    onSelect={[MockFunction]}
+    onUnselect={[MockFunction]}
+    renderElement={[Function]}
+    selectedElements={
+      Array [
+        "foo",
+      ]
+    }
+  />
+</div>
+`;
+
+exports[`should cancel filter selection when search is active 2`] = `
+<div
+  className="select-list"
+>
+  <div
+    className="display-flex-center"
+  >
+    <RadioToggle
+      className="select-list-filter spacer-right"
+      disabled={false}
+      name="filter"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "disabled": true,
+            "label": "selected",
+            "value": "selected",
+          },
+          Object {
+            "disabled": true,
+            "label": "unselected",
+            "value": "deselected",
+          },
+          Object {
+            "disabled": true,
+            "label": "all",
+            "value": "all",
+          },
+        ]
+      }
+      value="deselected"
+    />
+    <SearchBox
+      autoFocus={true}
+      loading={false}
+      onChange={[Function]}
+      placeholder="search_verb"
+      value="test"
+    />
+  </div>
+  <SelectListListContainer
+    disabledElements={
+      Array [
+        "bar",
+      ]
+    }
+    elements={
+      Array [
+        "foo",
+        "bar",
+        "baz",
+      ]
+    }
+    filter="all"
+    onSelect={[MockFunction]}
+    onUnselect={[MockFunction]}
+    renderElement={[Function]}
+    selectedElements={
+      Array [
+        "foo",
+      ]
+    }
+  />
+</div>
+`;
+
+exports[`should cancel filter selection when search is active 3`] = `
+<div
+  className="select-list"
+>
+  <div
+    className="display-flex-center"
+  >
+    <RadioToggle
+      className="select-list-filter spacer-right"
+      disabled={false}
+      name="filter"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "disabled": false,
+            "label": "selected",
+            "value": "selected",
+          },
+          Object {
+            "disabled": false,
+            "label": "unselected",
+            "value": "deselected",
+          },
+          Object {
+            "disabled": false,
+            "label": "all",
+            "value": "all",
+          },
+        ]
+      }
+      value="deselected"
+    />
+    <SearchBox
+      autoFocus={true}
+      loading={false}
+      onChange={[Function]}
+      placeholder="search_verb"
+      value=""
+    />
+  </div>
+  <SelectListListContainer
+    disabledElements={
+      Array [
+        "bar",
+      ]
+    }
+    elements={
+      Array [
+        "foo",
+        "bar",
+        "baz",
+      ]
+    }
+    filter="deselected"
+    onSelect={[MockFunction]}
+    onUnselect={[MockFunction]}
+    renderElement={[Function]}
+    selectedElements={
+      Array [
+        "foo",
+      ]
+    }
+  />
+</div>
+`;
+
+exports[`should display a loader when searching 1`] = `
+<div
+  className="select-list"
+>
+  <div
+    className="display-flex-center"
+  >
+    <RadioToggle
+      className="select-list-filter spacer-right"
+      disabled={false}
+      name="filter"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "disabled": false,
+            "label": "selected",
+            "value": "selected",
+          },
+          Object {
+            "disabled": false,
+            "label": "unselected",
+            "value": "deselected",
+          },
+          Object {
+            "disabled": false,
+            "label": "all",
+            "value": "all",
+          },
+        ]
+      }
+      value="selected"
+    />
+    <SearchBox
+      autoFocus={true}
+      loading={true}
+      onChange={[Function]}
+      placeholder="search_verb"
+      value=""
+    />
+  </div>
+  <SelectListListContainer
+    disabledElements={
+      Array [
+        "bar",
+      ]
+    }
+    elements={
+      Array [
+        "foo",
+        "bar",
+        "baz",
+      ]
+    }
+    filter="selected"
+    onSelect={[MockFunction]}
+    onUnselect={[MockFunction]}
+    renderElement={[Function]}
+    selectedElements={
+      Array [
+        "foo",
+      ]
+    }
+  />
+</div>
+`;
+
+exports[`should display pagination element properly and call search method with correct parameters 1`] = `
+<div
+  className="select-list"
+>
+  <div
+    className="display-flex-center"
+  >
+    <RadioToggle
+      className="select-list-filter spacer-right"
+      disabled={false}
+      name="filter"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "disabled": false,
+            "label": "selected",
+            "value": "selected",
+          },
+          Object {
+            "disabled": false,
+            "label": "unselected",
+            "value": "deselected",
+          },
+          Object {
+            "disabled": false,
+            "label": "all",
+            "value": "all",
+          },
+        ]
+      }
+      value="selected"
+    />
+    <SearchBox
+      autoFocus={true}
+      loading={true}
+      onChange={[Function]}
+      placeholder="search_verb"
+      value=""
+    />
+  </div>
+  <SelectListListContainer
+    disabledElements={
+      Array [
+        "bar",
+      ]
+    }
+    elements={
+      Array [
+        "foo",
+        "bar",
+        "baz",
+      ]
+    }
+    filter="selected"
+    onSelect={[MockFunction]}
+    onUnselect={[MockFunction]}
+    renderElement={[Function]}
+    selectedElements={
+      Array [
+        "foo",
+      ]
+    }
+  />
+  <ListFooter
+    count={3}
+    loadMore={[Function]}
+    reload={[Function]}
+    total={100}
+  />
+</div>
+`;
+
+exports[`should display pagination element properly and call search method with correct parameters 2`] = `
+<div
+  className="select-list"
+>
+  <div
+    className="display-flex-center"
+  >
+    <RadioToggle
+      className="select-list-filter spacer-right"
+      disabled={false}
+      name="filter"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "disabled": false,
+            "label": "selected",
+            "value": "selected",
+          },
+          Object {
+            "disabled": false,
+            "label": "unselected",
+            "value": "deselected",
+          },
+          Object {
+            "disabled": false,
+            "label": "all",
+            "value": "all",
+          },
+        ]
+      }
+      value="selected"
+    />
+    <SearchBox
+      autoFocus={true}
+      loading={true}
+      onChange={[Function]}
+      placeholder="search_verb"
+      value=""
+    />
+  </div>
+  <SelectListListContainer
+    disabledElements={
+      Array [
+        "bar",
+      ]
+    }
+    elements={
+      Array [
+        "foo",
+        "bar",
+        "baz",
+      ]
+    }
+    filter="selected"
+    onSelect={[MockFunction]}
+    onUnselect={[MockFunction]}
+    renderElement={[Function]}
+    selectedElements={
+      Array [
+        "foo",
+      ]
+    }
+  />
+  <ListFooter
+    count={3}
+    loadMore={[Function]}
+    needReload={true}
+    reload={[Function]}
+    total={100}
+  />
+</div>
+`;
+
+exports[`should display properly with advanced features 1`] = `
+<div
+  className="select-list"
+>
+  <div
+    className="display-flex-center"
+  >
+    <RadioToggle
+      className="select-list-filter spacer-right"
+      disabled={false}
+      name="filter"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "disabled": false,
+            "label": "selected",
+            "value": "selected",
+          },
+          Object {
+            "disabled": false,
+            "label": "unselected",
+            "value": "deselected",
+          },
+          Object {
+            "disabled": false,
+            "label": "all",
+            "value": "all",
+          },
+        ]
+      }
+      value="selected"
+    />
+    <SearchBox
+      autoFocus={true}
+      loading={false}
+      onChange={[Function]}
+      placeholder="search_verb"
+      value=""
+    />
+  </div>
+  <SelectListListContainer
+    allowBulkSelection={true}
+    disabledElements={
+      Array [
+        "bar",
+      ]
+    }
+    elements={
+      Array [
+        "foo",
+        "bar",
+        "baz",
+      ]
+    }
+    filter="selected"
+    onSelect={[MockFunction]}
+    onUnselect={[MockFunction]}
+    readOnly={true}
+    renderElement={[Function]}
+    selectedElements={
+      Array [
+        "foo",
+      ]
+    }
+  />
+  <ListFooter
+    count={3}
+    loadMore={[Function]}
+    reload={[Function]}
+    total={125}
+  />
+</div>
+`;
+
+exports[`should display properly with basics features 1`] = `
+<div
+  className="select-list"
+>
+  <div
+    className="display-flex-center"
+  >
+    <RadioToggle
+      className="select-list-filter spacer-right"
+      disabled={false}
+      name="filter"
+      onCheck={[Function]}
+      options={
+        Array [
+          Object {
+            "disabled": false,
+            "label": "selected",
+            "value": "selected",
+          },
+          Object {
+            "disabled": false,
+            "label": "unselected",
+            "value": "deselected",
+          },
+          Object {
+            "disabled": false,
+            "label": "all",
+            "value": "all",
+          },
+        ]
+      }
+      value="selected"
+    />
+    <SearchBox
+      autoFocus={true}
+      loading={false}
+      onChange={[Function]}
+      placeholder="search_verb"
+      value=""
+    />
+  </div>
+  <SelectListListContainer
+    disabledElements={
+      Array [
+        "bar",
+      ]
+    }
+    elements={
+      Array [
+        "foo",
+        "bar",
+        "baz",
+      ]
+    }
+    filter="selected"
+    onSelect={[MockFunction]}
+    onUnselect={[MockFunction]}
+    renderElement={[Function]}
+    selectedElements={
+      Array [
+        "foo",
+      ]
+    }
+  />
+</div>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/SelectListListContainer-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/SelectListListContainer-test.tsx.snap
new file mode 100644 (file)
index 0000000..bf96116
--- /dev/null
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+  className="select-list-list-container spacer-top"
+>
+  <ul
+    className="menu"
+  >
+    <li>
+      <Checkbox
+        checked={true}
+        disabled={false}
+        onCheck={[Function]}
+        thirdState={true}
+      >
+        <span
+          className="big-spacer-left"
+        >
+          bulk_change
+          <DeferredSpinner
+            className="spacer-left"
+            loading={false}
+            timeout={10}
+          />
+        </span>
+      </Checkbox>
+    </li>
+    <li
+      className="divider"
+    />
+    <SelectListListElement
+      disabled={false}
+      element="foo"
+      key="foo"
+      onSelect={[MockFunction]}
+      onUnselect={[MockFunction]}
+      renderElement={[Function]}
+      selected={true}
+    />
+    <SelectListListElement
+      disabled={false}
+      element="bar"
+      key="bar"
+      onSelect={[MockFunction]}
+      onUnselect={[MockFunction]}
+      renderElement={[Function]}
+      selected={false}
+    />
+    <SelectListListElement
+      disabled={false}
+      element="baz"
+      key="baz"
+      onSelect={[MockFunction]}
+      onUnselect={[MockFunction]}
+      renderElement={[Function]}
+      selected={false}
+    />
+  </ul>
+</div>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/SelectListListElement-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/SelectListListElement-test.tsx.snap
new file mode 100644 (file)
index 0000000..e5d4ba3
--- /dev/null
@@ -0,0 +1,41 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display a loader when checking 1`] = `
+<li
+  className=""
+>
+  <Checkbox
+    checked={false}
+    className="select-list-list-checkbox"
+    loading={false}
+    onCheck={[Function]}
+    thirdState={false}
+  >
+    <span
+      className="little-spacer-left"
+    >
+      foo
+    </span>
+  </Checkbox>
+</li>
+`;
+
+exports[`should display a loader when checking 2`] = `
+<li
+  className=""
+>
+  <Checkbox
+    checked={false}
+    className="select-list-list-checkbox"
+    loading={true}
+    onCheck={[Function]}
+    thirdState={false}
+  >
+    <span
+      className="little-spacer-left"
+    >
+      foo
+    </span>
+  </Checkbox>
+</li>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/SimpleModal-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/SimpleModal-test.tsx.snap
new file mode 100644 (file)
index 0000000..49b14a9
--- /dev/null
@@ -0,0 +1,52 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<Modal
+  contentLabel=""
+  onRequestClose={[MockFunction]}
+>
+  <div />
+</Modal>
+`;
+
+exports[`submits 1`] = `
+<Modal
+  contentLabel=""
+  onRequestClose={[MockFunction]}
+>
+  <Button
+    disabled={false}
+    onClick={[Function]}
+  >
+    close
+  </Button>
+</Modal>
+`;
+
+exports[`submits 2`] = `
+<Modal
+  contentLabel=""
+  onRequestClose={[MockFunction]}
+>
+  <Button
+    disabled={true}
+    onClick={[Function]}
+  >
+    close
+  </Button>
+</Modal>
+`;
+
+exports[`submits 3`] = `
+<Modal
+  contentLabel=""
+  onRequestClose={[MockFunction]}
+>
+  <Button
+    disabled={false}
+    onClick={[Function]}
+  >
+    close
+  </Button>
+</Modal>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/Tabs-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/Tabs-test.tsx.snap
new file mode 100644 (file)
index 0000000..2db4cec
--- /dev/null
@@ -0,0 +1,52 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should disable single tab 1`] = `
+<li>
+  <a
+    className="js-foo disabled selected"
+    href="#"
+    onClick={[Function]}
+  >
+    <span>
+      Foo
+    </span>
+  </a>
+</li>
+`;
+
+exports[`should render correctly 1`] = `
+<ul
+  className="flex-tabs"
+>
+  <Tab
+    key="foo"
+    name="foo"
+    onSelect={[MockFunction]}
+    selected={false}
+  >
+    Foo
+  </Tab>
+  <Tab
+    key="bar"
+    name="bar"
+    onSelect={[MockFunction]}
+    selected={true}
+  >
+    Bar
+  </Tab>
+</ul>
+`;
+
+exports[`should render single tab correctly 1`] = `
+<li>
+  <a
+    className="js-foo selected"
+    href="#"
+    onClick={[Function]}
+  >
+    <span>
+      Foo
+    </span>
+  </a>
+</li>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/Toggle-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/Toggle-test.tsx.snap
new file mode 100644 (file)
index 0000000..8862993
--- /dev/null
@@ -0,0 +1,55 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: disabled 1`] = `
+<Button
+  className="boolean-toggle boolean-toggle-on"
+  disabled={true}
+  name="toggle-name"
+  onClick={[Function]}
+>
+  <div
+    aria-label="on"
+    className="boolean-toggle-handle"
+  >
+    <CheckIcon
+      size={12}
+    />
+  </div>
+</Button>
+`;
+
+exports[`should render correctly: off 1`] = `
+<Button
+  className="boolean-toggle"
+  disabled={true}
+  name="toggle-name"
+  onClick={[Function]}
+>
+  <div
+    aria-label="off"
+    className="boolean-toggle-handle"
+  >
+    <CheckIcon
+      size={12}
+    />
+  </div>
+</Button>
+`;
+
+exports[`should render correctly: on 1`] = `
+<Button
+  className="boolean-toggle boolean-toggle-on"
+  disabled={true}
+  name="toggle-name"
+  onClick={[Function]}
+>
+  <div
+    aria-label="on"
+    className="boolean-toggle-handle"
+  >
+    <CheckIcon
+      size={12}
+    />
+  </div>
+</Button>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/Toggler-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/Toggler-test.tsx.snap
new file mode 100644 (file)
index 0000000..dfe5d96
--- /dev/null
@@ -0,0 +1,58 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should not render click wrappers 1`] = `
+<Fragment>
+  <div
+    id="toggle"
+  />
+  <div
+    id="overlay"
+  />
+</Fragment>
+`;
+
+exports[`should render children and overlay 1`] = `
+<Fragment>
+  <div
+    id="toggle"
+  />
+  <OutsideClickHandler
+    onClickOutside={[MockFunction]}
+  >
+    <EscKeydownHandler
+      onKeydown={[MockFunction]}
+    >
+      <div
+        id="overlay"
+      />
+    </EscKeydownHandler>
+  </OutsideClickHandler>
+</Fragment>
+`;
+
+exports[`should render only children 1`] = `
+<Fragment>
+  <div
+    id="toggle"
+  />
+</Fragment>
+`;
+
+exports[`should render when closeOnClick=true 1`] = `
+<Fragment>
+  <div
+    id="toggle"
+  />
+  <DocumentClickHandler
+    onClick={[MockFunction]}
+  >
+    <EscKeydownHandler
+      onKeydown={[MockFunction]}
+    >
+      <div
+        id="overlay"
+      />
+    </EscKeydownHandler>
+  </DocumentClickHandler>
+</Fragment>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/Tooltip-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/Tooltip-test.tsx.snap
new file mode 100644 (file)
index 0000000..786b1bf
--- /dev/null
@@ -0,0 +1,40 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should not render empty tooltips 1`] = `
+<div
+  id="tooltip"
+/>
+`;
+
+exports[`should not render empty tooltips 2`] = `
+<div
+  id="tooltip"
+/>
+`;
+
+exports[`should render 1`] = `
+<Fragment>
+  <div
+    id="tooltip"
+    onMouseEnter={[Function]}
+    onMouseLeave={[Function]}
+  />
+</Fragment>
+`;
+
+exports[`should render 2`] = `
+<Fragment>
+  <div
+    id="tooltip"
+    onMouseEnter={[Function]}
+    onMouseLeave={[Function]}
+  />
+  <TooltipPortal>
+    <WithTheme(ScreenPositionFixer)
+      ready={false}
+    >
+      <Component />
+    </WithTheme(ScreenPositionFixer)>
+  </TooltipPortal>
+</Fragment>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ValidationForm-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ValidationForm-test.tsx.snap
new file mode 100644 (file)
index 0000000..e00f009
--- /dev/null
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render and submit 1`] = `
+<Formik
+  enableReinitialize={false}
+  initialValues={
+    Object {
+      "foo": "bar",
+    }
+  }
+  isInitialValid={false}
+  onSubmit={[Function]}
+  validate={[MockFunction]}
+  validateOnBlur={true}
+  validateOnChange={true}
+>
+  <Component />
+</Formik>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap
new file mode 100644 (file)
index 0000000..c2d68a1
--- /dev/null
@@ -0,0 +1,98 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<div>
+  <label
+    htmlFor="field-id"
+  >
+    <span
+      className="text-middle"
+    >
+      <strong>
+        Field label
+      </strong>
+      <MandatoryFieldMarker />
+    </span>
+    <HelpTooltip
+      className="spacer-left"
+      overlay="Help message"
+    />
+  </label>
+  <div
+    className="little-spacer-top spacer-bottom"
+  >
+    <div />
+  </div>
+  <div
+    className="note abs-width-400"
+  >
+    My description
+  </div>
+</div>
+`;
+
+exports[`should render when valid 1`] = `
+<div>
+  <label
+    htmlFor="field-id"
+  >
+    <span
+      className="text-middle"
+    >
+      <strong>
+        Field label
+      </strong>
+      <MandatoryFieldMarker />
+    </span>
+  </label>
+  <div
+    className="little-spacer-top spacer-bottom"
+  >
+    <div />
+    <AlertSuccessIcon
+      className="spacer-left text-middle"
+    />
+  </div>
+  <div
+    className="note abs-width-400"
+  >
+    My description
+  </div>
+</div>
+`;
+
+exports[`should render with error 1`] = `
+<div>
+  <label
+    htmlFor="field-id"
+  >
+    <span
+      className="text-middle"
+    >
+      <strong>
+        Field label
+      </strong>
+    </span>
+  </label>
+  <div
+    className="little-spacer-top spacer-bottom"
+  >
+    <div />
+    <AlertErrorIcon
+      className="spacer-left text-middle"
+    />
+    <span
+      className="little-spacer-left text-danger text-middle"
+    >
+      Field error message
+    </span>
+  </div>
+  <div
+    className="note abs-width-400"
+  >
+    <div>
+      My description
+    </div>
+  </div>
+</div>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ValidationModal-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/ValidationModal-test.tsx.snap
new file mode 100644 (file)
index 0000000..67db997
--- /dev/null
@@ -0,0 +1,110 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Modal
+  contentLabel="title"
+  onRequestClose={[MockFunction]}
+>
+  <ValidationForm
+    initialValues={
+      Object {
+        "field": "foo",
+      }
+    }
+    isInitialValid={true}
+    onSubmit={[Function]}
+    validate={[MockFunction]}
+  >
+    <Component />
+  </ValidationForm>
+</Modal>
+`;
+
+exports[`should render correctly 2`] = `
+<ContextProvider
+  value={
+    Object {
+      "dirty": false,
+      "errors": Object {},
+      "handleBlur": [Function],
+      "handleChange": [Function],
+      "handleReset": [Function],
+      "handleSubmit": [Function],
+      "initialValues": Object {
+        "field": "foo",
+      },
+      "isSubmitting": false,
+      "isValid": true,
+      "isValidating": false,
+      "registerField": [Function],
+      "resetForm": [Function],
+      "setError": [Function],
+      "setErrors": [Function],
+      "setFieldError": [Function],
+      "setFieldTouched": [Function],
+      "setFieldValue": [Function],
+      "setFormikState": [Function],
+      "setStatus": [Function],
+      "setSubmitting": [Function],
+      "setTouched": [Function],
+      "setValues": [Function],
+      "submitCount": 0,
+      "submitForm": [Function],
+      "touched": Object {},
+      "unregisterField": [Function],
+      "validate": [MockFunction],
+      "validateField": [Function],
+      "validateForm": [Function],
+      "validateOnBlur": true,
+      "validateOnChange": true,
+      "validationSchema": undefined,
+      "values": Object {
+        "field": "foo",
+      },
+    }
+  }
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <header
+      className="modal-head"
+    >
+      <h2>
+        title
+      </h2>
+    </header>
+    <div
+      className="modal-body"
+    >
+      <input
+        name="field"
+        onBlur={[Function]}
+        onChange={[Function]}
+        type="text"
+        value="foo"
+      />
+    </div>
+    <footer
+      className="modal-foot"
+    >
+      <DeferredSpinner
+        className="spacer-right"
+        loading={false}
+      />
+      <SubmitButton
+        className="button-red"
+        disabled={true}
+      >
+        confirm
+      </SubmitButton>
+      <ResetButtonLink
+        disabled={false}
+        onClick={[MockFunction]}
+      >
+        cancel
+      </ResetButtonLink>
+    </footer>
+  </form>
+</ContextProvider>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/buttons-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/buttons-test.tsx.snap
new file mode 100644 (file)
index 0000000..8d915b8
--- /dev/null
@@ -0,0 +1,31 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Button should render correctly 1`] = `
+<button
+  className="button"
+  onClick={[Function]}
+  type="button"
+>
+  My button
+</button>
+`;
+
+exports[`ButtonIcon should render correctly 1`] = `
+<Tooltip
+  mouseEnterDelay={0.4}
+  overlay="my tooltip"
+  visible={true}
+>
+  <Button
+    className="button-icon"
+    stopPropagation={true}
+    style={
+      Object {
+        "color": "#236a97",
+      }
+    }
+  >
+    <i />
+  </Button>
+</Tooltip>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/__snapshots__/clipboard-test.tsx.snap b/server/sonar-ui-common/components/controls/__tests__/__snapshots__/clipboard-test.tsx.snap
new file mode 100644 (file)
index 0000000..de2081c
--- /dev/null
@@ -0,0 +1,52 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ClipboardBase should display correctly 1`] = `
+<Button>
+  copy
+</Button>
+`;
+
+exports[`ClipboardButton should display correctly 1`] = `
+<Tooltip
+  overlay="copied_action"
+  visible={false}
+>
+  <Button
+    className="no-select"
+    data-clipboard-text="foo"
+    innerRef={[Function]}
+  >
+    <CopyIcon
+      className="little-spacer-right"
+    />
+    copy
+  </Button>
+</Tooltip>
+`;
+
+exports[`ClipboardButton should render a custom label if provided 1`] = `
+<Tooltip
+  overlay="copied_action"
+  visible={false}
+>
+  <Button
+    className="no-select"
+    data-clipboard-text="foo"
+    innerRef={[Function]}
+  >
+    Foo Bar
+  </Button>
+</Tooltip>
+`;
+
+exports[`ClipboardIconButton should display correctly 1`] = `
+<ButtonIcon
+  aria-label="copy_to_clipboard"
+  className="no-select"
+  data-clipboard-text="foo"
+  innerRef={[Function]}
+  tooltip="copy_to_clipboard"
+>
+  <CopyIcon />
+</ButtonIcon>
+`;
diff --git a/server/sonar-ui-common/components/controls/__tests__/buttons-test.tsx b/server/sonar-ui-common/components/controls/__tests__/buttons-test.tsx
new file mode 100644 (file)
index 0000000..59e554f
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { click, mockEvent } from '../../../helpers/testUtils';
+import { Button, ButtonIcon, ButtonIconProps } from '../buttons';
+
+describe('Button', () => {
+  it('should render correctly', () => {
+    const onClick = jest.fn();
+    const preventDefault = jest.fn();
+    const stopPropagation = jest.fn();
+    const wrapper = shallowRender({ onClick });
+    expect(wrapper).toMatchSnapshot();
+    click(wrapper.find('button'), mockEvent({ preventDefault, stopPropagation }));
+    expect(onClick).toBeCalled();
+    expect(preventDefault).toBeCalled();
+    expect(stopPropagation).not.toBeCalled();
+  });
+
+  it('should not stop propagation, but prevent default of the click event', () => {
+    const preventDefault = jest.fn();
+    const stopPropagation = jest.fn();
+    const wrapper = shallowRender({ preventDefault: false, stopPropagation: true });
+    click(wrapper.find('button'), mockEvent({ preventDefault, stopPropagation }));
+    expect(preventDefault).not.toBeCalled();
+    expect(stopPropagation).toBeCalled();
+  });
+
+  it('should disable buttons with a class', () => {
+    const preventDefault = jest.fn();
+    const onClick = jest.fn();
+    const button = shallowRender({ disabled: true, onClick, preventDefault: false }).find('button');
+    expect(button.props().disabled).toBeUndefined();
+    expect(button.props().className).toContain('disabled');
+    expect(button.props()['aria-disabled']).toBe(true);
+    click(button, mockEvent({ preventDefault }));
+    expect(onClick).not.toBeCalled();
+    expect(preventDefault).toBeCalled();
+  });
+
+  function shallowRender(props: Partial<Button['props']> = {}) {
+    return shallow<Button>(<Button {...props}>My button</Button>);
+  }
+});
+
+describe('ButtonIcon', () => {
+  it('should render correctly', () => {
+    const wrapper = shallowRender();
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  function shallowRender(props: Partial<ButtonIconProps> = {}) {
+    return shallow(
+      <ButtonIcon tooltip="my tooltip" tooltipProps={{ visible: true }} {...props}>
+        <i />
+      </ButtonIcon>
+    ).dive();
+  }
+});
diff --git a/server/sonar-ui-common/components/controls/__tests__/clipboard-test.tsx b/server/sonar-ui-common/components/controls/__tests__/clipboard-test.tsx
new file mode 100644 (file)
index 0000000..0563651
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { mount, shallow } from 'enzyme';
+import * as React from 'react';
+import { Button } from '../buttons';
+import { ClipboardBase, ClipboardButton, ClipboardIconButton } from '../clipboard';
+
+const constructor = jest.fn();
+const destroy = jest.fn();
+const on = jest.fn();
+
+jest.mock(
+  'clipboard',
+  () =>
+    function (...args: any) {
+      constructor(...args);
+      return {
+        destroy,
+        on,
+      };
+    }
+);
+
+jest.useFakeTimers();
+
+describe('ClipboardBase', () => {
+  it('should display correctly', () => {
+    const children = jest.fn().mockReturnValue(<Button>copy</Button>);
+    const wrapper = shallowRender(children);
+    const instance = wrapper.instance();
+    expect(wrapper).toMatchSnapshot();
+    instance.handleSuccessCopy();
+    expect(children).toBeCalledWith({ copySuccess: true, setCopyButton: instance.setCopyButton });
+    jest.runAllTimers();
+    expect(children).toBeCalledWith({ copySuccess: false, setCopyButton: instance.setCopyButton });
+  });
+
+  it('should allow its content to be copied', () => {
+    const wrapper = mountRender(({ setCopyButton }) => (
+      <Button innerRef={setCopyButton}>click</Button>
+    ));
+    const button = wrapper.find('button').getDOMNode();
+    const instance = wrapper.instance();
+
+    expect(constructor).toBeCalledWith(button);
+    expect(on).toBeCalledWith('success', instance.handleSuccessCopy);
+
+    jest.clearAllMocks();
+
+    wrapper.unmount();
+    expect(destroy).toBeCalled();
+  });
+
+  function shallowRender(children?: ClipboardBase['props']['children']) {
+    return shallow<ClipboardBase>(<ClipboardBase>{children}</ClipboardBase>);
+  }
+
+  function mountRender(children?: ClipboardBase['props']['children']) {
+    return mount<ClipboardBase>(<ClipboardBase>{children}</ClipboardBase>);
+  }
+});
+
+describe('ClipboardButton', () => {
+  it('should display correctly', () => {
+    expect(shallowRender()).toMatchSnapshot();
+  });
+
+  it('should render a custom label if provided', () => {
+    expect(shallowRender('Foo Bar')).toMatchSnapshot();
+  });
+
+  function shallowRender(children?: React.ReactNode) {
+    return shallow(<ClipboardButton copyValue="foo">{children}</ClipboardButton>).dive();
+  }
+});
+
+describe('ClipboardIconButton', () => {
+  it('should display correctly', () => {
+    expect(shallowRender()).toMatchSnapshot();
+  });
+
+  function shallowRender() {
+    return shallow(<ClipboardIconButton copyValue="foo" />).dive();
+  }
+});
diff --git a/server/sonar-ui-common/components/controls/buttons.css b/server/sonar-ui-common/components/controls/buttons.css
new file mode 100644 (file)
index 0000000..25a6944
--- /dev/null
@@ -0,0 +1,322 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.button {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  vertical-align: middle;
+  height: var(--controlHeight);
+  line-height: calc(var(--controlHeight) - 2px);
+  padding: 0 var(--gridSize);
+  border: 1px solid var(--darkBlue);
+  border-radius: 2px;
+  box-sizing: border-box;
+  background: transparent;
+  color: var(--darkBlue);
+  font-weight: 600;
+  font-size: var(--smallFontSize);
+  text-decoration: none;
+  cursor: pointer;
+  outline: none;
+  transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
+}
+
+.button:hover,
+.button.button-active {
+  background: var(--darkBlue);
+  color: var(--white);
+}
+
+.button:active {
+  box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
+}
+
+.button:focus {
+  box-shadow: 0 0 0 3px rgba(35, 106, 151, 0.25);
+}
+
+.button-primary {
+  background: var(--darkBlue);
+  border-color: var(--darkBlue);
+  color: var(--white);
+}
+
+.button-primary:hover {
+  background: var(--veryDarkBlue);
+  border-color: var(--veryDarkBlue);
+}
+
+.button-primary.button-light {
+  background: var(--blue);
+  border-color: var(--blue);
+  color: var(--white);
+}
+
+.button-primary.button-light:hover {
+  background: var(--darkBlue);
+  border-color: var(--darkBlue);
+}
+
+.button.disabled {
+  color: var(--disableGrayText) !important;
+  border-color: var(--disableGrayBorder) !important;
+  background: var(--disableGrayBg) !important;
+  cursor: not-allowed !important;
+  box-shadow: none !important;
+}
+
+/* #region .button-red */
+.button-red {
+  border-color: var(--red);
+  color: var(--red);
+}
+
+.button-red:hover,
+.button-red.active {
+  background: var(--red);
+  color: var(--white);
+}
+
+.button-red:focus {
+  box-shadow: 0 0 0 3px rgba(212, 51, 63, 0.25);
+}
+
+/* #endregion */
+
+/* #region .button-success */
+.button-success {
+  border-color: var(--green);
+  color: var(--green);
+}
+
+.button-success:hover,
+.button-success.active {
+  background: var(--green);
+  color: var(--white);
+}
+
+.button-success:focus {
+  box-shadow: 0 0 0 3px rgba(0, 170, 0, 0.25);
+}
+
+/* #endregion */
+
+/* #region .button-link */
+.button-link {
+  display: inline-flex;
+  height: auto;
+  /* Keep this to not inherit the height from .button */
+  line-height: 1;
+  margin: 0;
+  padding: 0;
+  border: none;
+  border-radius: 0;
+  background: transparent;
+  color: var(--darkBlue);
+  border-bottom: 1px solid var(--lightBlue);
+  font-weight: 400;
+  font-size: inherit;
+  transition: border-color 0.2s ease, box-shadow 0.2s ease, color 0.2s ease, border-bottom 0.2s ease;
+}
+
+.dropdown .button-link {
+  border-bottom: none;
+}
+
+.button-link:hover {
+  background: transparent;
+  color: var(--blue);
+}
+
+.button-link:active,
+.button-link:focus {
+  box-shadow: none;
+  outline: 1px dotted var(--blue);
+}
+
+.button-link.disabled {
+  color: var(--secondFontColor);
+  background: transparent !important;
+  cursor: default;
+}
+
+/* #endregion */
+
+.button-small {
+  height: var(--smallControlHeight);
+  line-height: 18px;
+  padding: 0 6px;
+  font-size: 11px;
+}
+
+.button-tiny {
+  height: var(--tinyControlHeight);
+  line-height: var(--tinyControlHeight);
+  padding: 0 calc(var(--gridSize) / 2);
+}
+
+.button-large {
+  height: var(--largeControlHeight);
+  padding: 0 16px;
+  font-size: var(--mediumFontSize);
+}
+
+.button-huge {
+  flex-direction: column;
+  padding: calc(2 * var(--gridSize));
+  width: 200px;
+  height: 200px;
+  background-color: var(--white);
+  border: solid 1px var(--white);
+  border-radius: 3px;
+  transition: all 0.2s ease;
+  box-shadow: 0 1px 1px 1px var(--barBorderColor);
+}
+
+.button-huge:hover,
+.button-huge:focus,
+.button-huge:active {
+  background-color: var(--white);
+  color: var(--darkBlue);
+  box-shadow: var(--defaultShadow);
+  transform: translateY(-2px);
+}
+
+/* #region .button-group */
+/* TODO drop usage of this class in SQ (already dropped from SC) */
+.button-group {
+  display: inline-block;
+  vertical-align: middle;
+  font-size: 0;
+  white-space: nowrap;
+}
+
+.button-group > button,
+.button-group > .button {
+  position: relative;
+  z-index: var(--normalZIndex);
+  display: inline-block;
+  vertical-align: middle;
+  margin: 0;
+  cursor: pointer;
+}
+
+.button-group > .button:hover:not(.disabled),
+.button-group > .button:focus:not(.disabled),
+.button-group > .button:active:not(.disabled),
+.button-group > .button.active:not(.disabled) {
+  z-index: var(--aboveNormalZIndex);
+}
+
+.button-group > .button.disabled {
+  z-index: var(--belowNormalZIndex);
+}
+
+.button-group > .button:not(:first-child) {
+  border-top-left-radius: 0;
+  border-bottom-left-radius: 0;
+}
+
+.button-group > .button:not(:last-child):not(.dropdown-toggle) {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
+
+.button-group > .button + .button {
+  margin-left: -1px;
+}
+
+.button-group > a:not(.button) {
+  vertical-align: middle;
+  margin: 0 8px;
+  font-size: var(--smallFontSize);
+}
+
+/* #endregion */
+
+/* #region .button-icon */
+.button-icon {
+  display: inline-flex;
+  justify-content: center;
+  align-items: center;
+  vertical-align: middle;
+  width: var(--controlHeight);
+  height: var(--controlHeight);
+  padding: 0;
+  border: none;
+  color: inherit;
+}
+
+.button-icon.button-small {
+  width: var(--smallControlHeight);
+  height: var(--smallControlHeight);
+  padding: 0;
+}
+
+.button-icon.button-small svg {
+  margin-top: 0;
+}
+
+.button-icon.button-tiny {
+  width: var(--tinyControlHeight);
+  height: var(--tinyControlHeight);
+  padding: 0;
+}
+
+.button-icon.button-tiny svg {
+  margin-top: 0;
+}
+
+.button-icon:hover,
+.button-icon:focus {
+  background-color: currentColor;
+}
+
+.button-icon:not(.disabled):hover svg,
+.button-icon:not(.disabled):focus svg {
+  color: var(--white);
+}
+
+.button.button-icon.disabled {
+  background: transparent !important;
+}
+
+/* #endregion */
+
+.button-list {
+  display: inline-flex;
+  justify-content: space-between;
+  height: auto;
+  border: 1px solid var(--barBorderColor);
+  padding: var(--gridSize);
+  margin: calc(var(--gridSize) / 2);
+  color: var(--secondFontColor);
+  font-weight: normal;
+}
+
+.button-list:hover {
+  background-color: white;
+  border-color: var(--blue);
+  color: var(--darkBlue);
+}
+
+.no-select {
+  user-select: none !important;
+}
diff --git a/server/sonar-ui-common/components/controls/buttons.tsx b/server/sonar-ui-common/components/controls/buttons.tsx
new file mode 100644 (file)
index 0000000..d0567be
--- /dev/null
@@ -0,0 +1,182 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import ChevronRightIcon from '../icons/ChevronRightIcon';
+import ClearIcon, { ClearIconProps } from '../icons/ClearIcon';
+import DeleteIcon from '../icons/DeleteIcon';
+import EditIcon from '../icons/EditIcon';
+import { IconProps } from '../icons/Icon';
+import { ThemeConsumer } from '../theme';
+import './buttons.css';
+import Tooltip, { TooltipProps } from './Tooltip';
+
+type AllowedButtonAttributes = Pick<
+  React.ButtonHTMLAttributes<HTMLButtonElement>,
+  'className' | 'disabled' | 'id' | 'style' | 'title'
+>;
+
+interface ButtonProps extends AllowedButtonAttributes {
+  autoFocus?: boolean;
+  children?: React.ReactNode;
+  innerRef?: (node: HTMLElement | null) => void;
+  name?: string;
+  onClick?: () => void;
+  preventDefault?: boolean;
+  stopPropagation?: boolean;
+  type?: 'button' | 'submit' | 'reset' | undefined;
+}
+
+export class Button extends React.PureComponent<ButtonProps> {
+  handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
+    const { disabled, onClick, preventDefault = true, stopPropagation = false } = this.props;
+
+    event.currentTarget.blur();
+    if (preventDefault || disabled) {
+      event.preventDefault();
+    }
+    if (stopPropagation) {
+      event.stopPropagation();
+    }
+
+    if (onClick && !disabled) {
+      onClick();
+    }
+  };
+
+  render() {
+    const {
+      className,
+      disabled,
+      innerRef,
+      onClick,
+      preventDefault,
+      stopPropagation,
+      type = 'button',
+      ...props
+    } = this.props;
+    return (
+      // eslint-disable-next-line react/button-has-type
+      <button
+        {...props}
+        aria-disabled={disabled}
+        className={classNames('button', className, { disabled })}
+        id={this.props.id}
+        onClick={this.handleClick}
+        ref={this.props.innerRef}
+        type={type}
+      />
+    );
+  }
+}
+
+export function ButtonLink({ className, ...props }: ButtonProps) {
+  return <Button {...props} className={classNames('button-link', className)} />;
+}
+
+export function SubmitButton(props: T.Omit<ButtonProps, 'type'>) {
+  // do not prevent default to actually submit a form
+  return <Button {...props} preventDefault={false} type="submit" />;
+}
+
+export function ResetButtonLink(props: T.Omit<ButtonProps, 'type'>) {
+  return <ButtonLink {...props} type="reset" />;
+}
+
+export interface ButtonIconProps extends ButtonProps {
+  'aria-label'?: string;
+  'aria-labelledby'?: string;
+  className?: string;
+  color?: string;
+  onClick?: () => void;
+  tooltip?: React.ReactNode;
+  tooltipProps?: Partial<TooltipProps>;
+}
+
+export function ButtonIcon(props: ButtonIconProps) {
+  const { className, color, tooltip, tooltipProps, ...other } = props;
+  return (
+    <ThemeConsumer>
+      {(theme) => (
+        <Tooltip mouseEnterDelay={0.4} overlay={tooltip} {...tooltipProps}>
+          <Button
+            className={classNames(className, 'button-icon')}
+            stopPropagation={true}
+            style={{ color: color || theme.colors.darkBlue }}
+            {...other}
+          />
+        </Tooltip>
+      )}
+    </ThemeConsumer>
+  );
+}
+
+interface ClearButtonProps extends ButtonIconProps {
+  className?: string;
+  iconProps?: ClearIconProps;
+  onClick?: () => void;
+}
+
+export function ClearButton({ color, iconProps = {}, ...props }: ClearButtonProps) {
+  return (
+    <ThemeConsumer>
+      {(theme) => (
+        <ButtonIcon color={color || theme.colors.gray60} {...props}>
+          <ClearIcon {...iconProps} />
+        </ButtonIcon>
+      )}
+    </ThemeConsumer>
+  );
+}
+
+interface ActionButtonProps extends ButtonIconProps {
+  className?: string;
+  iconProps?: IconProps;
+  onClick?: () => void;
+}
+
+export function DeleteButton({ iconProps = {}, ...props }: ActionButtonProps) {
+  return (
+    <ThemeConsumer>
+      {(theme) => (
+        <ButtonIcon color={theme.colors.red} {...props}>
+          <DeleteIcon {...iconProps} />
+        </ButtonIcon>
+      )}
+    </ThemeConsumer>
+  );
+}
+
+export function EditButton({ iconProps = {}, ...props }: ActionButtonProps) {
+  return (
+    <ButtonIcon {...props}>
+      <EditIcon {...iconProps} />
+    </ButtonIcon>
+  );
+}
+
+export function ListButton({ className, children, ...props }: ButtonProps) {
+  return (
+    <Button className={classNames('button-list', className)} {...props}>
+      {children}
+      <ChevronRightIcon />
+    </Button>
+  );
+}
diff --git a/server/sonar-ui-common/components/controls/clipboard.tsx b/server/sonar-ui-common/components/controls/clipboard.tsx
new file mode 100644 (file)
index 0000000..e4778f4
--- /dev/null
@@ -0,0 +1,149 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import * as classNames from 'classnames';
+import * as Clipboard from 'clipboard';
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import CopyIcon from '../icons/CopyIcon';
+import { Button, ButtonIcon } from './buttons';
+import Tooltip from './Tooltip';
+
+export interface State {
+  copySuccess: boolean;
+}
+
+interface RenderProps {
+  setCopyButton: (node: HTMLElement | null) => void;
+  copySuccess: boolean;
+}
+
+interface BaseProps {
+  children: (props: RenderProps) => React.ReactNode;
+}
+
+export class ClipboardBase extends React.PureComponent<BaseProps, State> {
+  private clipboard?: Clipboard;
+  private copyButton?: HTMLElement | null;
+  mounted = false;
+  state: State = { copySuccess: false };
+
+  componentDidMount() {
+    this.mounted = true;
+    if (this.copyButton) {
+      this.clipboard = new Clipboard(this.copyButton);
+      this.clipboard.on('success', this.handleSuccessCopy);
+    }
+  }
+
+  componentDidUpdate() {
+    if (this.clipboard) {
+      this.clipboard.destroy();
+    }
+    if (this.copyButton) {
+      this.clipboard = new Clipboard(this.copyButton);
+      this.clipboard.on('success', this.handleSuccessCopy);
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+    if (this.clipboard) {
+      this.clipboard.destroy();
+    }
+  }
+
+  setCopyButton = (node: HTMLElement | null) => {
+    this.copyButton = node;
+  };
+
+  handleSuccessCopy = () => {
+    if (this.mounted) {
+      this.setState({ copySuccess: true });
+      setTimeout(() => {
+        if (this.mounted) {
+          this.setState({ copySuccess: false });
+        }
+      }, 1000);
+    }
+  };
+
+  render() {
+    return this.props.children({
+      setCopyButton: this.setCopyButton,
+      copySuccess: this.state.copySuccess,
+    });
+  }
+}
+
+interface ButtonProps {
+  className?: string;
+  copyValue: string;
+  children?: React.ReactNode;
+}
+
+export function ClipboardButton({ className, children, copyValue }: ButtonProps) {
+  return (
+    <ClipboardBase>
+      {({ setCopyButton, copySuccess }) => (
+        <Tooltip overlay={translate('copied_action')} visible={copySuccess}>
+          <Button
+            className={classNames('no-select', className)}
+            data-clipboard-text={copyValue}
+            innerRef={setCopyButton}>
+            {children || (
+              <>
+                <CopyIcon className="little-spacer-right" />
+                {translate('copy')}
+              </>
+            )}
+          </Button>
+        </Tooltip>
+      )}
+    </ClipboardBase>
+  );
+}
+
+interface IconButtonProps {
+  'aria-label'?: string;
+  className?: string;
+  copyValue: string;
+}
+
+export function ClipboardIconButton(props: IconButtonProps) {
+  const { className, copyValue } = props;
+  return (
+    <ClipboardBase>
+      {({ setCopyButton, copySuccess }) => {
+        return (
+          <ButtonIcon
+            aria-label={props['aria-label'] ?? translate('copy_to_clipboard')}
+            className={classNames('no-select', className)}
+            data-clipboard-text={copyValue}
+            innerRef={setCopyButton}
+            tooltip={translate(copySuccess ? 'copied_action' : 'copy_to_clipboard')}
+            tooltipProps={copySuccess ? { visible: copySuccess } : undefined}>
+            <CopyIcon />
+          </ButtonIcon>
+        );
+      }}
+    </ClipboardBase>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/AlertErrorIcon.tsx b/server/sonar-ui-common/components/icons/AlertErrorIcon.tsx
new file mode 100644 (file)
index 0000000..8ab7ac9
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IconProps, ThemedIcon } from './Icon';
+
+export default function AlertErrorIcon({ fill, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M11.402 10.018q0-0.232-0.17-0.402l-1.616-1.616 1.616-1.616q0.17-0.17 0.17-0.402 0-0.241-0.17-0.411l-0.804-0.804q-0.17-0.17-0.411-0.17-0.232 0-0.402 0.17l-1.616 1.616-1.616-1.616q-0.17-0.17-0.402-0.17-0.241 0-0.411 0.17l-0.804 0.804q-0.17 0.17-0.17 0.411 0 0.232 0.17 0.402l1.616 1.616-1.616 1.616q-0.17 0.17-0.17 0.402 0 0.241 0.17 0.411l0.804 0.804q0.17 0.17 0.411 0.17 0.232 0 0.402-0.17l1.616-1.616 1.616 1.616q0.17 0.17 0.402 0.17 0.241 0 0.411-0.17l0.804-0.804q0.17-0.17 0.17-0.411zM14.857 8q0 1.866-0.92 3.442t-2.496 2.496-3.442 0.92-3.442-0.92-2.496-2.496-0.92-3.442 0.92-3.442 2.496-2.496 3.442-0.92 3.442 0.92 2.496 2.496 0.92 3.442z"
+          style={{ fill: fill || theme.colors.red }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/AlertSuccessIcon.tsx b/server/sonar-ui-common/components/icons/AlertSuccessIcon.tsx
new file mode 100644 (file)
index 0000000..282f352
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IconProps, ThemedIcon } from './Icon';
+
+export default function AlertSuccessIcon({ fill, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M12.607 6.554q0-0.25-0.161-0.411l-0.813-0.804q-0.17-0.17-0.402-0.17t-0.402 0.17l-3.643 3.634-2.018-2.018q-0.17-0.17-0.402-0.17t-0.402 0.17l-0.813 0.804q-0.161 0.161-0.161 0.411 0 0.241 0.161 0.402l3.232 3.232q0.17 0.17 0.402 0.17 0.241 0 0.411-0.17l4.848-4.848q0.161-0.161 0.161-0.402zM14.857 8q0 1.866-0.92 3.442t-2.496 2.496-3.442 0.92-3.442-0.92-2.496-2.496-0.92-3.442 0.92-3.442 2.496-2.496 3.442-0.92 3.442 0.92 2.496 2.496 0.92 3.442z"
+          style={{ fill: fill || theme.colors.green }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/AlertWarnIcon.tsx b/server/sonar-ui-common/components/icons/AlertWarnIcon.tsx
new file mode 100644 (file)
index 0000000..99c3f27
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IconProps, ThemedIcon } from './Icon';
+
+export default function AlertWarnIcon({ fill, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M8 1.143q1.866 0 3.442.92t2.496 2.496.92 3.442-.92 3.442-2.496 2.496-3.442.92-3.442-.92-2.496-2.496-.92-3.442.92-3.442 2.496-2.496T8 1.143zm1.143 11.134v-1.696q0-.125-.08-.21t-.196-.085H7.153q-.116 0-.205.089t-.089.205v1.696q0 .116.089.205t.205.089h1.714q.116 0 .196-.085t.08-.21zm-.018-3.072l.161-5.545q0-.107-.089-.161-.089-.071-.214-.071H7.019q-.125 0-.214.071-.089.054-.089.161l.152 5.545q0 .089.089.156t.214.067h1.652q.125 0 .21-.067t.094-.156z"
+          style={{ fill: fill || theme.colors.orange }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/ArrowIcon.tsx b/server/sonar-ui-common/components/icons/ArrowIcon.tsx
new file mode 100644 (file)
index 0000000..1b71b1b
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+interface Props extends IconProps {
+  animated?: boolean;
+  inverseDirection?: boolean;
+}
+
+export default function ArrowIcon({
+  animated = false,
+  fill = 'currentColor',
+  inverseDirection = false,
+  ...iconProps
+}: Props) {
+  const style: React.CSSProperties = {};
+  if (inverseDirection) {
+    style.transform = 'scaleX(-1)';
+  }
+
+  if (animated) {
+    style.transition = 'transform 0.2s';
+  }
+  return (
+    <Icon style={style} {...iconProps}>
+      <path
+        d="M13.99 6.867l.668.005H4.99l3.04-3.046a.79.79 0 00.23-.561.789.789 0 00-.23-.56l-.473-.474A.784.784 0 006.998 2a.784.784 0 00-.558.23L1.23 7.44A.783.783 0 001 8c0 .212.081.41.23.56l5.21 5.21c.149.148.347.23.558.23.212 0 .41-.082.559-.23l.472-.473a.782.782 0 000-1.106L4.956 9.128H14a.819.819 0 00.801-.81v-.67c0-.435-.376-.78-.812-.78z"
+        fill={fill}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/BackIcon.tsx b/server/sonar-ui-common/components/icons/BackIcon.tsx
new file mode 100644 (file)
index 0000000..5ad31ca
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function BackIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M3.6 8.69l4.07 4.13.04.04a.7.7 0 01.12.7.69.69 0 01-.86.4.73.73 0 01-.26-.16L1 8l5.71-5.8.04-.03A.73.73 0 017.13 2l.1-.01c.1.01.2.04.3.09a.7.7 0 01.3.82c-.03.1-.09.19-.16.27L3.61 7.3c3.59-.03 7.18-.14 10.77.01.05 0 .06 0 .1.02a.68.68 0 01.52.61.7.7 0 01-.57.74h-.1z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/BranchIcon.tsx b/server/sonar-ui-common/components/icons/BranchIcon.tsx
new file mode 100644 (file)
index 0000000..2823224
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IconProps, ThemedIcon } from './Icon';
+
+export default function BranchIcon({ fill, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M12.5 6.5c0-1.1-.9-2-2-2s-2 .9-2 2c0 .8.5 1.5 1.2 1.8-.3.6-.7 1.1-1.2 1.4-.9.5-1.9.5-2.5.4V4c.9-.2 1.5-1 1.5-1.9 0-1.1-.9-2-2-2s-2 .9-2 2C3.5 3 4.1 3.8 5 4v8c-.9.2-1.5 1-1.5 1.9 0 1.1.9 2 2 2s2-.9 2-2c0-.9-.6-1.7-1.5-1.9v-1c.2 0 .5.1.7.1.7 0 1.5-.1 2.2-.6.8-.5 1.4-1.2 1.7-2.1 1.1 0 1.9-.9 1.9-1.9zm-8-4.4c0-.6.4-1 1-1s1 .4 1 1-.4 1-1 1-1-.5-1-1zm2 11.9c0 .6-.4 1-1 1s-1-.4-1-1 .4-1 1-1 1 .4 1 1zm4-6.5c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1z"
+          style={{ fill: fill || theme.colors.blue }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/BubblesIcon.tsx b/server/sonar-ui-common/components/icons/BubblesIcon.tsx
new file mode 100644 (file)
index 0000000..0a6bc91
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function BubblesIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon style={{ fillRule: 'nonzero' }} {...iconProps}>
+      <path
+        d="M4.1 10.2c1 0 1.9.8 1.9 1.9S5.1 14 4.1 14s-1.9-.8-1.9-1.9.8-1.9 1.9-1.9m0-2C2 8.2.2 9.9.2 12.1S1.9 16 4.1 16 8 14.3 8 12.1 6.2 8.2 4.1 8.2zM10.3 2c2 0 3.7 1.7 3.7 3.7s-1.7 3.7-3.7 3.7-3.8-1.6-3.8-3.7S8.2 2 10.3 2m0-2C7.1 0 4.5 2.6 4.5 5.7s2.6 5.7 5.7 5.7S16 8.9 16 5.7 13.4 0 10.3 0z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/BugIcon.tsx b/server/sonar-ui-common/components/icons/BugIcon.tsx
new file mode 100644 (file)
index 0000000..93a103f
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function BugIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M10.09,1.88A2.86,2.86,0,0,0,8,1a2.87,2.87,0,0,0-2.11.87A2.93,2.93,0,0,0,5,4h6A2.93,2.93,0,0,0,10.09,1.88Z"
+        style={{ fill }}
+      />
+      <path
+        d="M14.54,9H13V5.6L14.3,4.42a.5.5,0,0,0,0-.71.49.49,0,0,0-.7,0L12.17,5H3.82L2.34,3.66a.5.5,0,0,0-.67.74L2.94,5.55V9H1.46a.5.5,0,0,0,0,1H3a5.2,5.2,0,0,0,1.05,2.32l-2,1.81a.5.5,0,1,0,.67.74l2-1.82A4.62,4.62,0,0,0,7,14.1V8A1,1,0,0,1,8,7a.94.94,0,0,1,1,.9v6.17A4.55,4.55,0,0,0,11.18,13l2,1.83a.51.51,0,0,0,.33.13.48.48,0,0,0,.37-.17.49.49,0,0,0,0-.7l-2-1.8a5.34,5.34,0,0,0,1-2.29h1.64a.5.5,0,0,0,0-1Z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/BugTrackerIcon.tsx b/server/sonar-ui-common/components/icons/BugTrackerIcon.tsx
new file mode 100644 (file)
index 0000000..bc2319c
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function BugTrackerIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M13.5 9.5c1.003.033 1.466 1.952 0 2h-2.618L9.685 9.107 8 14.162 6.096 8.45l-.832 3.05-2.829-.002c-.984-.097-1.369-1.951.065-1.998h1.236l2.168-7.95L8 7.838l1.315-3.945L12.118 9.5H13.5z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/BulletListIcon.tsx b/server/sonar-ui-common/components/icons/BulletListIcon.tsx
new file mode 100644 (file)
index 0000000..e1b2556
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function BulletListIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M2.968 11.274v1.51q0 0.102-0.075 0.177t-0.177 0.075h-1.51q-0.102 0-0.177-0.075t-0.075-0.177v-1.51q0-0.102 0.075-0.177t0.177-0.075h1.51q0.102 0 0.177 0.075t0.075 0.177zM2.968 8.255v1.51q0 0.102-0.075 0.177t-0.177 0.075h-1.51q-0.102 0-0.177-0.075t-0.075-0.177v-1.51q0-0.102 0.075-0.177t0.177-0.075h1.51q0.102 0 0.177 0.075t0.075 0.177zM2.968 5.235v1.51q0 0.102-0.075 0.177t-0.177 0.075h-1.51q-0.102 0-0.177-0.075t-0.075-0.177v-1.51q0-0.102 0.075-0.177t0.177-0.075h1.51q0.102 0 0.177 0.075t0.075 0.177zM15.045 11.274v1.51q0 0.102-0.075 0.177t-0.177 0.075h-10.568q-0.102 0-0.177-0.075t-0.075-0.177v-1.51q0-0.102 0.075-0.177t0.177-0.075h10.568q0.102 0 0.177 0.075t0.075 0.177zM2.968 2.216v1.51q0 0.102-0.075 0.177t-0.177 0.075h-1.51q-0.102 0-0.177-0.075t-0.075-0.177v-1.51q0-0.102 0.075-0.177t0.177-0.075h1.51q0.102 0 0.177 0.075t0.075 0.177zM15.045 8.255v1.51q0 0.102-0.075 0.177t-0.177 0.075h-10.568q-0.102 0-0.177-0.075t-0.075-0.177v-1.51q0-0.102 0.075-0.177t0.177-0.075h10.568q0.102 0 0.177 0.075t0.075 0.177zM15.045 5.235v1.51q0 0.102-0.075 0.177t-0.177 0.075h-10.568q-0.102 0-0.177-0.075t-0.075-0.177v-1.51q0-0.102 0.075-0.177t0.177-0.075h10.568q0.102 0 0.177 0.075t0.075 0.177zM15.045 2.216v1.51q0 0.102-0.075 0.177t-0.177 0.075h-10.568q-0.102 0-0.177-0.075t-0.075-0.177v-1.51q0-0.102 0.075-0.177t0.177-0.075h10.568q0.102 0 0.177 0.075t0.075 0.177z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/CalendarIcon.tsx b/server/sonar-ui-common/components/icons/CalendarIcon.tsx
new file mode 100644 (file)
index 0000000..f7411e2
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function CalendarIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M2 14h2.25v-2.25H2V14zm2.75 0h2.5v-2.25h-2.5V14zM2 11.25h2.25v-2.5H2v2.5zm2.75 0h2.5v-2.5h-2.5v2.5zM2 8.25h2.25V6H2v2.25zM7.75 14h2.5v-2.25h-2.5V14zm-3-5.75h2.5V6h-2.5v2.25zm6 5.75H13v-2.25h-2.25V14zm-3-2.75h2.5v-2.5h-2.5v2.5zM5 4.5V2.25a.24.24 0 0 0-.074-.176A.24.24 0 0 0 4.75 2h-.5a.24.24 0 0 0-.176.074A.24.24 0 0 0 4 2.25V4.5a.24.24 0 0 0 .074.176.24.24 0 0 0 .176.074h.5a.24.24 0 0 0 .176-.074A.24.24 0 0 0 5 4.5zm5.75 6.75H13v-2.5h-2.25v2.5zm-3-3h2.5V6h-2.5v2.25zm3 0H13V6h-2.25v2.25zM11 4.5V2.25a.24.24 0 0 0-.074-.176A.24.24 0 0 0 10.75 2h-.5a.24.24 0 0 0-.176.074.24.24 0 0 0-.074.176V4.5a.24.24 0 0 0 .074.176.24.24 0 0 0 .176.074h.5a.24.24 0 0 0 .176-.074A.24.24 0 0 0 11 4.5zm3-.5v10c0 .27-.099.505-.297.703A.961.961 0 0 1 13 15H2a.961.961 0 0 1-.703-.297A.961.961 0 0 1 1 14V4c0-.27.099-.505.297-.703A.961.961 0 0 1 2 3h1v-.75c0-.344.122-.638.367-.883S3.907 1 4.25 1h.5c.344 0 .638.122.883.367S6 1.907 6 2.25V3h3v-.75c0-.344.122-.638.367-.883S9.907 1 10.25 1h.5c.344 0 .638.122.883.367s.367.54.367.883V3h1c.27 0 .505.099.703.297A.961.961 0 0 1 14 4z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/ChartLegendIcon.tsx b/server/sonar-ui-common/components/icons/ChartLegendIcon.tsx
new file mode 100644 (file)
index 0000000..07909e3
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { ThemedIcon } from './Icon';
+
+interface Props {
+  className?: string;
+  index: number;
+  size?: number;
+}
+
+export default function ChartLegendIcon({ index, ...iconProps }: Props) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => {
+        const COLORS = [theme.colors.blue, theme.colors.darkBlue, '#24c6e0'];
+        return (
+          <path
+            d="M14.325 7.143v1.714q0 0.357-0.25 0.607t-0.607 0.25h-10.857q-0.357 0-0.607-0.25t-0.25-0.607v-1.714q0-0.357 0.25-0.607t0.607-0.25h10.857q0.357 0 0.607 0.25t0.25 0.607z"
+            style={{ fill: COLORS[index] || COLORS[0] }}
+          />
+        );
+      }}
+    </ThemedIcon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/CheckIcon.tsx b/server/sonar-ui-common/components/icons/CheckIcon.tsx
new file mode 100644 (file)
index 0000000..a1ba82a
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function CheckIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M14.92 4.804q0 0.357-0.25 0.607l-7.679 7.679q-0.25 0.25-0.607 0.25t-0.607-0.25l-4.446-4.446q-0.25-0.25-0.25-0.607t0.25-0.607l1.214-1.214q0.25-0.25 0.607-0.25t0.607 0.25l2.625 2.634 5.857-5.866q0.25-0.25 0.607-0.25t0.607 0.25l1.214 1.214q0.25 0.25 0.25 0.607z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/ChevronDownIcon.tsx b/server/sonar-ui-common/components/icons/ChevronDownIcon.tsx
new file mode 100644 (file)
index 0000000..ca94d88
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function ChevronDownIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M7.72 11.596L3.119 6.992A.382.382 0 0 1 3 6.713c0-.108.04-.2.118-.279l1.03-1.03a.382.382 0 0 1 .278-.117c.108 0 .201.04.28.117L8 8.7l3.294-3.295a.382.382 0 0 1 .28-.117c.108 0 .2.04.279.117l1.03 1.03a.382.382 0 0 1 .117.28c0 .107-.04.2-.118.278L8.28 11.596a.382.382 0 0 1-.279.117.382.382 0 0 1-.28-.117z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/ChevronLeftIcon.tsx b/server/sonar-ui-common/components/icons/ChevronLeftIcon.tsx
new file mode 100644 (file)
index 0000000..aa74c73
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function ChevronLeftIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M4.404 8.28l4.604 4.602a.382.382 0 0 0 .279.118c.108 0 .2-.04.279-.118l1.03-1.03a.382.382 0 0 0 .117-.278.382.382 0 0 0-.117-.28L7.3 8l3.295-3.294a.382.382 0 0 0 .117-.28.382.382 0 0 0-.117-.279l-1.03-1.03A.382.382 0 0 0 9.286 3a.382.382 0 0 0-.278.118L4.404 7.72A.382.382 0 0 0 4.287 8c0 .108.04.201.117.28z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/ChevronRightIcon.tsx b/server/sonar-ui-common/components/icons/ChevronRightIcon.tsx
new file mode 100644 (file)
index 0000000..ad5b110
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function ChevronRightIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M11.596 8.28l-4.604 4.602a.382.382 0 0 1-.279.118.382.382 0 0 1-.279-.118l-1.03-1.03a.382.382 0 0 1-.117-.278c0-.108.04-.201.117-.28L8.7 8 5.404 4.706a.382.382 0 0 1-.117-.28c0-.108.04-.2.117-.279l1.03-1.03A.382.382 0 0 1 6.714 3c.107 0 .2.04.278.118l4.604 4.603a.382.382 0 0 1 .117.279c0 .108-.04.201-.117.28z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/ChevronUpIcon.tsx b/server/sonar-ui-common/components/icons/ChevronUpIcon.tsx
new file mode 100644 (file)
index 0000000..fe67d60
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function ChevronUpIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M8.28 4.404l4.602 4.604a.382.382 0 0 1 .118.279c0 .108-.04.2-.118.279l-1.03 1.03a.382.382 0 0 1-.278.117.382.382 0 0 1-.28-.117L8 7.3l-3.294 3.295a.382.382 0 0 1-.28.117.382.382 0 0 1-.279-.117l-1.03-1.03A.382.382 0 0 1 3 9.286c0-.107.04-.2.118-.278L7.72 4.404A.382.382 0 0 1 8 4.287c.108 0 .201.04.28.117z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/ChevronsIcon.tsx b/server/sonar-ui-common/components/icons/ChevronsIcon.tsx
new file mode 100644 (file)
index 0000000..f0f380e
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function ChevronsIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M6.27 11.07L3.2 8l3.07-3.07L5.33 4l-4 4 4 4 .94-.93zm3.46 0L12.8 8 9.73 4.93l.94-.93 4 4-4 4-.94-.93z"
+        fill={fill}
+        fillRule="nonzero"
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/ClearIcon.tsx b/server/sonar-ui-common/components/icons/ClearIcon.tsx
new file mode 100644 (file)
index 0000000..4170046
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export interface ClearIconProps extends IconProps {
+  thin?: boolean;
+}
+
+export default function ClearIcon({ fill = 'currentColor', thin, ...iconProps }: ClearIconProps) {
+  return (
+    <Icon {...iconProps}>
+      {thin ? (
+        <path
+          d="M14 3.209l-1.209-1.209-4.791 4.791-4.791-4.791-1.209 1.209 4.791 4.791-4.791 4.791 1.209 1.209 4.791-4.791 4.791 4.791 1.209-1.209-4.791-4.791z"
+          style={{ fill }}
+        />
+      ) : (
+        <path
+          d="M14 4.242L11.758 2l-3.76 3.76L4.242 2 2 4.242l3.756 3.756L2 11.758 4.242 14l3.756-3.76 3.76 3.76L14 11.758l-3.76-3.76L14 4.242z"
+          style={{ fill }}
+        />
+      )}
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/ClockIcon.tsx b/server/sonar-ui-common/components/icons/ClockIcon.tsx
new file mode 100644 (file)
index 0000000..556f316
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function ClockIcon({ className, ...iconProps }: IconProps) {
+  return (
+    <Icon className={classNames('icon-clock', className)} {...iconProps}>
+      <g fill="#fff" stroke="#ADADAD" transform="matrix(1.4 0 0 1.4 .3 .7)">
+        <circle cx="5.5" cy="5.2" r="5" />
+        <path d="M5.6 2.9v2.7l2-.5" fillRule="nonzero" />
+      </g>
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/CodeSmellIcon.tsx b/server/sonar-ui-common/components/icons/CodeSmellIcon.tsx
new file mode 100644 (file)
index 0000000..0ca8c20
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function CodeSmellIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M8,15.1a7,7,0,1,0-7-7A7,7,0,0,0,8,15.1Zm.74-8.9,1.46-2.52a.29.29,0,0,1,.25-.14.3.3,0,0,1,.15,0,5.26,5.26,0,0,1,2.61,4.53.28.28,0,0,1-.29.29H10a.28.28,0,0,1-.29-.29,1.78,1.78,0,0,0-.88-1.51A.29.29,0,0,1,8.75,6.2Zm.11,3.44A.23.23,0,0,1,9,9.6a.29.29,0,0,1,.25.14l1.46,2.52a.18.18,0,0,1,0,.13.3.3,0,0,1-.15.27,5.3,5.3,0,0,1-5.23,0,.3.3,0,0,1-.1-.4L6.73,9.74A.29.29,0,0,1,7,9.6a.23.23,0,0,1,.14,0A1.79,1.79,0,0,0,8.86,9.64ZM5.33,3.59a.3.3,0,0,1,.41.1L7.2,6.21a.29.29,0,0,1-.1.4,1.79,1.79,0,0,0-.87,1.51.28.28,0,0,1-.29.29H3a.32.32,0,0,1-.32-.29A5.26,5.26,0,0,1,5.33,3.59Z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/CogIcon.tsx b/server/sonar-ui-common/components/icons/CogIcon.tsx
new file mode 100644 (file)
index 0000000..99b3e12
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function CogIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M14.922 9.704L13.6 8.696a4.55 4.551 0 000-1.057l1.323-1.006a.62.62 0 00.156-.805l-1.374-2.314a.658.658 0 00-.795-.28l-1.558.611a5.275 5.275 0 00-.935-.53l-.24-1.611a.631.631 0 00-.635-.537H6.787a.63.63 0 00-.633.532l-.239 1.616a5.62 5.62 0 00-.934.53l-1.563-.611a.645.645 0 00-.789.273L1.253 5.826a.616.616 0 00.157.808L2.73 7.64a4.517 4.519 0 000 1.058L1.41 9.705a.62.62 0 00-.158.805l1.374 2.314a.658.658 0 00.794.28l1.557-.61c.293.206.607.384.937.53l.24 1.61a.63.63 0 00.632.537H9.54a.63.63 0 00.634-.532l.24-1.616a5.62 5.62 0 00.934-.53l1.563.611a.645.645 0 00.789-.273l1.382-2.328a.618.619 0 00-.16-.8zm-6.758 1.382C6.51 11.087 5.17 9.78 5.17 8.17S6.51 5.252 8.164 5.252c1.654 0 2.995 1.307 2.995 2.917-.001 1.61-1.342 2.915-2.995 2.917z"
+        fill={fill}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/CollapseIcon.tsx b/server/sonar-ui-common/components/icons/CollapseIcon.tsx
new file mode 100644 (file)
index 0000000..49145d4
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function CollapseIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M8 8.509v3.56c0 .138-.05.257-.151.357-.1.101-.22.151-.358.151a.489.489 0 0 1-.357-.15l-1.145-1.145-2.638 2.639a.251.251 0 0 1-.366 0l-.906-.906a.251.251 0 0 1 0-.366l2.639-2.638-1.144-1.145a.489.489 0 0 1-.151-.357c0-.138.05-.257.15-.358.101-.1.22-.151.358-.151h3.56c.138 0 .257.05.358.151.1.1.151.22.151.358zm6-5.34c0 .068-.026.129-.08.182l-2.638 2.638 1.144 1.145c.101.1.151.22.151.357 0 .138-.05.257-.15.358-.101.1-.22.151-.358.151h-3.56a.489.489 0 0 1-.358-.151A.489.489 0 0 1 8 7.491v-3.56c0-.138.05-.257.151-.357.1-.101.22-.151.358-.151.137 0 .257.05.357.15l1.145 1.145 2.638-2.639a.251.251 0 0 1 .366 0l.906.906c.053.053.079.114.079.183z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/ContinuousIntegrationIcon.tsx b/server/sonar-ui-common/components/icons/ContinuousIntegrationIcon.tsx
new file mode 100644 (file)
index 0000000..1c0cc4a
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function ContinuousIntegrationIcon({
+  fill = 'currentColor',
+  ...iconProps
+}: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M13.805 9.25c0 .016 0 .04-.008.055C13.133 12.07 10.852 14 7.969 14c-1.524 0-3-.602-4.11-1.656l-1.007 1.008a.497.497 0 0 1-.352.148.504.504 0 0 1-.5-.5V9.5c0-.273.227-.5.5-.5H6c.273 0 .5.227.5.5a.497.497 0 0 1-.148.352l-1.07 1.07a3.988 3.988 0 0 0 6.125-.828c.187-.305.28-.602.413-.914.04-.11.117-.18.235-.18h1.5c.14 0 .25.117.25.25zM14 3v3.5c0 .273-.227.5-.5.5H10a.504.504 0 0 1-.5-.5c0-.133.055-.258.148-.352l1.079-1.078A4.019 4.019 0 0 0 8 4c-1.39 0-2.68.719-3.406 1.906-.188.305-.282.602-.414.914-.04.11-.117.18-.235.18H2.391a.252.252 0 0 1-.25-.25v-.055C2.812 3.922 5.117 2 8 2c1.531 0 3.023.61 4.133 1.656l1.015-1.008A.497.497 0 0 1 13.5 2.5c.273 0 .5.227.5.5z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/CopyIcon.tsx b/server/sonar-ui-common/components/icons/CopyIcon.tsx
new file mode 100644 (file)
index 0000000..9937c6d
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function CopyIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <g fill={fill} fillRule="nonzero">
+        <path d="M2.931 15.005V3H2v13h9v-.995z" />
+        <path d="M10 4.015h3V14H4V1h6v3.015zM9 8V6H8v2H6v1h2v2h1V9h2V8H9z" />
+        <path d="M11 1v2h2a2.151 2.151 0 0 0-2-2z" />
+      </g>
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/DeleteIcon.tsx b/server/sonar-ui-common/components/icons/DeleteIcon.tsx
new file mode 100644 (file)
index 0000000..e5293b4
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function DeleteIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M13.571429 1.8750019h-3.214285l-.251787-.5113283a.64285716.65624976 0 0 0-.5758927-.3636718H6.4678572a.63535718.64859353 0 0 0-.5732142.3636718l-.2517858.5113283H2.4285714A.42857144.43749984 0 0 0 2 2.3125018v.8749996a.42857144.43749984 0 0 0 .4285714.4374999H13.571429A.42857144.43749984 0 0 0 14 3.1875014v-.8749996a.42857144.43749984 0 0 0-.428571-.4374999zM3.4250001 13.769529a1.2857144 1.3124996 0 0 0 1.2830357 1.230468h6.5839282A1.2857144 1.3124996 0 0 0 12.575 13.769529l.567857-9.269528H2.8571428z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/DetachIcon.tsx b/server/sonar-ui-common/components/icons/DetachIcon.tsx
new file mode 100644 (file)
index 0000000..4dc5fc8
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function DetachIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M12 9.25v2.5A2.25 2.25 0 0 1 9.75 14h-6.5A2.25 2.25 0 0 1 1 11.75v-6.5A2.25 2.25 0 0 1 3.25 3h5.5c.14 0 .25.11.25.25v.5c0 .14-.11.25-.25.25h-5.5C2.562 4 2 4.563 2 5.25v6.5c0 .688.563 1.25 1.25 1.25h6.5c.688 0 1.25-.563 1.25-1.25v-2.5c0-.14.11-.25.25-.25h.5c.14 0 .25.11.25.25zm3-6.75v4c0 .273-.227.5-.5.5a.497.497 0 0 1-.352-.148l-1.375-1.375L7.68 10.57a.27.27 0 0 1-.18.078.27.27 0 0 1-.18-.078l-.89-.89a.27.27 0 0 1-.078-.18.27.27 0 0 1 .078-.18l5.093-5.093-1.375-1.375A.497.497 0 0 1 10 2.5c0-.273.227-.5.5-.5h4c.273 0 .5.227.5.5z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/DropdownIcon.tsx b/server/sonar-ui-common/components/icons/DropdownIcon.tsx
new file mode 100644 (file)
index 0000000..b9bebaa
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+interface DropdownIconProps {
+  turned?: boolean;
+}
+
+export default function DropdownIcon({
+  fill = 'currentColor',
+  size = 16,
+  turned = false,
+  ...iconProps
+}: IconProps & DropdownIconProps) {
+  return (
+    <Icon
+      height={size}
+      style={turned ? { transform: 'rotate(180deg)' } : undefined}
+      viewBox="0 0 7 16"
+      width={(size / 16) * 7}
+      {...iconProps}>
+      <path
+        d="M7 6.469a.42.42 0 0 1-.13.307L3.808 9.84a.42.42 0 0 1-.308.13.42.42 0 0 1-.308-.13L.13 6.776A.42.42 0 0 1 0 6.47a.42.42 0 0 1 .13-.308.42.42 0 0 1 .307-.13h6.126a.42.42 0 0 1 .307.13.42.42 0 0 1 .13.308z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/EditIcon.tsx b/server/sonar-ui-common/components/icons/EditIcon.tsx
new file mode 100644 (file)
index 0000000..1930be7
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function EditIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M4.875 12.986l.721-.72-1.861-1.862-.721.72v.848h1.014v1.014h.847zm4.143-7.35c0-.117-.058-.175-.174-.175a.183.183 0 0 0-.135.056L4.416 9.81a.183.183 0 0 0-.056.135c0 .116.058.174.175.174a.183.183 0 0 0 .134-.056L8.962 5.77a.183.183 0 0 0 .056-.134zM8.59 4.115l3.295 3.295L5.295 14H2v-3.295l6.59-6.59zm5.41.76a.97.97 0 0 1-.293.713l-1.315 1.315-3.295-3.295L10.412 2.3c.19-.2.428-.301.713-.301.28 0 .52.1.72.301l1.862 1.853c.195.206.293.447.293.721z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/EllipsisIcon.tsx b/server/sonar-ui-common/components/icons/EllipsisIcon.tsx
new file mode 100644 (file)
index 0000000..4ef8fe2
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function EllipsisIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M5.273 7.182v1.636a.818.818 0 0 1-.818.818H2.818A.818.818 0 0 1 2 8.818V7.182c0-.452.366-.818.818-.818h1.637c.451 0 .818.366.818.818zm4.363 0v1.636a.818.818 0 0 1-.818.818H7.182a.818.818 0 0 1-.818-.818V7.182c0-.452.366-.818.818-.818h1.636c.452 0 .818.366.818.818zm4.364 0v1.636a.818.818 0 0 1-.818.818h-1.637a.818.818 0 0 1-.818-.818V7.182c0-.452.367-.818.818-.818h1.637c.452 0 .818.366.818.818z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/ExpandIcon.tsx b/server/sonar-ui-common/components/icons/ExpandIcon.tsx
new file mode 100644 (file)
index 0000000..ffe6136
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function ExpandIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M7.898 9.25a.247.247 0 0 1-.078.18l-2.593 2.593 1.125 1.125a.48.48 0 0 1 .148.352.48.48 0 0 1-.148.352A.48.48 0 0 1 6 14H2.5a.48.48 0 0 1-.352-.148A.48.48 0 0 1 2 13.5V10a.48.48 0 0 1 .148-.352A.48.48 0 0 1 2.5 9.5a.48.48 0 0 1 .352.148l1.125 1.125L6.57 8.18a.247.247 0 0 1 .36 0l.89.89a.247.247 0 0 1 .078.18zM14 2.5V6a.48.48 0 0 1-.148.352.48.48 0 0 1-.352.148.48.48 0 0 1-.352-.148l-1.125-1.125L9.43 7.82a.247.247 0 0 1-.36 0l-.89-.89a.247.247 0 0 1 0-.36l2.593-2.593-1.125-1.125A.48.48 0 0 1 9.5 2.5a.48.48 0 0 1 .148-.352A.48.48 0 0 1 10 2h3.5a.48.48 0 0 1 .352.148A.48.48 0 0 1 14 2.5z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/ExpandSnippetIcon.tsx b/server/sonar-ui-common/components/icons/ExpandSnippetIcon.tsx
new file mode 100644 (file)
index 0000000..3ddea9c
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function ExpandSnippetIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <g fill="none" fillRule="evenodd">
+        <path
+          d="M8 1v4H4"
+          stroke={fill}
+          strokeWidth="2"
+          transform="scale(-.83333 -.84583) rotate(45 7.66 -19.75)"
+        />
+        <path d="M3 5.78h10v1.7H3z" fill={fill} />
+        <path d="M7.17 2.4h1.66v5.07H7.17z" fill={fill} />
+        <g>
+          <path
+            d="M8.16 1.81V6.1H3.9"
+            stroke={fill}
+            strokeWidth="2"
+            transform="scale(.83333 .84583) rotate(45 -4.2 13.2)"
+          />
+          <path d="M13 10.01H3v-1.7h10z" fill={fill} />
+          <path d="M8.83 13.4H7.17V9.15h1.66z" fill={fill} />
+        </g>
+      </g>
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/FavoriteIcon.tsx b/server/sonar-ui-common/components/icons/FavoriteIcon.tsx
new file mode 100644 (file)
index 0000000..2ebce21
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import { ThemeConsumer } from '../theme';
+import Icon, { IconProps } from './Icon';
+
+interface Props extends IconProps {
+  favorite: boolean;
+}
+
+export default function FavoriteIcon({ className, favorite, fill, ...iconProps }: Props) {
+  return (
+    <ThemeConsumer>
+      {(theme) => (
+        <Icon
+          className={classNames('icon-outline', { 'is-filled': favorite }, className)}
+          style={{ color: fill || theme.colors.orange }}
+          {...iconProps}>
+          <g transform="matrix(0.988024,0,0,0.988024,0.0957953,0.717719)">
+            <path d="M15.428,5.777C15.428,5.908 15.35,6.051 15.195,6.205L11.954,9.366L12.722,13.83C12.728,13.872 12.731,13.932 12.731,14.009C12.731,14.134 12.7,14.24 12.637,14.326C12.575,14.412 12.484,14.455 12.365,14.455C12.252,14.455 12.133,14.42 12.008,14.348L7.999,12.241L3.99,14.348C3.859,14.42 3.74,14.455 3.633,14.455C3.508,14.455 3.414,14.412 3.352,14.326C3.289,14.24 3.258,14.134 3.258,14.009C3.258,13.973 3.264,13.914 3.276,13.83L4.044,9.366L0.794,6.205C0.645,6.045 0.57,5.902 0.57,5.777C0.57,5.557 0.737,5.42 1.07,5.366L5.552,4.714L7.561,0.652C7.674,0.408 7.82,0.286 7.999,0.286C8.177,0.286 8.323,0.408 8.436,0.652L10.445,4.714L14.927,5.366C15.261,5.42 15.427,5.557 15.427,5.777L15.428,5.777Z" />
+          </g>
+        </Icon>
+      )}
+    </ThemeConsumer>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/FilterIcon.tsx b/server/sonar-ui-common/components/icons/FilterIcon.tsx
new file mode 100644 (file)
index 0000000..0253f65
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function FilterIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M13.957 2.333a.536.536 0 0 1-.12.596l-4.2 4.202v6.323a.552.552 0 0 1-.333.503.632.632 0 0 1-.213.043.51.51 0 0 1-.384-.162l-2.181-2.182a.542.542 0 0 1-.162-.383V7.13L2.162 2.929a.536.536 0 0 1-.12-.596A.552.552 0 0 1 2.547 2h10.908c.222 0 .418.137.503.333z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/GroupIcon.tsx b/server/sonar-ui-common/components/icons/GroupIcon.tsx
new file mode 100644 (file)
index 0000000..3a798ad
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IconProps, ThemedIcon } from './Icon';
+
+export default function GroupIcon({ fill, size = 36, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon viewBox="0 0 36 36" {...iconProps}>
+      {({ theme }) => (
+        <g transform="matrix(0.0625,0,0,0.0625,3,4)">
+          <path
+            d="M148.25,224C121.25,224.833 99.167,235.5 82,256L48.5,256C34.833,256 23.333,252.625 14,245.875C4.667,239.125 0,229.25 0,216.25C0,157.417 10.333,128 31,128C32,128 35.625,129.75 41.875,133.25C48.125,136.75 56.25,140.292 66.25,143.875C76.25,147.458 86.167,149.25 96,149.25C107.167,149.25 118.25,147.333 129.25,143.5C128.417,149.667 128,155.167 128,160C128,183.167 134.75,204.5 148.25,224ZM416,383.25C416,403.25 409.917,419.042 397.75,430.625C385.583,442.208 369.417,448 349.25,448L130.75,448C110.583,448 94.417,442.208 82.25,430.625C70.083,419.042 64,403.25 64,383.25C64,374.417 64.292,365.792 64.875,357.375C65.458,348.958 66.625,339.875 68.375,330.125C70.125,320.375 72.333,311.333 75,303C77.667,294.667 81.25,286.542 85.75,278.625C90.25,270.708 95.417,263.958 101.25,258.375C107.083,252.792 114.208,248.333 122.625,245C131.042,241.667 140.333,240 150.5,240C152.167,240 155.75,241.792 161.25,245.375C166.75,248.958 172.833,252.958 179.5,257.375C186.167,261.792 195.083,265.792 206.25,269.375C217.417,272.958 228.667,274.75 240,274.75C251.333,274.75 262.583,272.958 273.75,269.375C284.917,265.792 293.833,261.792 300.5,257.375C307.167,252.958 313.25,248.958 318.75,245.375C324.25,241.792 327.833,240 329.5,240C339.667,240 348.958,241.667 357.375,245C365.792,248.333 372.917,252.792 378.75,258.375C384.583,263.958 389.75,270.708 394.25,278.625C398.75,286.542 402.333,294.667 405,303C407.667,311.333 409.875,320.375 411.625,330.125C413.375,339.875 414.542,348.958 415.125,357.375C415.708,365.792 416,374.417 416,383.25ZM160,64C160,81.667 153.75,96.75 141.25,109.25C128.75,121.75 113.667,128 96,128C78.333,128 63.25,121.75 50.75,109.25C38.25,96.75 32,81.667 32,64C32,46.333 38.25,31.25 50.75,18.75C63.25,6.25 78.333,0 96,0C113.667,0 128.75,6.25 141.25,18.75C153.75,31.25 160,46.333 160,64ZM336,160C336,186.5 326.625,209.125 307.875,227.875C289.125,246.625 266.5,256 240,256C213.5,256 190.875,246.625 172.125,227.875C153.375,209.125 144,186.5 144,160C144,133.5 153.375,110.875 172.125,92.125C190.875,73.375 213.5,64 240,64C266.5,64 289.125,73.375 307.875,92.125C326.625,110.875 336,133.5 336,160ZM480,216.25C480,229.25 475.333,239.125 466,245.875C456.667,252.625 445.167,256 431.5,256L398,256C380.833,235.5 358.75,224.833 331.75,224C345.25,204.5 352,183.167 352,160C352,155.167 351.583,149.667 350.75,143.5C361.75,147.333 372.833,149.25 384,149.25C393.833,149.25 403.75,147.458 413.75,143.875C423.75,140.292 431.875,136.75 438.125,133.25C444.375,129.75 448,128 449,128C469.667,128 480,157.417 480,216.25ZM448,64C448,81.667 441.75,96.75 429.25,109.25C416.75,121.75 401.667,128 384,128C366.333,128 351.25,121.75 338.75,109.25C326.25,96.75 320,81.667 320,64C320,46.333 326.25,31.25 338.75,18.75C351.25,6.25 366.333,0 384,0C401.667,0 416.75,6.25 429.25,18.75C441.75,31.25 448,46.333 448,64Z"
+            style={{ fill: fill || theme.colors.gray67 }}
+          />
+        </g>
+      )}
+    </ThemedIcon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/HelpIcon.tsx b/server/sonar-ui-common/components/icons/HelpIcon.tsx
new file mode 100644 (file)
index 0000000..77973da
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+interface Props extends IconProps {
+  fillInner?: string;
+}
+
+export default function HelpIcon({ fill = 'currentColor', fillInner, ...iconProps }: Props) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M9.167 12.375v-1.75a.284.284 0 00-.082-.21.284.284 0 00-.21-.082h-1.75a.284.284 0 00-.21.082.284.284 0 00-.082.21v1.75c0 .085.028.155.082.21a.284.284 0 00.21.082h1.75a.284.284 0 00.21-.082.284.284 0 00.082-.21zM11.5 6.25c0-.535-.169-1.03-.506-1.486a3.452 3.452 0 00-1.262-1.057 3.462 3.462 0 00-1.55-.374c-1.476 0-2.603.647-3.381 1.942-.091.146-.067.273.073.383l1.203.911c.042.036.1.055.173.055a.269.269 0 00.228-.11c.322-.413.583-.692.784-.838.206-.146.468-.219.784-.219.291 0 .551.079.779.237.228.158.342.337.342.538 0 .23-.061.416-.183.556-.121.14-.328.276-.62.41a3.13 3.13 0 00-1.052.788c-.32.356-.479.737-.479 1.144v.328c0 .085.028.155.082.21a.284.284 0 00.21.082h1.75a.284.284 0 00.21-.082.284.284 0 00.082-.21c0-.115.065-.266.196-.45a1.54 1.54 0 01.496-.452c.195-.11.344-.196.447-.26a3.84 3.84 0 00.42-.319c.175-.149.31-.294.405-.437a2.407 2.407 0 00.369-1.29zM15 8c0 1.27-.313 2.441-.939 3.514a6.969 6.969 0 01-2.547 2.547A6.848 6.848 0 018 15a6.848 6.848 0 01-3.514-.939 6.969 6.969 0 01-2.547-2.547A6.848 6.848 0 011 8c0-1.27.313-2.441.939-3.514A6.969 6.969 0 014.486 1.94 6.848 6.848 0 018 1c1.27 0 2.441.313 3.514.939a6.969 6.969 0 012.547 2.547A6.848 6.848 0 0115 8z"
+        fill={fill}
+      />
+      {fillInner && (
+        <path
+          d="M9.167 12.375v-1.75a.284.284 0 00-.082-.21.284.284 0 00-.21-.082h-1.75a.284.284 0 00-.21.082.284.284 0 00-.082.21v1.75c0 .085.028.155.082.21a.284.284 0 00.21.082h1.75a.284.284 0 00.21-.082.284.284 0 00.082-.21zM11.5 6.25c0-.535-.169-1.03-.506-1.486a3.452 3.452 0 00-1.262-1.057 3.462 3.462 0 00-1.55-.374c-1.476 0-2.603.647-3.381 1.942-.091.146-.067.273.073.383l1.203.911c.042.036.1.055.173.055a.269.269 0 00.228-.11c.322-.413.583-.692.784-.838.206-.146.468-.219.784-.219.291 0 .551.079.779.237.228.158.342.337.342.538 0 .23-.061.416-.183.556-.121.14-.328.276-.62.41a3.13 3.13 0 00-1.052.788c-.32.356-.479.737-.479 1.144v.328c0 .085.028.155.082.21a.284.284 0 00.21.082h1.75a.284.284 0 00.21-.082.284.284 0 00.082-.21c0-.115.065-.266.196-.45a1.54 1.54 0 01.496-.452c.195-.11.344-.196.447-.26a3.84 3.84 0 00.42-.319c.175-.149.31-.294.405-.437a2.407 2.407 0 00.369-1.29z"
+          fill={fillInner}
+        />
+      )}
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/HistoryIcon.tsx b/server/sonar-ui-common/components/icons/HistoryIcon.tsx
new file mode 100644 (file)
index 0000000..63f69e6
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function HistoryIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M14.7 3.4v3.3c0 .1 0 .2-.1.2s-.2 0-.3-.1l-.9-.9-4.8 4.8c-.1.1-.1.1-.2.1s-.1 0-.2-.1L6.4 9l-3.2 3.2-1.5-1.5 4.5-4.5c.1-.1.1-.1.2-.1s.1 0 .2.1L8.4 8l3.5-3.5-.9-1c-.1-.1-.1-.2-.1-.3s.1-.1.2-.1h3.3c.1 0 .1 0 .2.1.1 0 .1.1.1.2z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/HomeIcon.tsx b/server/sonar-ui-common/components/icons/HomeIcon.tsx
new file mode 100644 (file)
index 0000000..0902723
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import { ThemeConsumer } from '../theme';
+import Icon, { IconProps } from './Icon';
+
+interface Props extends IconProps {
+  filled?: boolean;
+}
+
+export default function HomeIcon({ className, fill, filled = false, ...iconProps }: Props) {
+  return (
+    <ThemeConsumer>
+      {(theme) => (
+        <Icon
+          className={classNames(className, 'icon-outline', { 'is-filled': filled })}
+          style={{ color: fill || theme.colors.orange }}
+          {...iconProps}>
+          <g transform="matrix(0.870918,0,0,0.870918,0.978227,0.978227)">
+            <path d="M15.9,7.8L8.2,0.1C8.1,0 7.9,0 7.8,0.1L0.1,7.8C0,7.9 0,8.1 0.1,8.2C0.2,8.3 0.2,8.3 0.3,8.3L2.2,8.3L2.2,15.8C2.2,15.9 2.2,15.9 2.3,16C2.3,16 2.4,16.1 2.5,16.1L6.2,16.1C6.3,16.1 6.5,16 6.5,15.8L6.5,10.5L9.7,10.5L9.7,15.8C9.7,15.9 9.8,16.1 10,16.1L13.7,16.1C13.8,16.1 14,16 14,15.8L14,8.2L15.9,8.2C16,8.2 16,8.2 16.1,8.1C16,8 16.1,7.9 15.9,7.8Z" />
+          </g>
+        </Icon>
+      )}
+    </ThemeConsumer>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/HouseIcon.tsx b/server/sonar-ui-common/components/icons/HouseIcon.tsx
new file mode 100644 (file)
index 0000000..ac7232d
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function HouseIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M13.002 8.848v4.168a.56.56 0 0 1-.556.555H9.11v-3.334H6.89v3.334H3.554a.56.56 0 0 1-.556-.555V8.848c0-.018.01-.035.01-.052L8 4.68l4.993 4.116c.009.017.009.034.009.052zm1.936-.6l-.538.643a.289.289 0 0 1-.183.096h-.026a.273.273 0 0 1-.182-.061L8 3.916l-6.009 5.01a.297.297 0 0 1-.208.06.289.289 0 0 1-.183-.095l-.538-.642a.285.285 0 0 1 .035-.391L7.34 2.656a1.07 1.07 0 0 1 1.32 0l2.119 1.772V2.735c0-.157.121-.278.278-.278h1.667c.156 0 .278.121.278.278v3.542l1.901 1.58c.113.096.13.279.035.392z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/Icon.tsx b/server/sonar-ui-common/components/icons/Icon.tsx
new file mode 100644 (file)
index 0000000..35aae24
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { Theme, ThemeConsumer } from '../theme';
+
+export interface IconProps extends React.AriaAttributes {
+  className?: string;
+  fill?: string;
+  size?: number;
+}
+
+interface Props extends React.AriaAttributes {
+  children: React.ReactNode;
+  className?: string;
+  size?: number;
+  style?: React.CSSProperties;
+
+  // try to avoid using these:
+  width?: number;
+  height?: number;
+  viewBox?: string;
+}
+
+export default function Icon({
+  children,
+  className,
+  size = 16,
+  style,
+  height = size,
+  width = size,
+  viewBox = '0 0 16 16',
+  ...iconProps
+}: Props) {
+  return (
+    <svg
+      className={className}
+      height={height}
+      style={{
+        fillRule: 'evenodd',
+        clipRule: 'evenodd',
+        strokeLinejoin: 'round',
+        strokeMiterlimit: 1.41421,
+        ...style,
+      }}
+      version="1.1"
+      viewBox={viewBox}
+      width={width}
+      xmlnsXlink="http://www.w3.org/1999/xlink"
+      xmlSpace="preserve"
+      {...iconProps}>
+      {children}
+    </svg>
+  );
+}
+
+interface ThemedProps extends Props {
+  children: (themeContext: { theme: Theme }) => React.ReactNode;
+}
+
+export function ThemedIcon({ children, ...iconProps }: ThemedProps) {
+  return (
+    <ThemeConsumer>{(theme) => <Icon {...iconProps}>{children({ theme })}</Icon>}</ThemeConsumer>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/InfoIcon.tsx b/server/sonar-ui-common/components/icons/InfoIcon.tsx
new file mode 100644 (file)
index 0000000..9e75397
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function InfoIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M10.333 12.375v-1.458a.288.288 0 0 0-.291-.292h-.875V5.958a.288.288 0 0 0-.292-.291H5.958a.288.288 0 0 0-.291.291v1.459c0 .164.127.291.291.291h.875v2.917h-.875a.288.288 0 0 0-.291.292v1.458c0 .164.127.292.291.292h4.084a.288.288 0 0 0 .291-.292zM9.167 4.208V2.75a.288.288 0 0 0-.292-.292h-1.75a.288.288 0 0 0-.292.292v1.458c0 .164.128.292.292.292h1.75a.288.288 0 0 0 .292-.292zM15 8c0 3.865-3.135 7-7 7s-7-3.135-7-7 3.135-7 7-7 7 3.135 7 7z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/IssueIcon.tsx b/server/sonar-ui-common/components/icons/IssueIcon.tsx
new file mode 100644 (file)
index 0000000..8e1e10b
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import BugIcon from './BugIcon';
+import CodeSmellIcon from './CodeSmellIcon';
+import { IconProps } from './Icon';
+import SecurityHotspotIcon from './SecurityHotspotIcon';
+import VulnerabilityIcon from './VulnerabilityIcon';
+
+interface Props extends IconProps {
+  type: T.IssueType;
+}
+
+export default function IssueIcon({ type, ...iconProps }: Props) {
+  switch (type) {
+    case 'BUG':
+      return <BugIcon {...iconProps} />;
+    case 'VULNERABILITY':
+      return <VulnerabilityIcon {...iconProps} />;
+    case 'CODE_SMELL':
+      return <CodeSmellIcon {...iconProps} />;
+    case 'SECURITY_HOTSPOT':
+      return <SecurityHotspotIcon {...iconProps} />;
+    default:
+      return null;
+  }
+}
diff --git a/server/sonar-ui-common/components/icons/IssueTypeIcon.tsx b/server/sonar-ui-common/components/icons/IssueTypeIcon.tsx
new file mode 100644 (file)
index 0000000..1962b67
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IconProps } from './Icon';
+import IssueIcon from './IssueIcon';
+
+export interface Props extends IconProps {
+  query: string;
+}
+
+export default function IssueTypeIcon({ query, ...iconProps }: Props) {
+  let type: T.IssueType;
+
+  switch (query.toLowerCase()) {
+    case 'bug':
+    case 'bugs':
+    case 'new_bugs':
+      type = 'BUG';
+      break;
+    case 'vulnerability':
+    case 'vulnerabilities':
+    case 'new_vulnerabilities':
+      type = 'VULNERABILITY';
+      break;
+    case 'code_smell':
+    case 'code_smells':
+    case 'new_code_smells':
+      type = 'CODE_SMELL';
+      break;
+    case 'security_hotspot':
+    case 'security_hotspots':
+    case 'new_security_hotspots':
+      type = 'SECURITY_HOTSPOT';
+      break;
+    default:
+      return null;
+  }
+
+  return <IssueIcon type={type} {...iconProps} />;
+}
diff --git a/server/sonar-ui-common/components/icons/LightBulbIcon.tsx b/server/sonar-ui-common/components/icons/LightBulbIcon.tsx
new file mode 100644 (file)
index 0000000..ec5b999
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function LightBulbIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M10.042 5.083a.3.3 0 0 1-.292.292.3.3 0 0 1-.292-.292c0-.629-.975-.875-1.458-.875a.3.3 0 0 1-.292-.291A.3.3 0 0 1 8 3.625c.848 0 2.042.447 2.042 1.458zm1.458 0c0-1.823-1.85-2.916-3.5-2.916S4.5 3.26 4.5 5.083c0 .584.237 1.194.62 1.641.173.2.373.392.556.602.647.774 1.194 1.686 1.285 2.716h2.078c.091-1.03.638-1.942 1.285-2.716.183-.21.383-.402.556-.602.383-.447.62-1.057.62-1.64zm1.167 0c0 .94-.31 1.75-.94 2.443-.628.693-1.457 1.668-1.53 2.643a.876.876 0 0 1 .428.748.852.852 0 0 1-.228.583.852.852 0 0 1 .228.583c0 .301-.155.575-.41.739a.89.89 0 0 1 .118.428c0 .592-.465.875-.993.875A1.479 1.479 0 0 1 8 15a1.479 1.479 0 0 1-1.34-.875c-.528 0-.993-.283-.993-.875 0-.146.045-.3.118-.428a.876.876 0 0 1-.41-.739c0-.218.082-.428.228-.583a.852.852 0 0 1-.228-.583c0-.301.164-.593.428-.748-.073-.975-.902-1.95-1.53-2.643a3.507 3.507 0 0 1-.94-2.443C3.333 2.604 5.694 1 8 1c2.306 0 4.667 1.604 4.667 4.083z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/LinkIcon.tsx b/server/sonar-ui-common/components/icons/LinkIcon.tsx
new file mode 100644 (file)
index 0000000..ba5c1a3
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function LinkIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <g transform="matrix(0.823497,0,0,0.823497,1.47008,1.4122)">
+        <path
+          d="M13.501,11.429C13.501,11.191 13.418,10.989 13.251,10.822L11.394,8.965C11.227,8.798 11.025,8.715 10.787,8.715C10.537,8.715 10.323,8.81 10.144,9.001C10.162,9.019 10.219,9.074 10.314,9.166C10.409,9.258 10.473,9.322 10.506,9.358C10.539,9.394 10.583,9.451 10.64,9.528C10.697,9.605 10.735,9.681 10.756,9.756C10.777,9.831 10.787,9.913 10.787,10.002C10.787,10.24 10.704,10.442 10.537,10.609C10.37,10.776 10.168,10.859 9.93,10.859C9.841,10.859 9.759,10.849 9.684,10.828C9.609,10.807 9.533,10.769 9.456,10.712C9.379,10.655 9.322,10.611 9.286,10.578C9.25,10.545 9.186,10.481 9.094,10.386C9.002,10.291 8.947,10.234 8.929,10.216C8.732,10.401 8.634,10.618 8.634,10.868C8.634,11.106 8.717,11.308 8.884,11.475L10.723,13.323C10.884,13.484 11.086,13.564 11.33,13.564C11.568,13.564 11.77,13.487 11.937,13.332L13.25,12.028C13.417,11.861 13.5,11.662 13.5,11.43L13.501,11.429ZM7.224,5.134C7.224,4.896 7.141,4.694 6.974,4.527L5.135,2.679C4.968,2.512 4.766,2.429 4.528,2.429C4.296,2.429 4.094,2.509 3.921,2.67L2.608,3.974C2.441,4.141 2.358,4.34 2.358,4.572C2.358,4.81 2.441,5.012 2.608,5.179L4.465,7.036C4.626,7.197 4.828,7.277 5.072,7.277C5.322,7.277 5.536,7.185 5.715,7C5.697,6.982 5.64,6.927 5.545,6.835C5.45,6.743 5.386,6.679 5.353,6.643C5.32,6.607 5.276,6.55 5.219,6.473C5.162,6.396 5.124,6.32 5.103,6.245C5.082,6.17 5.072,6.088 5.072,5.999C5.072,5.761 5.155,5.559 5.322,5.392C5.489,5.225 5.691,5.142 5.929,5.142C6.018,5.142 6.1,5.152 6.175,5.173C6.25,5.194 6.326,5.232 6.403,5.289C6.48,5.346 6.537,5.39 6.573,5.423C6.609,5.456 6.673,5.52 6.765,5.615C6.857,5.71 6.912,5.767 6.93,5.785C7.127,5.6 7.225,5.383 7.225,5.133L7.224,5.134ZM15.215,11.429C15.215,12.143 14.962,12.747 14.456,13.242L13.143,14.546C12.649,15.04 12.045,15.287 11.33,15.287C10.61,15.287 10.003,15.034 9.509,14.528L7.67,12.68C7.176,12.186 6.929,11.582 6.929,10.867C6.929,10.135 7.191,9.513 7.715,9.001L6.929,8.215C6.417,8.739 5.798,9.001 5.072,9.001C4.358,9.001 3.751,8.751 3.251,8.251L1.394,6.394C0.894,5.894 0.644,5.287 0.644,4.573C0.644,3.859 0.897,3.255 1.403,2.76L2.716,1.456C3.21,0.962 3.814,0.715 4.529,0.715C5.249,0.715 5.856,0.968 6.35,1.474L8.189,3.322C8.683,3.816 8.93,4.42 8.93,5.135C8.93,5.867 8.668,6.489 8.144,7.001L8.93,7.787C9.442,7.263 10.061,7.001 10.787,7.001C11.501,7.001 12.108,7.251 12.608,7.751L14.465,9.608C14.965,10.108 15.215,10.715 15.215,11.429L15.215,11.429Z"
+          style={{ fill }}
+        />
+      </g>
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/ListIcon.tsx b/server/sonar-ui-common/components/icons/ListIcon.tsx
new file mode 100644 (file)
index 0000000..8596ef9
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function ListIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M15.045 11.526v1.007q0 0.204-0.149 0.354t-0.354 0.149h-13.084q-0.204 0-0.354-0.149t-0.149-0.354v-1.006q0-0.204 0.149-0.354t0.354-0.149h13.084q0.204 0 0.354 0.149t0.149 0.354zM15.045 8.506v1.006q0 0.204-0.149 0.354t-0.354 0.149h-13.084q-0.204 0-0.354-0.149t-0.149-0.354v-1.006q0-0.204 0.149-0.354t0.354-0.149h13.084q0.204 0 0.354 0.149t0.149 0.354zM15.045 5.487v1.006q0 0.204-0.149 0.354t-0.354 0.149h-13.084q-0.204 0-0.354-0.149t-0.149-0.354v-1.006q0-0.204 0.149-0.354t0.354-0.149h13.084q0.204 0 0.354 0.149t0.149 0.354zM15.045 2.468v1.006q0 0.204-0.149 0.354t-0.354 0.149h-13.084q-0.204 0-0.354-0.149t-0.149-0.354v-1.006q0-0.204 0.149-0.354t0.354-0.149h13.084q0.204 0 0.354 0.149t0.149 0.354z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/LockIcon.tsx b/server/sonar-ui-common/components/icons/LockIcon.tsx
new file mode 100644 (file)
index 0000000..827e287
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function LockIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M5.455 7.364h5.09v-1.91A2.55 2.55 0 0 0 8 2.91a2.55 2.55 0 0 0-2.545 2.546v1.909zm8.272.954v5.727a.955.955 0 0 1-.954.955H3.227a.955.955 0 0 1-.954-.955V8.318c0-.527.427-.954.954-.954h.318v-1.91C3.545 3.01 5.554 1 8 1s4.455 2.009 4.455 4.455v1.909h.318c.527 0 .954.427.954.954z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/LongLivingBranchIcon.tsx b/server/sonar-ui-common/components/icons/LongLivingBranchIcon.tsx
new file mode 100644 (file)
index 0000000..a2f7cc4
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IconProps, ThemedIcon } from './Icon';
+
+export default function LongLivingBranchIcon({ fill, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <g transform="translate(5, 0)">
+          <path
+            d="M4.5 8c0-.9-.6-1.7-1.5-1.9V4c.9-.2 1.5-1 1.5-1.9 0-1.1-.9-2-2-2s-2 .9-2 2C.5 3 1.1 3.8 2 4v2.1C1.1 6.3.5 7.1.5 8s.6 1.7 1.5 2v2.1c-.9.2-1.5 1-1.5 1.9 0 1.1.9 2 2 2s2-.9 2-2c0-.9-.6-1.7-1.5-1.9V10c.9-.3 1.5-1 1.5-2zm-3-5.9c0-.6.4-1 1-1s1 .4 1 1-.4 1-1 1-1-.5-1-1zm0 5.9c0-.6.4-1 1-1s1 .4 1 1-.4 1-1 1-1-.4-1-1zm2 6c0 .6-.4 1-1 1s-1-.4-1-1 .4-1 1-1 1 .5 1 1z"
+            style={{ fill: fill || theme.colors.blue }}
+          />
+        </g>
+      )}
+    </ThemedIcon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/MeasuresIcon.tsx b/server/sonar-ui-common/components/icons/MeasuresIcon.tsx
new file mode 100644 (file)
index 0000000..c1dd222
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function MeasuresIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps} style={{ fillRule: 'nonzero' }}>
+      <path d="M3.33 6.13h2v6.54h-2zm3.74-2.8h1.86v9.34H7.07zm3.73 5.34h1.87v4H10.8z" fill={fill} />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/MinimizeIcon.tsx b/server/sonar-ui-common/components/icons/MinimizeIcon.tsx
new file mode 100644 (file)
index 0000000..ce2f678
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function MinimizeIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M14 12.1v1.267c0 .176-.08.325-.239.448a.918.918 0 0 1-.58.185H2.819a.918.918 0 0 1-.58-.185C2.08 13.692 2 13.543 2 13.367V12.1c0-.176.08-.326.239-.449a.918.918 0 0 1 .58-.185h10.363c.227 0 .42.062.58.185.158.123.238.273.238.449z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/NotificationIcon.tsx b/server/sonar-ui-common/components/icons/NotificationIcon.tsx
new file mode 100644 (file)
index 0000000..2ba99cc
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IconProps, ThemedIcon } from './Icon';
+
+interface Props extends IconProps {
+  hasUnread?: boolean;
+}
+
+export default function NotificationIcon({
+  fill = 'currentColor',
+  hasUnread,
+  ...iconProps
+}: Props) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) =>
+        hasUnread ? (
+          <>
+            <path
+              d="M8 1a.875.875 0 0 0-.875.875v.57c-2.009.418-3.498 2.118-3.498 4.242 0 2.798-.987 3.652-1.516 4.22a.856.856 0 0 0-.236.593.875.875 0 0 0 .877.875h10.496a.875.875 0 0 0 .877-.875.854.854 0 0 0-.236-.594c-.497-.534-1.388-1.342-1.494-3.76a2.814 2.814 0 0 1-.768.108A2.814 2.814 0 0 1 8.814 4.44a2.814 2.814 0 0 1 .665-1.818 4.543 4.543 0 0 0-.604-.178v-.57A.875.875 0 0 0 8 1zM6.25 13.25a1.75 1.75 0 0 0 3.5 0h-3.5z"
+              style={{ fill }}
+            />
+            <circle cx="11.627" cy="4.441" r="2" style={{ fill: theme.colors.blue }} />
+          </>
+        ) : (
+          <path
+            d="M8 15a1.75 1.75 0 0 0 1.75-1.75h-3.5c0 .967.784 1.75 1.75 1.75zm5.89-4.094c-.529-.567-1.517-1.421-1.517-4.218 0-2.125-1.49-3.826-3.499-4.243v-.57a.875.875 0 1 0-1.748 0v.57c-2.01.417-3.499 2.118-3.499 4.243 0 2.797-.988 3.65-1.517 4.218a.854.854 0 0 0-.235.594.876.876 0 0 0 .878.875h10.494a.876.876 0 0 0 .878-.875.853.853 0 0 0-.235-.594z"
+            style={{ fill }}
+          />
+        )
+      }
+    </ThemedIcon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/OnboardingAddMembersIcon.tsx b/server/sonar-ui-common/components/icons/OnboardingAddMembersIcon.tsx
new file mode 100644 (file)
index 0000000..536ce5b
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IconProps, ThemedIcon } from './Icon';
+
+export default function OnboardingAddMembersIcon({ fill, size = 64, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon height={(size / 64) * 80} viewBox="0 0 64 80" width={size} {...iconProps}>
+      {({ theme }) => (
+        <g>
+          <path
+            d="M49 34c0 9.389-7.611 17-17 17s-17-7.611-17-17 7.611-17 17-17 17 7.611 17 17z"
+            style={{ fill: 'none', stroke: fill || theme.colors.darkBlue, strokeWidth: 2 }}
+          />
+          <path
+            d="M36 32c0 2.2-1.8 4-4 4s-4-1.8-4-4v-1c0-2.2 1.8-4 4-4s4 1.8 4 4v1zm4 39a8 8 0 1 1-16 0 8 8 0 0 1 16 0z"
+            style={{ fill: 'none', stroke: fill || theme.colors.darkBlue, strokeWidth: 2 }}
+          />
+          <path
+            d="M33 70h2v2h-2v2h-2v-2h-2v-2h2v-2h2v2zm-5-14l-.072-.001c-1.521-.054-2.834-1.337-2.925-2.855L25 50h2c0 1.745-.532 3.91.952 3.999L28 54h8v.002l.072-.005c.506-.042.922-.489.928-1.003V50h2c0 1.024.011 2.048-.001 3.072-.054 1.518-1.337 2.834-2.855 2.925l-.072.002L36 56v8h-2v-7.982c-1.333.007-2.667.007-4 0V64h-2v-8zm-7 0H1V10 0h62v56H43v-2h18V10H3v44h18v2zm38-4H43v-2h14V14H7v36h14v2H5V12h54v40zm-19-9l1 .017c-.03 1.79-2.454 2.506-3.918 2.717-4.074.584-8.503.911-12.176-.477-.949-.358-1.887-1.119-1.906-2.24l.191-.017H23v-3.566l5.38-3.228.913-.913 1.414 1.414-1.087 1.087L25 40.566v2.438c.067 1.304 10.98 2.117 13.844.157.076-.052.152-.172.156-.178v-2.417l-4.62-2.772-1.087-1.087 1.414-1.414.913.913L41 39.434V43h-1zm14-4h-2v-2h2v2zm-42 0h-2v-2h2v2zm42-4h-2v-2h2v2zm-42 0h-2v-2h2v2zm42-4h-2v-2h2v2zm-42 0h-2v-2h2v2zm20.198-10.999c3.529.062 6.837 1.669 9.386 4.169l-1.289 1.539c-4.178-4.152-11.167-5.254-16.359-.228l-.231.228-1.41-1.418c2.633-2.617 6.031-4.313 9.903-4.29zM3 2v6h58V2H3zm56 4H17V4h42v2zM11 6H9V4h2v2zM7 6H5V4h2v2zm8 0h-2V4h2v2z"
+            style={{ fill: fill || theme.colors.darkBlue, fillRule: 'nonzero' }}
+          />
+        </g>
+      )}
+    </ThemedIcon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/OnboardingProjectIcon.tsx b/server/sonar-ui-common/components/icons/OnboardingProjectIcon.tsx
new file mode 100644 (file)
index 0000000..d46a9a4
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IconProps, ThemedIcon } from './Icon';
+
+export default function OnboardingProjectIcon({ fill, size = 64, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon size={size} viewBox="0 0 64 64" {...iconProps}>
+      {({ theme }) => (
+        <g fill="none" fillRule="evenodd" stroke={fill || theme.colors.darkBlue} strokeWidth="2">
+          <path d="M2 59h60V13H2zm0-46h60V5H2zm3-4h2m2 0h2m2 0h2m2 0h42" />
+          <path d="M59 34h-6l-2-4h-6l-2 5h-6l-2 2h-6l-2-4h-6l-2 5h-6l-2 4H5m1 14v-9m4 9v-6m4 6V43m4 13V45m4 11V42m4 14V39m4 17V41m4 15V46m4 10V40m4 16V44m4 12V37m4 19V38m4 18V43m4 13V39m-3-18h-2m-2 0h-2m-2 0h-2M9 29h14M9 33h7m17-12h8m-14 4h8m-8-4h4m-21 4h12v-4H10z" />
+          <path d="M58 31V17H6v22" />
+        </g>
+      )}
+    </ThemedIcon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/OnboardingTeamIcon.tsx b/server/sonar-ui-common/components/icons/OnboardingTeamIcon.tsx
new file mode 100644 (file)
index 0000000..0f57cf4
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IconProps, ThemedIcon } from './Icon';
+
+export default function OnboardingTeamIcon({ fill, size = 64, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon size={size} viewBox="0 0 64 64" {...iconProps}>
+      {({ theme }) => (
+        <g fill="none" fillRule="evenodd" stroke={fill || theme.colors.darkBlue} strokeWidth="2">
+          <path d="M32 9v5M11.5195 43.0898l7.48-4.091m33.481-18.0994l-7.48 4.1m-33.481-4.1l7.48 4.1M45 38.999l7.48 4.101M32 50v5m15-23c0 8.284-6.715 15-15 15s-15-6.716-15-15c0-8.285 6.715-15 15-15s15 6.715 15 15z" />
+          <path d="M40 38c0 1.656-3.58 2-8 2s-8-.344-8-2m16 0v-3l-5-3-1-1m-10 7v-3l5-3 1-1m6-4c0 2.2-1.8 4-4 4s-4-1.8-4-4v-1c0-2.2 1.8-4 4-4s4 1.8 4 4v1zm-.0098-21.71c7.18 1.069 13.439 4.96 17.609 10.51m-17.609 42.91c7.18-1.07 13.439-4.96 17.609-10.51M6.6299 41.25c-1.06-2.88-1.63-6-1.63-9.25s.57-6.37 1.63-9.25m3.7705-6.9502c4.17-5.55 10.43-9.44 17.609-10.51m-17.609 42.9104c4.17 5.55 10.43 9.439 17.609 10.51M57.3701 22.75c1.06 2.88 1.63 6 1.63 9.25s-.57 6.37-1.63 9.25" />
+          <path d="M36 5c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2M12 19c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2m51 0c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2M12 45c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2m51 0c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2M36 59c0 2.209-1.79 4-4 4-2.209 0-4-1.791-4-4 0-2.21 1.791-4 4-4 2.21 0 4 1.79 4 4zm-5 0h2" />
+        </g>
+      )}
+    </ThemedIcon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/OpenCloseIcon.tsx b/server/sonar-ui-common/components/icons/OpenCloseIcon.tsx
new file mode 100644 (file)
index 0000000..169edb7
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import ChevronDownIcon from './ChevronDownIcon';
+import ChevronRightIcon from './ChevronRightIcon';
+import { IconProps } from './Icon';
+
+interface Props extends IconProps {
+  open: boolean;
+}
+
+export default function OpenCloseIcon({ open, ...iconProps }: Props) {
+  return open ? <ChevronDownIcon {...iconProps} /> : <ChevronRightIcon {...iconProps} />;
+}
diff --git a/server/sonar-ui-common/components/icons/PendingIcon.tsx b/server/sonar-ui-common/components/icons/PendingIcon.tsx
new file mode 100644 (file)
index 0000000..32e02a3
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IconProps, ThemedIcon } from './Icon';
+
+export default function PendingIcon({ fill, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <g transform="matrix(0.0364583,0,0,0.0364583,1,-0.166667)">
+          <path
+            d="M224,136L224,248C224,250.333 223.25,252.25 221.75,253.75C220.25,255.25 218.333,256 216,256L136,256C133.667,256 131.75,255.25 130.25,253.75C128.75,252.25 128,250.333 128,248L128,232C128,229.667 128.75,227.75 130.25,226.25C131.75,224.75 133.667,224 136,224L192,224L192,136C192,133.667 192.75,131.75 194.25,130.25C195.75,128.75 197.667,128 200,128L216,128C218.333,128 220.25,128.75 221.75,130.25C223.25,131.75 224,133.667 224,136ZM328,224C328,199.333 321.917,176.583 309.75,155.75C297.583,134.917 281.083,118.417 260.25,106.25C239.417,94.083 216.667,88 192,88C167.333,88 144.583,94.083 123.75,106.25C102.917,118.417 86.417,134.917 74.25,155.75C62.083,176.583 56,199.333 56,224C56,248.667 62.083,271.417 74.25,292.25C86.417,313.083 102.917,329.583 123.75,341.75C144.583,353.917 167.333,360 192,360C216.667,360 239.417,353.917 260.25,341.75C281.083,329.583 297.583,313.083 309.75,292.25C321.917,271.417 328,248.667 328,224ZM384,224C384,258.833 375.417,290.958 358.25,320.375C341.083,349.792 317.792,373.083 288.375,390.25C258.958,407.417 226.833,416 192,416C157.167,416 125.042,407.417 95.625,390.25C66.208,373.083 42.917,349.792 25.75,320.375C8.583,290.958 0,258.833 0,224C0,189.167 8.583,157.042 25.75,127.625C42.917,98.208 66.208,74.917 95.625,57.75C125.042,40.583 157.167,32 192,32C226.833,32 258.958,40.583 288.375,57.75C317.792,74.917 341.083,98.208 358.25,127.625C375.417,157.042 384,189.167 384,224Z"
+            style={{ fill: fill || theme.colors.gray67 }}
+          />
+        </g>
+      )}
+    </ThemedIcon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/PinIcon.tsx b/server/sonar-ui-common/components/icons/PinIcon.tsx
new file mode 100644 (file)
index 0000000..0efc3b6
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function PinIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M7.25 7.25v-3.5a.243.243 0 0 0-.07-.18A.243.243 0 0 0 7 3.5a.243.243 0 0 0-.18.07.243.243 0 0 0-.07.18v3.5c0 .073.023.133.07.18.047.047.107.07.18.07a.243.243 0 0 0 .18-.07.243.243 0 0 0 .07-.18zM12.5 10a.482.482 0 0 1-.148.352.482.482 0 0 1-.352.148H8.648l-.398 3.773a.29.29 0 0 1-.082.161.219.219 0 0 1-.16.066H8c-.141 0-.224-.07-.25-.211L7.156 10.5H4a.482.482 0 0 1-.352-.148A.482.482 0 0 1 3.5 10c0-.641.204-1.217.613-1.73.409-.513.871-.77 1.387-.77v-4a.96.96 0 0 1-.703-.297A.96.96 0 0 1 4.5 2.5a.96.96 0 0 1 .297-.703A.96.96 0 0 1 5.5 1.5h5a.96.96 0 0 1 .703.297.96.96 0 0 1 .297.703.96.96 0 0 1-.297.703.96.96 0 0 1-.703.297v4c.516 0 .978.257 1.387.77.409.513.613 1.089.613 1.73z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/PlusCircleIcon.tsx b/server/sonar-ui-common/components/icons/PlusCircleIcon.tsx
new file mode 100644 (file)
index 0000000..e2852cc
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function PlusCircleIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M8 1c3.863 0 7 3.137 7 7s-3.137 7-7 7-7-3.137-7-7 3.137-7 7-7zm3.726 7.985A.274.274 0 0 0 12 8.711V7.289a.274.274 0 0 0-.274-.274H8.985V4.274A.274.274 0 0 0 8.711 4H7.289a.274.274 0 0 0-.274.274v2.741H4.274A.274.274 0 0 0 4 7.289v1.422c0 .152.123.274.274.274h2.741v2.741c0 .151.122.274.274.274h1.422a.274.274 0 0 0 .274-.274V8.985h2.741z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/PlusIcon.tsx b/server/sonar-ui-common/components/icons/PlusIcon.tsx
new file mode 100644 (file)
index 0000000..3fd1bba
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function PlusIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path d="M1,7L7,7L7,1L9,1L9,7L15,7L15,9L9,9L9,15L7,15L7,9L1,9L1,7Z" style={{ fill }} />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/ProjectEventIcon.tsx b/server/sonar-ui-common/components/icons/ProjectEventIcon.tsx
new file mode 100644 (file)
index 0000000..88244d7
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function ProjectEventIcon({ fill = '#fff', size = 14, ...iconProps }: IconProps) {
+  return (
+    <Icon size={size} {...iconProps}>
+      <path
+        d="M8 2 L14 8 L8 14 L2 8 L8 2 L14 8"
+        style={{ fill, stroke: 'currentColor', strokeWidth: '2px' }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/ProjectLinkIcon.tsx b/server/sonar-ui-common/components/icons/ProjectLinkIcon.tsx
new file mode 100644 (file)
index 0000000..cd95640
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import BugTrackerIcon from './BugTrackerIcon';
+import ContinuousIntegrationIcon from './ContinuousIntegrationIcon';
+import DetachIcon from './DetachIcon';
+import HouseIcon from './HouseIcon';
+import { IconProps } from './Icon';
+import SCMIcon from './SCMIcon';
+
+interface ProjectLinkIconProps {
+  type: string;
+}
+
+export default function ProjectLinkIcon({ type, ...iconProps }: IconProps & ProjectLinkIconProps) {
+  switch (type) {
+    case 'issue':
+      return <BugTrackerIcon {...iconProps} />;
+    case 'homepage':
+      return <HouseIcon {...iconProps} />;
+    case 'ci':
+      return <ContinuousIntegrationIcon {...iconProps} />;
+    case 'scm':
+      return <SCMIcon {...iconProps} />;
+    default:
+      return <DetachIcon {...iconProps} />;
+  }
+}
diff --git a/server/sonar-ui-common/components/icons/PullRequestIcon.tsx b/server/sonar-ui-common/components/icons/PullRequestIcon.tsx
new file mode 100644 (file)
index 0000000..457d2d2
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IconProps, ThemedIcon } from './Icon';
+
+export default function PullRequestIcon({ fill, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M13,11.9L13,5.5C13,5.4 13.232,1.996 7.9,2L9.1,0.8L8.5,0.1L5.9,2.6L8.5,5.1L9.2,4.4L7.905,3.008C12.256,2.99 12,5.4 12,5.5L12,11.9C11.1,12.1 10.5,12.9 10.5,13.8C10.5,14.9 11.4,15.8 12.5,15.8C13.6,15.8 14.5,14.9 14.5,13.8C14.5,12.9 13.9,12.2 13,11.9ZM4,11.9C4.9,12.2 5.5,12.9 5.5,13.8C5.5,14.9 4.6,15.8 3.5,15.8C2.4,15.8 1.5,14.9 1.5,13.8C1.5,12.9 2.1,12.1 3,11.9L3,4.1C2.1,3.9 1.5,3.1 1.5,2.2C1.5,1.1 2.4,0.2 3.5,0.2C4.6,0.2 5.5,1.1 5.5,2.2C5.5,3.1 4.9,3.9 4,4.1L4,11.9ZM12.5,14.9C11.9,14.9 11.5,14.5 11.5,13.9C11.5,13.3 11.9,12.9 12.5,12.9C13.1,12.9 13.5,13.3 13.5,13.9C13.5,14.5 13.1,14.9 12.5,14.9ZM3.5,14.9C2.9,14.9 2.5,14.5 2.5,13.9C2.5,13.3 2.9,12.9 3.5,12.9C4.1,12.9 4.5,13.3 4.5,13.9C4.5,14.5 4.1,14.9 3.5,14.9ZM2.5,2.2C2.5,1.6 2.9,1.2 3.5,1.2C4.1,1.2 4.5,1.6 4.5,2.2C4.5,2.8 4.1,3.2 3.5,3.2C2.9,3.2 2.5,2.8 2.5,2.2Z"
+          style={{ fill: fill || theme.colors.blue }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/QualifierIcon.tsx b/server/sonar-ui-common/components/icons/QualifierIcon.tsx
new file mode 100644 (file)
index 0000000..bff1196
--- /dev/null
@@ -0,0 +1,185 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IconProps, ThemedIcon } from './Icon';
+
+const qualifierIcons: T.Dict<(props: IconProps) => React.ReactElement> = {
+  app: ApplicationIcon,
+  brc: SubProjectIcon,
+  dev: DeveloperIcon,
+  dir: DirectoryIcon,
+  fil: FileIcon,
+  svw: SubPortfolioIcon,
+  trk: ProjectIcon,
+  uts: UnitTestIcon,
+  vw: PortfolioIcon,
+
+  // deprecated:
+  cla: UnitTestIcon,
+  dev_prj: ProjectIcon,
+  lib: LibraryIcon,
+  pac: DirectoryIcon,
+};
+
+interface QualifierIconProps {
+  className?: string;
+  fill?: string;
+  qualifier: string | null | undefined;
+}
+
+export default function QualifierIcon(props: QualifierIconProps) {
+  if (!props.qualifier) {
+    return null;
+  }
+
+  const qualifier = props.qualifier.toLowerCase();
+  const FoundIcon = qualifierIcons[qualifier];
+  return FoundIcon ? <FoundIcon className={props.className} fill={props.fill} /> : null;
+}
+
+function ApplicationIcon({ fill, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M3.014 10.986a2 2 0 1 1-.001 4.001 2 2 0 0 1 .001-4.001zm9.984 0a2 2 0 1 1-.001 4.001 2 2 0 0 1 .001-4.001zm-5.004-.021c1.103 0 2 .896 2 2s-.897 2-2 2a2 2 0 0 1 0-4zm-4.98 1.021a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm9.984 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm-5.004-.021a1 1 0 1 1 0 2 1 1 0 0 1 0-2zM2.984 6a2 2 0 1 1-.001 4.001A2 2 0 0 1 2.984 6zm9.984 0a2 2 0 1 1-.001 4.001A2 2 0 0 1 12.968 6zm-5.004-.021c1.103 0 2 .897 2 2a2 2 0 1 1-2-2zM2.984 7a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm9.984 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm-5.004-.021a1.001 1.001 0 0 1 0 2 1 1 0 0 1 0-2zM3 1.025a2 2 0 1 1-.001 4.001A2 2 0 0 1 3 1.025zm9.984 0a2 2 0 1 1-.001 4.001 2 2 0 0 1 .001-4.001zM7.98 1.004c1.103 0 2 .896 2 2s-.897 2-2 2a2 2 0 0 1 0-4zM3 2.025a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm9.984 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2zM7.98 2.004a1.001 1.001 0 0 1 0 2 1 1 0 0 1 0-2z"
+          style={{ fill: fill || theme.colors.blue }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function DeveloperIcon({ fill, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M7.974 8.02a3.5 3.5 0 0 1-2.482-1.017 3.428 3.428 0 0 1-1.028-2.455c0-.927.365-1.8 1.028-2.455a3.505 3.505 0 0 1 2.482-1.017 3.5 3.5 0 0 1 2.482 1.017 3.434 3.434 0 0 1 1.027 2.455c0 .928-.365 1.8-1.027 2.455A3.504 3.504 0 0 1 7.974 8.02zm0-5.778c-1.286 0-2.332 1.034-2.332 2.306s1.046 2.307 2.332 2.307c1.285 0 2.332-1.035 2.332-2.307S9.258 2.242 7.974 2.242zm3.534 6.418c.127.016.243.045.348.086.17.066.302.146.406.246.132.124.253.282.36.47.126.218.226.442.3.668.08.253.15.535.206.838.056.313.095.604.113.867.02.28.03.57.03.862 0 .532-.174.758-.306.882-.142.132-.397.31-.973.31H3.948c-.233 0-.437-.03-.606-.09-.14-.05-.26-.123-.366-.222-.13-.123-.306-.35-.306-.88 0-.294.01-.584.03-.863.018-.263.056-.554.112-.867a6.5 6.5 0 0 1 .207-.838c.073-.226.173-.45.298-.667.108-.19.23-.347.36-.47.106-.1.238-.18.407-.247.105-.04.22-.07.348-.086.202.13.432.277.683.435.342.217.756.4 1.265.564.523.166 1.06.25 1.59.25a5.25 5.25 0 0 0 1.592-.25c.51-.164.923-.348 1.266-.565.25-.158.48-.304.682-.435l-.002.002zm-.244-1.18c-.055 0-.184.066-.387.196-.202.13-.43.276-.685.437-.255.16-.586.307-.994.437-.408.13-.818.196-1.23.196-.41 0-.82-.065-1.228-.196a4.303 4.303 0 0 1-.993-.437c-.255-.16-.484-.306-.686-.437-.202-.13-.33-.196-.386-.196-.374 0-.716.06-1.026.183-.31.12-.572.283-.787.487a3.28 3.28 0 0 0-.57.737 4.662 4.662 0 0 0-.395.888c-.098.303-.18.633-.244.988a9.652 9.652 0 0 0-.128.992c-.02.306-.032.62-.032.942 0 .73.224 1.304.672 1.726.448.42 1.043.632 1.785.632h8.044c.743 0 1.34-.21 1.787-.633.447-.42.67-.996.67-1.725 0-.32-.01-.635-.03-.942a9.159 9.159 0 0 0-.374-1.98c-.098-.304-.23-.6-.395-.888a3.23 3.23 0 0 0-.57-.737 2.404 2.404 0 0 0-.788-.487 2.779 2.779 0 0 0-1.026-.183h-.004z"
+          style={{ fill: fill || theme.colors.blue }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function DirectoryIcon({ fill, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M14 12.286V5.703a.673.673 0 0 0-.195-.5.644.644 0 0 0-.49-.203H6.704a.686.686 0 0 1-.5-.214.707.707 0 0 1-.203-.51v-.57c0-.2-.07-.363-.207-.502A.679.679 0 0 0 5.29 3H2.707a.672.672 0 0 0-.5.204.683.683 0 0 0-.206.5v8.582c0 .2.07.367.206.506.137.14.304.208.5.208h10.61a.66.66 0 0 0 .49-.208.685.685 0 0 0 .194-.506H14zm1-6.598v6.65c0 .458-.152.83-.475 1.16-.324.326-.7.502-1.15.502H2.647c-.452 0-.84-.175-1.162-.503a1.572 1.572 0 0 1-.486-1.158V3.654a1.6 1.6 0 0 1 .486-1.17A1.578 1.578 0 0 1 2.648 2h2.7c.45 0 .84.157 1.164.485.324.328.488.714.488 1.17V4h6.373c.452 0 .83.174 1.152.5.323.33.475.73.475 1.187v.001z"
+          style={{ fill: fill || theme.colors.orange }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function FileIcon({ fill, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M14 15H2V1l7.997.02c1 .034 1.759.758 2.428 1.42.667.663 1.561 1.605 1.574 2.555H14V15zM9 2H3v12h10V6H9V2zm3 10H4v-1h8v1zm0-2H4V9h8v1zm-1.988-5h3.008c-.012-.674-.714-1.443-1.204-1.937-.488-.495-1.039-1.058-1.816-1.055v2.96l.012.032z"
+          style={{ fill: fill || theme.colors.blue }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function LibraryIcon({ fill, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M1 13h4V3H1v10zm3-1H2v-2h2v2zM2 4h2v4H2V4zm4 9h4V3H6v10zm3-1H7v-2h2v2zM7 4h2v4H7V4zm4 9h4V3h-4v10zm3-1h-2v-2h2v2zm-2-8h2v4h-2V4z"
+          style={{ fill: fill || theme.colors.blue }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function PortfolioIcon({ fill, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M14.97 14.97H1.016V1.015H14.97V14.97zm-1-12.955H2.015V13.97H13.97V2.015zm-.973 10.982H9V9h3.997v3.997zM7 12.996H3.004V9H7v3.996zM11.997 10H10v1.997h1.997V10zM6 10H4.004v1.996H6V10zm1-3H3.006V3.006H7V7zm5.985 0H9V3.015h3.985V7zM6 4.006H4.006V6H6V4.006zm5.985.009H10V6h1.985V4.015z"
+          style={{ fill: fill || theme.colors.blue }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function ProjectIcon({ fill, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M14.985 13.988L1 14.005 1.02 5h13.966v8.988h-.001zM1.998 5.995l.006 7.02L14.022 13 14 6.004l-12.002-.01v.001zM3 4.5V4h9.996l.004.5h1l-.005-1.497-11.98.003L2 4.5h1zm1-2v-.504h8.002L12 2.5h1l-.004-1.495H3.003L3 2.5h1z"
+          style={{ fill: fill || theme.colors.blue }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function SubPortfolioIcon({ fill, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M14 7h2v9H7v-2H0V0h14v7zM8 8v7h7V8H8zm3 6H9v-2h2v2zm3 0h-2v-2h2v2zm-1-7V1H1v12h6V7h6zm-7 5H2V8h4v4zm5-1H9V9h2v2zm3 0h-2V9h2v2zM5 9H3v2h2V9zm1-3H2V2h4v4zm6 0H8V2h4v4zM5 3H3v2h2V3zm6 0H9v2h2V3z"
+          style={{ fill: fill || theme.colors.blue }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function SubProjectIcon({ fill, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M8 9V8h6v1h1v1h1v6H6v-6h1V9h1zm7 2H7v4h8v-4zm-1-7v3h-1V5H1v7h4v1H0V4h14zm-1-2v1.5h-1V3H2v.5H1V2h12zm-1-2v1.5h-1V1H3v.5H2V0h10z"
+          style={{ fill: fill || theme.colors.blue }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function UnitTestIcon({ fill, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M14 15H2V1l7.997.02c1.013-.03 1.57.893 2.239 1.555.667.663 1.75 1.47 1.763 2.42H14V15zM9 2H3v12h10V6H9V2zM7 8l-3 2.5L7 13V8zm1 5l3-2.5L8 8v5zm2.012-8h3.008c-.012-.674-.78-1.258-1.27-1.752-.488-.495-.973-1.243-1.75-1.24v2.96l.012.032z"
+          style={{ fill: fill || theme.colors.blue }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/RecommendedIcon.tsx b/server/sonar-ui-common/components/icons/RecommendedIcon.tsx
new file mode 100644 (file)
index 0000000..7be72cf
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function RecommendedIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M15.089 13.199l-1.742-3.736c-0.962 1.401-2.464 2.398-4.203 2.701l1.459 3.128c0.186 0.4 0.764 0.373 0.914-0.040l0.748-2.054 0.154-0.072 2.054 0.748c0.412 0.151 0.804-0.276 0.618-0.675zM8.040 0.384c-3.003 0-5.446 2.443-5.446 5.446s2.443 5.446 5.446 5.446c3.003 0 5.446-2.443 5.446-5.446s-2.443-5.446-5.446-5.446zM10.689 5.429l-0.966 0.941 0.228 1.33c0.070 0.406-0.358 0.711-0.718 0.522l-1.194-0.628-1.194 0.628c-0.363 0.19-0.788-0.118-0.718-0.522l0.228-1.33-0.966-0.941c-0.293-0.286-0.131-0.786 0.274-0.844l1.335-0.194 0.597-1.209c0.181-0.367 0.707-0.368 0.888 0l0.597 1.209 1.335 0.194c0.405 0.059 0.568 0.558 0.274 0.844zM2.732 9.463l-1.742 3.736c-0.187 0.4 0.208 0.825 0.618 0.674l2.054-0.748 0.154 0.072 0.748 2.054c0.15 0.412 0.727 0.441 0.914 0.040l1.459-3.128c-1.739-0.302-3.241-1.3-4.203-2.701z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/RocketIcon.tsx b/server/sonar-ui-common/components/icons/RocketIcon.tsx
new file mode 100644 (file)
index 0000000..c779b9e
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function RocketIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M13.754 2.002C11.41 1.96 8.74 3.184 7.049 5.084A6.345 6.345 0 002.7 6.935a.25.25 0 00.14.426l1.927.276-.238.266a.25.25 0 00.01.344l3.213 3.213a.25.25 0 00.344.01l.266-.239.276 1.928c.014.093.088.162.177.192a.23.23 0 00.072.011.282.282 0 00.193-.08 6.331 6.331 0 001.836-4.332c1.901-1.694 3.136-4.365 3.081-6.704a.251.251 0 00-.244-.244zM11.45 6.318a1.246 1.246 0 01-.884.365c-.32 0-.64-.122-.884-.365a1.252 1.252 0 010-1.768 1.251 1.251 0 011.768 0 1.251 1.251 0 010 1.768zm-8.088 4.135c-.535.535-1.27 2.952-1.351 3.225a.25.25 0 00.311.311c.274-.082 2.69-.816 3.226-1.351a1.547 1.547 0 000-2.185 1.548 1.548 0 00-2.186 0z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/RuleScopeIcon.tsx b/server/sonar-ui-common/components/icons/RuleScopeIcon.tsx
new file mode 100644 (file)
index 0000000..20f597c
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function RuleScopeIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M8 3.071c2.724 0 4.929 2.204 4.929 4.929s-2.204 4.929-4.929 4.929c-2.724 0-4.929-2.204-4.929-4.929s2.204-4.929 4.929-4.929zM8 1.357c-3.669 0-6.643 2.974-6.643 6.643s2.974 6.643 6.643 6.643 6.643-2.974 6.643-6.643-2.974-6.643-6.643-6.643zM8 6.286c0.945 0 1.714 0.769 1.714 1.714s-0.769 1.714-1.714 1.714-1.714-0.769-1.714-1.714 0.769-1.714 1.714-1.714zM8 4.571c-1.893 0-3.429 1.535-3.429 3.429s1.535 3.429 3.429 3.429 3.429-1.535 3.429-3.429-1.535-3.429-3.429-3.429z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/SCMIcon.tsx b/server/sonar-ui-common/components/icons/SCMIcon.tsx
new file mode 100644 (file)
index 0000000..d1738e8
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function SCMIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M12.557 4.545c.241.247.443.743.443 1.098v7.714c0 .355-.28.643-.625.643h-8.75A.634.634 0 0 1 3 13.357V2.643C3 2.288 3.28 2 3.625 2h5.833c.345 0 .827.208 1.068.455l2.031 2.09zM9.667 2.91v2.518h2.448a.86.86 0 0 0-.144-.275L9.934 3.058a.823.823 0 0 0-.267-.147zm2.5 10.232V6.286H9.458a.634.634 0 0 1-.625-.643V2.857h-5v10.286h8.334z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/SearchIcon.tsx b/server/sonar-ui-common/components/icons/SearchIcon.tsx
new file mode 100644 (file)
index 0000000..59e8613
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function SearchIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M10.308 7.077c0-.89-.316-1.65-.949-2.283a3.111 3.111 0 0 0-2.282-.948c-.89 0-1.65.316-2.283.948a3.111 3.111 0 0 0-.948 2.283c0 .89.316 1.65.948 2.282a3.111 3.111 0 0 0 2.283.949c.89 0 1.65-.316 2.282-.949a3.111 3.111 0 0 0 .949-2.282zm3.692 6c0 .25-.091.466-.274.649a.887.887 0 0 1-.65.274.857.857 0 0 1-.648-.274L9.954 11.26c-.86.596-1.82.894-2.877.894a4.989 4.989 0 0 1-1.972-.4 5.076 5.076 0 0 1-1.623-1.082A5.076 5.076 0 0 1 2.4 9.049 4.989 4.989 0 0 1 2 7.077c0-.688.133-1.345.4-1.972a5.076 5.076 0 0 1 1.082-1.623A5.076 5.076 0 0 1 5.105 2.4 4.989 4.989 0 0 1 7.077 2c.687 0 1.345.133 1.972.4a5.076 5.076 0 0 1 1.623 1.082c.454.454.815.995 1.082 1.623.266.627.4 1.284.4 1.972a4.938 4.938 0 0 1-.894 2.877l2.473 2.474a.883.883 0 0 1 .267.649z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/SecurityHotspotIcon.tsx b/server/sonar-ui-common/components/icons/SecurityHotspotIcon.tsx
new file mode 100644 (file)
index 0000000..9127b80
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function SecurityHotspotIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M14.08 3.23a1 1 0 00-.67-.77L8.16 1a1.06 1.06 0 00-.5 0L2.41 2.46a.94.94 0 00-.67.77c-.08.57-.74 5.63 1.14 8.31A9 9 0 007.68 15a.85.85 0 00.23 0 .78.78 0 00.22 0 8.93 8.93 0 004.81-3.46c1.85-2.68 1.21-7.74 1.14-8.31zM12.21 8a6.15 6.15 0 01-.86 2.42A7.92 7.92 0 018 13V8zM8 3v5H3.59a24.29 24.29 0 010-3.82z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/SettingsIcon.tsx b/server/sonar-ui-common/components/icons/SettingsIcon.tsx
new file mode 100644 (file)
index 0000000..30db5fb
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function SettingsIcon({
+  fill = 'currentColor',
+  size = 14,
+  ...iconProps
+}: IconProps) {
+  return (
+    <Icon size={size} viewBox="0 0 14 14" {...iconProps}>
+      <g transform="matrix(0.0364583,0,0,0.0364583,0,-1.16667)">
+        <path
+          d="M256,224C256,206.333 249.75,191.25 237.25,178.75C224.75,166.25 209.667,160 192,160C174.333,160 159.25,166.25 146.75,178.75C134.25,191.25 128,206.333 128,224C128,241.667 134.25,256.75 146.75,269.25C159.25,281.75 174.333,288 192,288C209.667,288 224.75,281.75 237.25,269.25C249.75,256.75 256,241.667 256,224ZM384,196.75L384,252.25C384,254.25 383.333,256.167 382,258C380.667,259.833 379,260.917 377,261.25L330.75,268.25C327.583,277.25 324.333,284.833 321,291C326.833,299.333 335.75,310.833 347.75,325.5C349.417,327.5 350.25,329.583 350.25,331.75C350.25,333.917 349.5,335.833 348,337.5C343.5,343.667 335.25,352.667 323.25,364.5C311.25,376.333 303.417,382.25 299.75,382.25C297.75,382.25 295.583,381.5 293.25,380L258.75,353C251.417,356.833 243.833,360 236,362.5C233.333,385.167 230.917,400.667 228.75,409C227.583,413.667 224.583,416 219.75,416L164.25,416C161.917,416 159.875,415.292 158.125,413.875C156.375,412.458 155.417,410.667 155.25,408.5L148.25,362.5C140.083,359.833 132.583,356.75 125.75,353.25L90.5,380C88.833,381.5 86.75,382.25 84.25,382.25C81.917,382.25 79.833,381.333 78,379.5C57,360.5 43.25,346.5 36.75,337.5C35.583,335.833 35,333.917 35,331.75C35,329.75 35.667,327.833 37,326C39.5,322.5 43.75,316.958 49.75,309.375C55.75,301.792 60.25,295.917 63.25,291.75C58.75,283.417 55.333,275.167 53,267L7.25,260.25C5.083,259.917 3.333,258.875 2,257.125C0.667,255.375 0,253.417 0,251.25L0,195.75C0,193.75 0.667,191.833 2,190C3.333,188.167 4.917,187.083 6.75,186.75L53.25,179.75C55.583,172.083 58.833,164.417 63,156.75C56.333,147.25 47.417,135.75 36.25,122.25C34.583,120.25 33.75,118.25 33.75,116.25C33.75,114.583 34.5,112.667 36,110.5C40.333,104.5 48.542,95.542 60.625,83.625C72.708,71.708 80.583,65.75 84.25,65.75C86.417,65.75 88.583,66.583 90.75,68.25L125.25,95C132.583,91.167 140.167,88 148,85.5C150.667,62.833 153.083,47.333 155.25,39C156.417,34.333 159.417,32 164.25,32L219.75,32C222.083,32 224.125,32.708 225.875,34.125C227.625,35.542 228.583,37.333 228.75,39.5L235.75,85.5C243.917,88.167 251.417,91.25 258.25,94.75L293.75,68C295.25,66.5 297.25,65.75 299.75,65.75C301.917,65.75 304,66.583 306,68.25C327.5,88.083 341.25,102.25 347.25,110.75C348.417,112.083 349,113.917 349,116.25C349,118.25 348.333,120.167 347,122C344.5,125.5 340.25,131.042 334.25,138.625C328.25,146.208 323.75,152.083 320.75,156.25C325.083,164.583 328.5,172.75 331,180.75L376.75,187.75C378.917,188.083 380.667,189.125 382,190.875C383.333,192.625 384,194.583 384,196.75Z"
+          style={{ fill }}
+        />
+      </g>
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/SeverityIcon.tsx b/server/sonar-ui-common/components/icons/SeverityIcon.tsx
new file mode 100644 (file)
index 0000000..8dd7b7e
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IconProps, ThemedIcon } from './Icon';
+
+interface Props extends IconProps {
+  severity: string | null | undefined;
+}
+
+const severityIcons: T.Dict<(props: IconProps) => React.ReactElement> = {
+  blocker: BlockerSeverityIcon,
+  critical: CriticalSeverityIcon,
+  major: MajorSeverityIcon,
+  minor: MinorSeverityIcon,
+  info: InfoSeverityIcon,
+};
+
+export default function SeverityIcon({ severity, ...iconProps }: Props) {
+  if (!severity) {
+    return null;
+  }
+
+  const Icon = severityIcons[severity.toLowerCase()];
+  return Icon ? <Icon {...iconProps} /> : null;
+}
+
+function BlockerSeverityIcon(iconProps: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M8 14c-3.311 0-6-2.689-6-6s2.689-6 6-6 6 2.689 6 6-2.689 6-6 6zM7 9h2V4H7v5zm0 3h2v-2H7v2z"
+          style={{ fill: theme.colors.red, fillRule: 'nonzero' }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function CriticalSeverityIcon(iconProps: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M8 2c3.311 0 6 2.689 6 6s-2.689 6-6 6-6-2.689-6-6 2.689-6 6-6zm1 10V7.414l1.893 1.893c.13.124.282.216.457.261a1.006 1.006 0 0 0 1.176-.591 1.01 1.01 0 0 0 .01-.729 1.052 1.052 0 0 0-.229-.355c-1.212-1.212-2.394-2.456-3.638-3.636a1.073 1.073 0 0 0-.169-.123 1.05 1.05 0 0 0-.448-.133h-.104a1.053 1.053 0 0 0-.493.16 1.212 1.212 0 0 0-.162.132C6.08 5.505 4.836 6.687 3.656 7.932a.994.994 0 0 0-.051 1.275c.208.271.548.42.888.389.198-.019.378-.098.535-.218.041-.035.04-.034.079-.071L7 7.414V12h2z"
+          style={{ fill: theme.colors.red, fillRule: 'nonzero' }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function MajorSeverityIcon(iconProps: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M8 2c3.311 0 6 2.689 6 6s-2.689 6-6 6-6-2.689-6-6 2.689-6 6-6zm.08 2.903c.071.008.14.019.208.039.138.042.26.114.37.205 1.244 1.146 2.426 2.357 3.639 3.536.1.103.181.218.234.352a1.01 1.01 0 0 1 .001.728 1.002 1.002 0 0 1-1.169.609 1.042 1.042 0 0 1-.46-.255L8 7.295l-2.903 2.822c-.039.036-.039.036-.08.07a1.002 1.002 0 0 1-1.604-.947c.032-.196.122-.37.253-.519C4.847 7.51 6.09 6.362 7.303 5.183c.052-.048.106-.093.167-.131a1.041 1.041 0 0 1 .61-.149z"
+          style={{ fill: theme.colors.red }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function MinorSeverityIcon(iconProps: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M8 2c3.311 0 6 2.689 6 6s-2.689 6-6 6-6-2.689-6-6 2.689-6 6-6zm1 6.586V4H7v4.586L5.107 6.693a1.178 1.178 0 0 0-.165-.134 1.041 1.041 0 0 0-.662-.152 1 1 0 0 0-.587 1.7c1.212 1.212 2.394 2.456 3.638 3.636.094.08.195.146.311.191a1.008 1.008 0 0 0 1.065-.227c1.213-1.212 2.457-2.394 3.637-3.639a.994.994 0 0 0 .051-1.275 1.012 1.012 0 0 0-.888-.389 1.041 1.041 0 0 0-.535.218c-.04.034-.04.034-.079.071L9 8.586z"
+          style={{ fill: theme.colors.lightGreen }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function InfoSeverityIcon(iconProps: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M8 2c3.311 0 6 2.689 6 6s-2.689 6-6 6-6-2.689-6-6 2.689-6 6-6zm1 5H7v5h2V7zm0-3H7v2h2V4z"
+          style={{ fill: theme.colors.blue }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/ShortLivingBranchIcon.tsx b/server/sonar-ui-common/components/icons/ShortLivingBranchIcon.tsx
new file mode 100644 (file)
index 0000000..26d55d5
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IconProps, ThemedIcon } from './Icon';
+
+export default function ShortLivingBranchIcon({ fill, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <g transform="translate(3, 0)">
+          <path
+            d="M9.5 6.5c0-1.1-.9-2-2-2s-2 .9-2 2c0 .8.5 1.5 1.2 1.8-.3.6-.7 1.1-1.2 1.4-.9.5-1.9.5-2.5.4V4c.9-.2 1.5-1 1.5-1.9 0-1.1-.9-2-2-2s-2 .9-2 2C.5 3 1.1 3.8 2 4v8c-.9.2-1.5 1-1.5 1.9 0 1.1.9 2 2 2s2-.9 2-2c0-.9-.6-1.7-1.5-1.9v-1c.2 0 .5.1.7.1.7 0 1.5-.1 2.2-.6.8-.5 1.4-1.2 1.7-2.1 1.1 0 1.9-.9 1.9-1.9zm-8-4.4c0-.6.4-1 1-1s1 .4 1 1-.4 1-1 1-1-.5-1-1zm2 11.9c0 .6-.4 1-1 1s-1-.4-1-1 .4-1 1-1 1 .4 1 1zm4-6.5c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1z"
+            style={{ fill: fill || theme.colors.blue }}
+          />
+        </g>
+      )}
+    </ThemedIcon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/SortAscIcon.tsx b/server/sonar-ui-common/components/icons/SortAscIcon.tsx
new file mode 100644 (file)
index 0000000..a0921ba
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function SortAscIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M6.571 12.857q0 0.107-0.089 0.214l-2.848 2.848q-0.089 0.080-0.205 0.080-0.107 0-0.205-0.080l-2.857-2.857q-0.134-0.143-0.063-0.313 0.071-0.179 0.268-0.179h1.714v-12.286q0-0.125 0.080-0.205t0.205-0.080h1.714q0.125 0 0.205 0.080t0.080 0.205v12.286h1.714q0.125 0 0.205 0.080t0.080 0.205zM16 14v1.714q0 0.125-0.080 0.205t-0.205 0.080h-7.429q-0.125 0-0.205-0.080t-0.080-0.205v-1.714q0-0.125 0.080-0.205t0.205-0.080h7.429q0.125 0 0.205 0.080t0.080 0.205zM14.286 9.429v1.714q0 0.125-0.080 0.205t-0.205 0.080h-5.714q-0.125 0-0.205-0.080t-0.080-0.205v-1.714q0-0.125 0.080-0.205t0.205-0.080h5.714q0.125 0 0.205 0.080t0.080 0.205zM12.571 4.857v1.714q0 0.125-0.080 0.205t-0.205 0.080h-4q-0.125 0-0.205-0.080t-0.080-0.205v-1.714q0-0.125 0.080-0.205t0.205-0.080h4q0.125 0 0.205 0.080t0.080 0.205zM10.857 0.286v1.714q0 0.125-0.080 0.205t-0.205 0.080h-2.286q-0.125 0-0.205-0.080t-0.080-0.205v-1.714q0-0.125 0.080-0.205t0.205-0.080h2.286q0.125 0 0.205 0.080t0.080 0.205z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/SortDescIcon.tsx b/server/sonar-ui-common/components/icons/SortDescIcon.tsx
new file mode 100644 (file)
index 0000000..f57c8de
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function SortDescIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M10.857 14v1.714q0 0.125-0.080 0.205t-0.205 0.080h-2.286q-0.125 0-0.205-0.080t-0.080-0.205v-1.714q0-0.125 0.080-0.205t0.205-0.080h2.286q0.125 0 0.205 0.080t0.080 0.205zM6.571 12.857q0 0.107-0.089 0.214l-2.848 2.848q-0.089 0.080-0.205 0.080-0.107 0-0.205-0.080l-2.857-2.857q-0.134-0.143-0.063-0.313 0.071-0.179 0.268-0.179h1.714v-12.286q0-0.125 0.080-0.205t0.205-0.080h1.714q0.125 0 0.205 0.080t0.080 0.205v12.286h1.714q0.125 0 0.205 0.080t0.080 0.205zM12.571 9.429v1.714q0 0.125-0.080 0.205t-0.205 0.080h-4q-0.125 0-0.205-0.080t-0.080-0.205v-1.714q0-0.125 0.080-0.205t0.205-0.080h4q0.125 0 0.205 0.080t0.080 0.205zM14.286 4.857v1.714q0 0.125-0.080 0.205t-0.205 0.080h-5.714q-0.125 0-0.205-0.080t-0.080-0.205v-1.714q0-0.125 0.080-0.205t0.205-0.080h5.714q0.125 0 0.205 0.080t0.080 0.205zM16 0.286v1.714q0 0.125-0.080 0.205t-0.205 0.080h-7.429q-0.125 0-0.205-0.080t-0.080-0.205v-1.714q0-0.125 0.080-0.205t0.205-0.080h7.429q0.125 0 0.205 0.080t0.080 0.205z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/StatusIcon.tsx b/server/sonar-ui-common/components/icons/StatusIcon.tsx
new file mode 100644 (file)
index 0000000..e517fd7
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IconProps, ThemedIcon } from './Icon';
+
+interface Props extends IconProps {
+  status: string;
+}
+
+const statusIcons: T.Dict<(props: IconProps) => React.ReactElement> = {
+  open: OpenStatusIcon,
+  confirmed: ConfirmedStatusIcon,
+  reopened: ReopenedStatusIcon,
+  resolved: ResolvedStatusIcon,
+  closed: ClosedStatusIcon,
+  to_review: OpenStatusIcon,
+  in_review: ConfirmedStatusIcon,
+  reviewed: ResolvedStatusIcon,
+};
+
+export default function StatusIcon({ status, ...iconProps }: Props) {
+  const Icon = statusIcons[status.toLowerCase()];
+  return Icon ? <Icon {...iconProps} /> : null;
+}
+
+function OpenStatusIcon(iconProps: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M8 3.75c-.77 0-1.482.19-2.133.57A4.25 4.25 0 0 0 4.32 5.867c-.38.65-.57 1.362-.57 2.133 0 .77.19 1.482.57 2.133.38.65.896 1.167 1.547 1.547.65.38 1.362.57 2.133.57.77 0 1.482-.19 2.133-.57a4.242 4.242 0 0 0 1.547-1.547c.38-.65.57-1.362.57-2.133 0-.77-.19-1.482-.57-2.133a4.25 4.25 0 0 0-1.547-1.547A4.153 4.153 0 0 0 8 3.75zM14 8c0 1.09-.268 2.092-.805 3.012a5.96 5.96 0 0 1-2.183 2.183A5.863 5.863 0 0 1 8 14a5.863 5.863 0 0 1-3.012-.805 5.96 5.96 0 0 1-2.183-2.183A5.863 5.863 0 0 1 2 8c0-1.09.268-2.092.805-3.012a5.96 5.96 0 0 1 2.183-2.183A5.863 5.863 0 0 1 8 2c1.09 0 2.092.268 3.012.805a5.96 5.96 0 0 1 2.183 2.183C13.732 5.908 14 6.91 14 8z"
+          style={{ fill: theme.colors.blue }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function ConfirmedStatusIcon(iconProps: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M10 8c0 .552-.195 1.023-.586 1.414-.39.39-.862.586-1.414.586a1.926 1.926 0 0 1-1.414-.586A1.928 1.928 0 0 1 6 8c0-.552.195-1.023.586-1.414C6.976 6.196 7.448 6 8 6c.552 0 1.023.195 1.414.586.39.39.586.862.586 1.414zM8 3.75c-.77 0-1.482.19-2.133.57A4.25 4.25 0 0 0 4.32 5.867c-.38.65-.57 1.362-.57 2.133 0 .77.19 1.482.57 2.133.38.65.896 1.167 1.547 1.547.65.38 1.362.57 2.133.57.77 0 1.482-.19 2.133-.57a4.242 4.242 0 0 0 1.547-1.547c.38-.65.57-1.362.57-2.133 0-.77-.19-1.482-.57-2.133a4.25 4.25 0 0 0-1.547-1.547A4.153 4.153 0 0 0 8 3.75zM14 8c0 1.09-.268 2.092-.805 3.012a5.96 5.96 0 0 1-2.183 2.183A5.863 5.863 0 0 1 8 14a5.863 5.863 0 0 1-3.012-.805 5.96 5.96 0 0 1-2.183-2.183A5.863 5.863 0 0 1 2 8c0-1.09.268-2.092.805-3.012a5.96 5.96 0 0 1 2.183-2.183A5.863 5.863 0 0 1 8 2c1.09 0 2.092.268 3.012.805a5.96 5.96 0 0 1 2.183 2.183C13.732 5.908 14 6.91 14 8z"
+          style={{ fill: theme.colors.blue }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function ReopenedStatusIcon(iconProps: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M8 12.25v-8.5c-.77 0-1.482.19-2.133.57A4.25 4.25 0 0 0 4.32 5.867c-.38.65-.57 1.362-.57 2.133 0 .77.19 1.482.57 2.133.38.65.896 1.167 1.547 1.547.65.38 1.362.57 2.133.57zM14 8c0 1.09-.268 2.092-.805 3.012a5.96 5.96 0 0 1-2.183 2.183A5.863 5.863 0 0 1 8 14a5.863 5.863 0 0 1-3.012-.805 5.96 5.96 0 0 1-2.183-2.183A5.863 5.863 0 0 1 2 8c0-1.09.268-2.092.805-3.012a5.96 5.96 0 0 1 2.183-2.183A5.863 5.863 0 0 1 8 2c1.09 0 2.092.268 3.012.805a5.96 5.96 0 0 1 2.183 2.183C13.732 5.908 14 6.91 14 8z"
+          style={{ fill: theme.colors.blue }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function ResolvedStatusIcon(iconProps: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M12.03 6.734a.49.49 0 0 0-.14-.36l-.71-.702a.48.48 0 0 0-.352-.15.474.474 0 0 0-.35.15l-3.19 3.18-1.765-1.766a.479.479 0 0 0-.35-.15.479.479 0 0 0-.353.15l-.71.703a.482.482 0 0 0-.14.358c0 .14.046.258.14.352l2.828 2.828c.098.1.216.15.35.15.142 0 .26-.05.36-.15l4.243-4.242a.475.475 0 0 0 .14-.352l-.001.001zM14 8c0 1.09-.268 2.092-.805 3.012a5.96 5.96 0 0 1-2.183 2.183A5.863 5.863 0 0 1 8 14a5.863 5.863 0 0 1-3.012-.805 5.96 5.96 0 0 1-2.183-2.183A5.863 5.863 0 0 1 2 8c0-1.09.268-2.092.805-3.012a5.96 5.96 0 0 1 2.183-2.183A5.863 5.863 0 0 1 8 2c1.09 0 2.092.268 3.012.805a5.96 5.96 0 0 1 2.183 2.183C13.732 5.908 14 6.91 14 8z"
+          style={{ fill: theme.colors.baseFontColor }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function ClosedStatusIcon(iconProps: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M14 8c0 1.09-.268 2.092-.805 3.012a5.96 5.96 0 0 1-2.183 2.183A5.863 5.863 0 0 1 8 14a5.863 5.863 0 0 1-3.012-.805 5.96 5.96 0 0 1-2.183-2.183A5.863 5.863 0 0 1 2 8c0-1.09.268-2.092.805-3.012a5.96 5.96 0 0 1 2.183-2.183A5.863 5.863 0 0 1 8 2c1.09 0 2.092.268 3.012.805a5.96 5.96 0 0 1 2.183 2.183C13.732 5.908 14 6.91 14 8z"
+          style={{ fill: theme.colors.baseFontColor }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/TagsIcon.tsx b/server/sonar-ui-common/components/icons/TagsIcon.tsx
new file mode 100644 (file)
index 0000000..0239da5
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function TagsIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M4.303 5.36a.94.94 0 0 0-.944-.945.94.94 0 0 0-.944.944c0 .524.42.944.944.944a.94.94 0 0 0 .944-.944zm7.866 4.246a.95.95 0 0 1-.273.663l-3.62 3.627a.95.95 0 0 1-1.334 0L1.671 8.618C1.295 8.249 1 7.534 1 7.01V3.944A.95.95 0 0 1 1.944 3H5.01c.523 0 1.238.295 1.614.67l5.271 5.265a.98.98 0 0 1 .273.67zm2.831 0a.95.95 0 0 1-.273.663l-3.62 3.627a.98.98 0 0 1-.67.273c-.384 0-.575-.177-.826-.435l3.465-3.465a.95.95 0 0 0 0-1.334L7.805 3.67C7.429 3.295 6.714 3 6.19 3h1.651c.524 0 1.239.295 1.615.67l5.271 5.265a.98.98 0 0 1 .273.67z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/TestStatusIcon.tsx b/server/sonar-ui-common/components/icons/TestStatusIcon.tsx
new file mode 100644 (file)
index 0000000..80c4651
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IconProps, ThemedIcon } from './Icon';
+
+interface Props extends IconProps {
+  status: string;
+}
+
+const statusIcons: T.Dict<(props: IconProps) => React.ReactElement> = {
+  ok: OkTestStatusIcon,
+  failure: FailureTestStatusIcon,
+  error: ErrorTestStatusIcon,
+  skipped: SkippedTestStatusIcon,
+};
+
+export default function TestStatusIcon({ status, ...iconProps }: Props) {
+  const Icon = statusIcons[status.toLowerCase()];
+  return Icon ? <Icon {...iconProps} /> : null;
+}
+
+function OkTestStatusIcon(iconProps: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M12.03 6.734a.49.49 0 0 0-.14-.36l-.71-.702a.48.48 0 0 0-.352-.15.474.474 0 0 0-.35.15l-3.19 3.18-1.765-1.766a.479.479 0 0 0-.35-.15.479.479 0 0 0-.353.15l-.71.703a.482.482 0 0 0-.14.358c0 .14.046.258.14.352l2.828 2.828c.098.1.216.15.35.15.142 0 .26-.05.36-.15l4.243-4.242a.475.475 0 0 0 .14-.352l-.001.001zM14 8c0 1.09-.268 2.092-.805 3.012a5.96 5.96 0 0 1-2.183 2.183A5.863 5.863 0 0 1 8 14a5.863 5.863 0 0 1-3.012-.805 5.96 5.96 0 0 1-2.183-2.183A5.863 5.863 0 0 1 2 8c0-1.09.268-2.092.805-3.012a5.96 5.96 0 0 1 2.183-2.183A5.863 5.863 0 0 1 8 2c1.09 0 2.092.268 3.012.805a5.96 5.96 0 0 1 2.183 2.183C13.732 5.908 14 6.91 14 8z"
+          style={{ fill: theme.colors.green }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function FailureTestStatusIcon(iconProps: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M8 14c-3.311 0-6-2.689-6-6s2.689-6 6-6 6 2.689 6 6-2.689 6-6 6zM7 9h2V4H7v5zm0 3h2v-2H7v2z"
+          style={{ fill: theme.colors.orange, fillRule: 'nonzero' }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function ErrorTestStatusIcon(iconProps: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M10.977 9.766a.497.497 0 0 0-.149-.352L9.414 8l1.414-1.414a.497.497 0 0 0 0-.711l-.703-.703a.497.497 0 0 0-.71 0L8 6.586 6.586 5.172a.497.497 0 0 0-.711 0l-.703.703a.497.497 0 0 0 0 .71L6.586 8 5.172 9.414a.497.497 0 0 0 0 .711l.703.703a.497.497 0 0 0 .71 0L8 9.414l1.414 1.414a.497.497 0 0 0 .711 0l.703-.703a.515.515 0 0 0 .149-.36zM14 8c0 3.313-2.688 6-6 6-3.313 0-6-2.688-6-6 0-3.313 2.688-6 6-6 3.313 0 6 2.688 6 6z"
+          style={{ fill: theme.colors.red, fillRule: 'nonzero' }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
+
+function SkippedTestStatusIcon(iconProps: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M11.5 8.5v-1c0-.273-.227-.5-.5-.5H5c-.273 0-.5.227-.5.5v1c0 .273.227.5.5.5h6c.273 0 .5-.227.5-.5zM14 8c0 3.313-2.688 6-6 6-3.313 0-6-2.688-6-6 0-3.313 2.688-6 6-6 3.313 0 6 2.688 6 6z"
+          style={{ fill: theme.colors.gray71, fillRule: 'nonzero' }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/TreeIcon.tsx b/server/sonar-ui-common/components/icons/TreeIcon.tsx
new file mode 100644 (file)
index 0000000..cb3359c
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function TreeIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M15.045 2.467c0-0.277-0.225-0.503-0.503-0.503h-13.084c-0.277 0-0.503 0.225-0.503 0.503v1.007c0 0.277 0.225 0.503 0.503 0.503h13.084c0.277 0 0.503-0.225 0.503-0.503v-1.007zM15.045 5.487c0-0.277-0.194-0.503-0.432-0.503h-11.216c-0.238 0-0.432 0.225-0.432 0.503v1.007c0 0.277 0.193 0.503 0.432 0.503h11.216c0.238 0 0.432-0.225 0.432-0.503v-1.007zM15.045 8.506c0-0.277-0.161-0.503-0.359-0.503h-9.346c-0.198 0-0.359 0.225-0.359 0.503v1.007c0 0.277 0.161 0.503 0.359 0.503h9.346c0.198 0 0.359-0.225 0.359-0.503v-1.007zM15.045 11.527c0-0.277-0.129-0.503-0.287-0.503h-7.477c-0.159 0-0.288 0.225-0.288 0.503v1.007c0 0.277 0.129 0.503 0.288 0.503h7.477c0.159 0 0.287-0.225 0.287-0.503v-1.007z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/TreemapIcon.tsx b/server/sonar-ui-common/components/icons/TreemapIcon.tsx
new file mode 100644 (file)
index 0000000..0384abd
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function TreemapIcon({ fill = 'currentColor', size = 14, ...iconProps }: IconProps) {
+  return (
+    <Icon size={size} {...iconProps}>
+      <path
+        d="M0 0h8v16h-8zM9.143 0h6.857v9.143h-6.857zM9.143 10.286h6.857v5.714h-6.857z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/VisibleIcon.tsx b/server/sonar-ui-common/components/icons/VisibleIcon.tsx
new file mode 100644 (file)
index 0000000..25e21b7
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function VisibleIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M13.524 8.403q-1.093-1.697-2.74-2.539 0.439 0.748 0.439 1.618 0 1.331-0.946 2.276t-2.276 0.946-2.276-0.946-0.946-2.276q0-0.87 0.439-1.618-1.647 0.842-2.74 2.539 0.957 1.474 2.399 2.348t3.125 0.874 3.125-0.874 2.399-2.348zM8.345 5.641q0-0.144-0.101-0.245t-0.245-0.101q-0.899 0-1.543 0.644t-0.644 1.543q0 0.144 0.101 0.245t0.245 0.101 0.245-0.101 0.101-0.245q0-0.619 0.439-1.057t1.057-0.439q0.144 0 0.245-0.101t0.101-0.245zM14.444 8.403q0 0.245-0.144 0.496-1.007 1.654-2.708 2.65t-3.593 0.996-3.593-1-2.708-2.647q-0.144-0.252-0.144-0.496t0.144-0.496q1.007-1.647 2.708-2.647t3.593-1 3.593 1 2.708 2.647q0.144 0.252 0.144 0.496z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/VulnerabilityIcon.tsx b/server/sonar-ui-common/components/icons/VulnerabilityIcon.tsx
new file mode 100644 (file)
index 0000000..e31eeab
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function VulnerabilityIcon({ fill = 'currentColor', ...iconProps }: IconProps) {
+  return (
+    <Icon {...iconProps}>
+      <path
+        d="M12,7.05H6V5a2,2,0,1,1,4,0,1,1,0,0,0,2,0A4,4,0,1,0,4,5V7.06A1.12,1.12,0,0,0,3,8.17V14a1.12,1.12,0,0,0,1.12,1.12H12A1.12,1.12,0,0,0,13.1,14V8.17A1.12,1.12,0,0,0,12,7.05ZM8,13a2,2,0,1,1,2-2A2,2,0,0,1,8,13Z"
+        style={{ fill }}
+      />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/WarningIcon.tsx b/server/sonar-ui-common/components/icons/WarningIcon.tsx
new file mode 100644 (file)
index 0000000..1c9ed02
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IconProps, ThemedIcon } from './Icon';
+
+export default function WarningIcon({ fill, ...iconProps }: IconProps) {
+  return (
+    <ThemedIcon {...iconProps}>
+      {({ theme }) => (
+        <path
+          d="M9 12.242v-1.484c0-.14-.11-.258-.25-.258h-1.5c-.14 0-.25.117-.25.258v1.484c0 .14.11.258.25.258h1.5c.14 0 .25-.117.25-.258zM8.984 9.32l.141-3.586a.189.189 0 0 0-.078-.148C9 5.546 8.93 5.5 8.859 5.5H7.141c-.07 0-.141.047-.188.086-.055.039-.078.117-.078.164l.133 3.57c0 .102.117.18.265.18H8.72c.14 0 .258-.078.265-.18zm-.109-7.297l6 11A1 1 0 0 1 14 14.5H2a1 1 0 0 1-.875-1.477l6-11a.994.994 0 0 1 1.75 0z"
+          style={{ fill: fill || theme.colors.warningIconColor }}
+        />
+      )}
+    </ThemedIcon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/__tests__/Icon-test.tsx b/server/sonar-ui-common/components/icons/__tests__/Icon-test.tsx
new file mode 100644 (file)
index 0000000..3402e9b
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import Icon, { IconProps } from '../Icon';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<IconProps> = {}) {
+  return shallow(
+    <Icon {...props}>
+      <path d="test-path" />
+    </Icon>
+  );
+}
diff --git a/server/sonar-ui-common/components/icons/__tests__/IssueIcon-test.tsx b/server/sonar-ui-common/components/icons/__tests__/IssueIcon-test.tsx
new file mode 100644 (file)
index 0000000..2a78555
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import IssueIcon from '../IssueIcon';
+
+it('should render correctly', () => {
+  expect(shallowRender('BUG')).toMatchSnapshot();
+  expect(shallowRender('VULNERABILITY')).toMatchSnapshot();
+  expect(shallowRender('CODE_SMELL')).toMatchSnapshot();
+  expect(shallowRender('SECURITY_HOTSPOT')).toMatchSnapshot();
+});
+
+function shallowRender(type: T.IssueType) {
+  return shallow(<IssueIcon type={type} />);
+}
diff --git a/server/sonar-ui-common/components/icons/__tests__/IssueTypeIcon-test.tsx b/server/sonar-ui-common/components/icons/__tests__/IssueTypeIcon-test.tsx
new file mode 100644 (file)
index 0000000..605d5bd
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import IssueTypeIcon, { Props } from '../IssueTypeIcon';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+  expect(shallowRender({ className: 'my-class', query: 'security_hotspots' })).toMatchSnapshot();
+  expect(shallowRender({ query: 'new_code_smells' })).toMatchSnapshot();
+  expect(shallowRender({ query: 'vulnerability' })).toMatchSnapshot();
+  expect(shallowRender({ query: 'unknown' }).type()).toBeNull();
+});
+
+function shallowRender(props: Partial<Props> = {}) {
+  return shallow(<IssueTypeIcon query="bugs" {...props} />);
+}
diff --git a/server/sonar-ui-common/components/icons/__tests__/TestStatusIcon-test.tsx b/server/sonar-ui-common/components/icons/__tests__/TestStatusIcon-test.tsx
new file mode 100644 (file)
index 0000000..e25a9f3
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import TestStatusIcon from '../TestStatusIcon';
+
+it('should render correctly', () => {
+  expect(shallowRender('OK')).toMatchSnapshot();
+  expect(shallowRender('failure')).toMatchSnapshot();
+  expect(shallowRender('skipped')).toMatchSnapshot();
+  expect(shallowRender('Error')).toMatchSnapshot();
+});
+
+function shallowRender(status: string) {
+  return shallow(<TestStatusIcon status={status} />);
+}
diff --git a/server/sonar-ui-common/components/icons/__tests__/__snapshots__/Icon-test.tsx.snap b/server/sonar-ui-common/components/icons/__tests__/__snapshots__/Icon-test.tsx.snap
new file mode 100644 (file)
index 0000000..a61c031
--- /dev/null
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<svg
+  height={16}
+  style={
+    Object {
+      "clipRule": "evenodd",
+      "fillRule": "evenodd",
+      "strokeLinejoin": "round",
+      "strokeMiterlimit": 1.41421,
+    }
+  }
+  version="1.1"
+  viewBox="0 0 16 16"
+  width={16}
+  xmlSpace="preserve"
+  xmlnsXlink="http://www.w3.org/1999/xlink"
+>
+  <path
+    d="test-path"
+  />
+</svg>
+`;
diff --git a/server/sonar-ui-common/components/icons/__tests__/__snapshots__/IssueIcon-test.tsx.snap b/server/sonar-ui-common/components/icons/__tests__/__snapshots__/IssueIcon-test.tsx.snap
new file mode 100644 (file)
index 0000000..f3f5fcf
--- /dev/null
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `<BugIcon />`;
+
+exports[`should render correctly 2`] = `<VulnerabilityIcon />`;
+
+exports[`should render correctly 3`] = `<CodeSmellIcon />`;
+
+exports[`should render correctly 4`] = `<SecurityHotspotIcon />`;
diff --git a/server/sonar-ui-common/components/icons/__tests__/__snapshots__/IssueTypeIcon-test.tsx.snap b/server/sonar-ui-common/components/icons/__tests__/__snapshots__/IssueTypeIcon-test.tsx.snap
new file mode 100644 (file)
index 0000000..4373bb0
--- /dev/null
@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<IssueIcon
+  type="BUG"
+/>
+`;
+
+exports[`should render correctly 2`] = `
+<IssueIcon
+  className="my-class"
+  type="SECURITY_HOTSPOT"
+/>
+`;
+
+exports[`should render correctly 3`] = `
+<IssueIcon
+  type="CODE_SMELL"
+/>
+`;
+
+exports[`should render correctly 4`] = `
+<IssueIcon
+  type="VULNERABILITY"
+/>
+`;
diff --git a/server/sonar-ui-common/components/icons/__tests__/__snapshots__/TestStatusIcon-test.tsx.snap b/server/sonar-ui-common/components/icons/__tests__/__snapshots__/TestStatusIcon-test.tsx.snap
new file mode 100644 (file)
index 0000000..12b493c
--- /dev/null
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `<OkTestStatusIcon />`;
+
+exports[`should render correctly 2`] = `<FailureTestStatusIcon />`;
+
+exports[`should render correctly 3`] = `<SkippedTestStatusIcon />`;
+
+exports[`should render correctly 4`] = `<ErrorTestStatusIcon />`;
diff --git a/server/sonar-ui-common/components/intl/DateFormatter.tsx b/server/sonar-ui-common/components/intl/DateFormatter.tsx
new file mode 100644 (file)
index 0000000..4aae2a7
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { DateSource, FormattedDate } from 'react-intl';
+import { parseDate } from '../../helpers/dates';
+
+export interface DateFormatterProps {
+  children?: (formattedDate: string) => React.ReactNode;
+  date: DateSource;
+  long?: boolean;
+}
+
+export const formatterOption = { year: 'numeric', month: 'short', day: '2-digit' };
+
+export const longFormatterOption = { year: 'numeric', month: 'long', day: 'numeric' };
+
+export default function DateFormatter({ children, date, long }: DateFormatterProps) {
+  return (
+    <FormattedDate value={parseDate(date)} {...(long ? longFormatterOption : formatterOption)}>
+      {children}
+    </FormattedDate>
+  );
+}
diff --git a/server/sonar-ui-common/components/intl/DateFromNow.tsx b/server/sonar-ui-common/components/intl/DateFromNow.tsx
new file mode 100644 (file)
index 0000000..a800cac
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { differenceInHours } from 'date-fns';
+import * as React from 'react';
+import { DateSource, FormattedRelative } from 'react-intl';
+import { parseDate } from '../../helpers/dates';
+import { translate } from '../../helpers/l10n';
+import DateTimeFormatter from './DateTimeFormatter';
+
+export interface DateFromNowProps {
+  children?: (formattedDate: string) => React.ReactNode;
+  date?: DateSource;
+  hourPrecision?: boolean;
+}
+
+export default function DateFromNow(props: DateFromNowProps) {
+  const { children: originalChildren = (f: string) => f, date, hourPrecision } = props;
+  let children = originalChildren;
+
+  if (!date) {
+    /*
+     * We return a JSX.Element to bypass typescript issue with functional components return type
+     * (https://github.com/DefinitelyTyped/DefinitelyTyped/issues/20544)
+     */
+    // eslint-disable-next-line react/jsx-no-useless-fragment
+    return <>{originalChildren(translate('never'))}</>;
+  }
+
+  if (date && hourPrecision && differenceInHours(Date.now(), date) < 1) {
+    children = () => originalChildren(translate('less_than_1_hour_ago'));
+  }
+
+  const parsedDate = parseDate(date);
+
+  return (
+    <DateTimeFormatter date={parsedDate}>
+      {(formattedDate) => (
+        <span title={formattedDate}>
+          <FormattedRelative value={parsedDate}>{children}</FormattedRelative>
+        </span>
+      )}
+    </DateTimeFormatter>
+  );
+}
diff --git a/server/sonar-ui-common/components/intl/DateTimeFormatter.tsx b/server/sonar-ui-common/components/intl/DateTimeFormatter.tsx
new file mode 100644 (file)
index 0000000..c5d31fd
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { DateSource, FormattedDate } from 'react-intl';
+import { parseDate } from '../../helpers/dates';
+
+interface Props {
+  children?: (formattedDate: string) => React.ReactNode;
+  date: DateSource;
+}
+
+export const formatterOption = {
+  year: 'numeric',
+  month: 'long',
+  day: 'numeric',
+  hour: 'numeric',
+  minute: 'numeric',
+};
+
+export default function DateTimeFormatter({ children, date }: Props) {
+  return (
+    <FormattedDate value={parseDate(date)} {...formatterOption}>
+      {children}
+    </FormattedDate>
+  );
+}
diff --git a/server/sonar-ui-common/components/intl/TimeFormatter.tsx b/server/sonar-ui-common/components/intl/TimeFormatter.tsx
new file mode 100644 (file)
index 0000000..3eda133
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { DateSource, FormattedTime } from 'react-intl';
+import { parseDate } from '../../helpers/dates';
+
+export interface TimeFormatterProps {
+  children?: (formattedDate: string) => React.ReactNode;
+  date: DateSource;
+  long?: boolean;
+}
+
+export const formatterOption = { hour: 'numeric', minute: 'numeric' };
+
+export const longFormatterOption = { hour: 'numeric', minute: 'numeric', second: 'numeric' };
+
+export default function TimeFormatter({ children, date, long }: TimeFormatterProps) {
+  return (
+    <FormattedTime value={parseDate(date)} {...(long ? longFormatterOption : formatterOption)}>
+      {children}
+    </FormattedTime>
+  );
+}
diff --git a/server/sonar-ui-common/components/intl/__mocks__/DateFromNow.tsx b/server/sonar-ui-common/components/intl/__mocks__/DateFromNow.tsx
new file mode 100644 (file)
index 0000000..1835010
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { DateSource } from 'react-intl';
+
+interface Props {
+  children?: (formattedDate: string) => React.ReactNode;
+  date: DateSource;
+}
+
+export default function DateFromNow({ children, date }: Props) {
+  return children && children(date.toString());
+}
diff --git a/server/sonar-ui-common/components/intl/__tests__/DateFormatter-test.tsx b/server/sonar-ui-common/components/intl/__tests__/DateFormatter-test.tsx
new file mode 100644 (file)
index 0000000..18906fb
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import DateFormatter, { DateFormatterProps } from '../DateFormatter';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('standard');
+  expect(shallowRender({ long: true })).toMatchSnapshot('long');
+});
+
+function shallowRender(overrides: Partial<DateFormatterProps> = {}) {
+  return shallow(
+    <DateFormatter date={new Date('2020-02-20T20:20:20Z')} {...overrides}>
+      {(formatted) => <span>{formatted}</span>}
+    </DateFormatter>
+  );
+}
diff --git a/server/sonar-ui-common/components/intl/__tests__/DateFromNow-test.tsx b/server/sonar-ui-common/components/intl/__tests__/DateFromNow-test.tsx
new file mode 100644 (file)
index 0000000..9edfbd6
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { FormattedRelative, IntlProvider } from 'react-intl';
+import DateFromNow, { DateFromNowProps } from '../DateFromNow';
+import DateTimeFormatter from '../DateTimeFormatter';
+
+const date = '2020-02-20T20:20:20Z';
+
+it('should render correctly', () => {
+  const wrapper = shallowRender();
+
+  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.find(DateTimeFormatter).props().children(date)).toMatchSnapshot('children');
+});
+
+it('should render correctly when there is no date', () => {
+  const children = jest.fn();
+
+  shallowRender({ date: undefined }, children);
+
+  expect(children).toHaveBeenCalledWith('never');
+});
+
+it('should render correctly when the date is less than one hour in the past', () => {
+  const veryCloseDate = new Date(date);
+  veryCloseDate.setMinutes(veryCloseDate.getMinutes() - 10);
+  jest.spyOn(Date, 'now').mockImplementation(() => (new Date(date) as unknown) as number);
+  const children = jest.fn();
+
+  shallowRender({ date: veryCloseDate, hourPrecision: true }, children)
+    .dive()
+    .dive()
+    .find(FormattedRelative)
+    .props()
+    .children(date);
+
+  expect(children).toHaveBeenCalledWith('less_than_1_hour_ago');
+});
+
+function shallowRender(overrides: Partial<DateFromNowProps> = {}, children: jest.Mock = jest.fn()) {
+  return shallow<DateFromNowProps>(
+    <IntlProvider defaultLocale="en-US" locale="en">
+      <DateFromNow date={date} {...overrides}>
+        {(formattedDate) => children(formattedDate)}
+      </DateFromNow>
+    </IntlProvider>
+  ).dive();
+}
diff --git a/server/sonar-ui-common/components/intl/__tests__/DateTimeFormatter-test.tsx b/server/sonar-ui-common/components/intl/__tests__/DateTimeFormatter-test.tsx
new file mode 100644 (file)
index 0000000..affaf07
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import DateTimeFormatter from '../DateTimeFormatter';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('standard');
+});
+
+function shallowRender() {
+  return shallow(
+    <DateTimeFormatter date={new Date('2020-02-20T20:20:20Z')}>
+      {(formatted) => <span>{formatted}</span>}
+    </DateTimeFormatter>
+  );
+}
diff --git a/server/sonar-ui-common/components/intl/__tests__/TimeFormatter-test.tsx b/server/sonar-ui-common/components/intl/__tests__/TimeFormatter-test.tsx
new file mode 100644 (file)
index 0000000..da291c5
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import TimeFormatter, { TimeFormatterProps } from '../TimeFormatter';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('standard');
+  expect(shallowRender({ long: true })).toMatchSnapshot('long');
+});
+
+function shallowRender(overrides: Partial<TimeFormatterProps> = {}) {
+  return shallow(
+    <TimeFormatter date={new Date('2020-02-20T20:20:20Z')} {...overrides}>
+      {(formatted) => <span>{formatted}</span>}
+    </TimeFormatter>
+  );
+}
diff --git a/server/sonar-ui-common/components/intl/__tests__/__snapshots__/DateFormatter-test.tsx.snap b/server/sonar-ui-common/components/intl/__tests__/__snapshots__/DateFormatter-test.tsx.snap
new file mode 100644 (file)
index 0000000..2f603c6
--- /dev/null
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: long 1`] = `
+<FormattedDate
+  day="numeric"
+  month="long"
+  value={2020-02-20T20:20:20.000Z}
+  year="numeric"
+>
+  <Component />
+</FormattedDate>
+`;
+
+exports[`should render correctly: standard 1`] = `
+<FormattedDate
+  day="2-digit"
+  month="short"
+  value={2020-02-20T20:20:20.000Z}
+  year="numeric"
+>
+  <Component />
+</FormattedDate>
+`;
diff --git a/server/sonar-ui-common/components/intl/__tests__/__snapshots__/DateFromNow-test.tsx.snap b/server/sonar-ui-common/components/intl/__tests__/__snapshots__/DateFromNow-test.tsx.snap
new file mode 100644 (file)
index 0000000..19ee546
--- /dev/null
@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<DateTimeFormatter
+  date={2020-02-20T20:20:20.000Z}
+>
+  <Component />
+</DateTimeFormatter>
+`;
+
+exports[`should render correctly: children 1`] = `
+<span
+  title="2020-02-20T20:20:20Z"
+>
+  <FormattedRelative
+    updateInterval={10000}
+    value={2020-02-20T20:20:20.000Z}
+  >
+    [Function]
+  </FormattedRelative>
+</span>
+`;
diff --git a/server/sonar-ui-common/components/intl/__tests__/__snapshots__/DateTimeFormatter-test.tsx.snap b/server/sonar-ui-common/components/intl/__tests__/__snapshots__/DateTimeFormatter-test.tsx.snap
new file mode 100644 (file)
index 0000000..c991112
--- /dev/null
@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: standard 1`] = `
+<FormattedDate
+  day="numeric"
+  hour="numeric"
+  minute="numeric"
+  month="long"
+  value={2020-02-20T20:20:20.000Z}
+  year="numeric"
+>
+  <Component />
+</FormattedDate>
+`;
diff --git a/server/sonar-ui-common/components/intl/__tests__/__snapshots__/TimeFormatter-test.tsx.snap b/server/sonar-ui-common/components/intl/__tests__/__snapshots__/TimeFormatter-test.tsx.snap
new file mode 100644 (file)
index 0000000..7c19be3
--- /dev/null
@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: long 1`] = `
+<FormattedTime
+  hour="numeric"
+  minute="numeric"
+  second="numeric"
+  value={2020-02-20T20:20:20.000Z}
+>
+  <Component />
+</FormattedTime>
+`;
+
+exports[`should render correctly: standard 1`] = `
+<FormattedTime
+  hour="numeric"
+  minute="numeric"
+  value={2020-02-20T20:20:20.000Z}
+>
+  <Component />
+</FormattedTime>
+`;
diff --git a/server/sonar-ui-common/components/lazyLoadComponent.tsx b/server/sonar-ui-common/components/lazyLoadComponent.tsx
new file mode 100644 (file)
index 0000000..9600306
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IS_SSR } from '../helpers/init';
+import { translate } from '../helpers/l10n';
+import { requestTryAndRepeatUntil } from '../helpers/request';
+import { Alert } from './ui/Alert';
+
+export function lazyLoadComponent<T extends React.ComponentType<any>>(
+  factory: () => Promise<{ default: T }>,
+  displayName?: string
+) {
+  const LazyComponent = React.lazy(() =>
+    requestTryAndRepeatUntil(factory, { max: 2, slowThreshold: 2 }, () => true)
+  );
+
+  function LazyComponentWrapper(props: React.ComponentProps<T>) {
+    if (IS_SSR) {
+      return null;
+    }
+    return (
+      <LazyErrorBoundary>
+        <React.Suspense fallback={null}>
+          <LazyComponent {...props} />
+        </React.Suspense>
+      </LazyErrorBoundary>
+    );
+  }
+
+  LazyComponentWrapper.displayName = displayName;
+  return LazyComponentWrapper;
+}
+
+interface ErrorBoundaryProps {
+  children: React.ReactNode;
+}
+
+interface ErrorBoundaryState {
+  hasError: boolean;
+}
+
+export class LazyErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
+  state: ErrorBoundaryState = { hasError: false };
+
+  static getDerivedStateFromError() {
+    // Update state so the next render will show the fallback UI.
+    return { hasError: true };
+  }
+
+  render() {
+    if (this.state.hasError) {
+      return <Alert variant="error">{translate('default_error_message')}</Alert>;
+    }
+    return this.props.children;
+  }
+}
diff --git a/server/sonar-ui-common/components/theme.ts b/server/sonar-ui-common/components/theme.ts
new file mode 100644 (file)
index 0000000..c3810aa
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { css, ThemeContext as EmotionThemeContext } from '@emotion/core';
+import emotionStyled, { CreateStyled } from '@emotion/styled';
+import {
+  ThemeProvider as EmotionThemeProvider,
+  ThemeProviderProps,
+  useTheme as emotionUseTheme,
+  withTheme,
+} from 'emotion-theming';
+import * as React from 'react';
+
+export interface Theme {
+  colors: T.Dict<string>;
+  sizes: T.Dict<string>;
+  rawSizes: T.Dict<number>;
+  fonts: T.Dict<string>;
+  zIndexes: T.Dict<string>;
+  others: T.Dict<string>;
+}
+
+export interface ThemedProps {
+  theme: Theme;
+}
+
+const ThemeContext = EmotionThemeContext as React.Context<Theme>;
+
+export const styled = emotionStyled as CreateStyled<Theme>;
+export const ThemeConsumer = ThemeContext.Consumer;
+export const ThemeProvider = EmotionThemeProvider as React.ProviderExoticComponent<
+  ThemeProviderProps<Theme>
+>;
+export const useTheme = emotionUseTheme as () => Theme;
+
+export function themeGet(type: keyof Theme, name: string | number) {
+  return function ({ theme }: Partial<ThemedProps>) {
+    return theme?.[type][name];
+  };
+}
+export function themeColor(name: keyof Theme['colors']) {
+  return themeGet('colors', name);
+}
+export function themeSize(name: keyof Theme['sizes']) {
+  return themeGet('sizes', name);
+}
+
+export { css, withTheme };
+export default ThemeContext;
diff --git a/server/sonar-ui-common/components/ui/Alert.tsx b/server/sonar-ui-common/components/ui/Alert.tsx
new file mode 100644 (file)
index 0000000..2921735
--- /dev/null
@@ -0,0 +1,159 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import AlertErrorIcon from '../icons/AlertErrorIcon';
+import AlertSuccessIcon from '../icons/AlertSuccessIcon';
+import AlertWarnIcon from '../icons/AlertWarnIcon';
+import InfoIcon from '../icons/InfoIcon';
+import { css, styled, Theme, themeColor, ThemedProps, themeSize, useTheme } from '../theme';
+import DeferredSpinner from './DeferredSpinner';
+
+type AlertDisplay = 'banner' | 'inline' | 'block';
+type AlertVariant = 'error' | 'warning' | 'success' | 'info' | 'loading';
+
+export interface AlertProps {
+  display?: AlertDisplay;
+  variant: AlertVariant;
+}
+
+interface AlertVariantInformation {
+  icon: JSX.Element;
+  color: string;
+  borderColor: string;
+  backGroundColor: string;
+}
+
+const StyledAlertIcon = styled.div<{ isBanner: boolean; variantInfo: AlertVariantInformation }>`
+  flex: 0 0 auto;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: calc(${({ isBanner }) => (isBanner ? 2 : 4)} * ${themeSize('gridSize')});
+  border-right: ${({ isBanner }) => (!isBanner ? '1px solid' : 'none')};
+  border-color: ${({ variantInfo }) => variantInfo.borderColor};
+`;
+
+const StyledAlertContent = styled.div`
+  flex: 1 1 auto;
+  overflow: auto;
+  text-align: left;
+  padding: ${themeSize('gridSize')} calc(2 * ${themeSize('gridSize')});
+`;
+
+const alertInnerIsBannerMixin = ({ theme }: ThemedProps) => css`
+  min-width: ${theme.sizes.minPageWidth};
+  max-width: ${theme.sizes.maxPageWidth};
+  margin-left: auto;
+  margin-right: auto;
+  padding-left: ${theme.sizes.pagePadding};
+  padding-right: ${theme.sizes.pagePadding};
+  box-sizing: border-box;
+`;
+
+const StyledAlertInner = styled.div<{ isBanner: boolean }>`
+  display: flex;
+  align-items: stretch;
+  ${({ isBanner }) => (isBanner ? alertInnerIsBannerMixin : null)}
+`;
+
+const StyledAlert = styled.div<{ isInline: boolean; variantInfo: AlertVariantInformation }>`
+  border: 1px solid;
+  border-radius: 2px;
+  margin-bottom: ${themeSize('gridSize')};
+  border-color: ${({ variantInfo }) => variantInfo.borderColor};
+  background-color: ${({ variantInfo }) => variantInfo.backGroundColor};
+  color: ${({ variantInfo }) => variantInfo.color};
+  display: ${({ isInline }) => (isInline ? 'inline-block' : 'block')};
+
+  :empty {
+    display: none;
+  }
+
+  a,
+  .button-link {
+    border-color: ${themeColor('darkBlue')};
+  }
+`;
+
+function getAlertVariantInfo({ colors }: Theme, variant: AlertVariant): AlertVariantInformation {
+  const variantList: T.Dict<AlertVariantInformation> = {
+    error: {
+      icon: <AlertErrorIcon fill={colors.alertIconError} />,
+      color: colors.alertTextError,
+      borderColor: colors.alertBorderError,
+      backGroundColor: colors.alertBackgroundError,
+    },
+    warning: {
+      icon: <AlertWarnIcon fill={colors.alertIconWarning} />,
+      color: colors.alertTextWarning,
+      borderColor: colors.alertBorderWarning,
+      backGroundColor: colors.alertBackgroundWarning,
+    },
+    success: {
+      icon: <AlertSuccessIcon fill={colors.alertIconSuccess} />,
+      color: colors.alertTextSuccess,
+      borderColor: colors.alertBorderSuccess,
+      backGroundColor: colors.alertBackgroundSuccess,
+    },
+    info: {
+      icon: <InfoIcon fill={colors.alertIconInfo} />,
+      color: colors.alertTextInfo,
+      borderColor: colors.alertBorderInfo,
+      backGroundColor: colors.alertBackgroundInfo,
+    },
+    loading: {
+      icon: <DeferredSpinner timeout={0} />,
+      color: colors.alertTextInfo,
+      borderColor: colors.alertBorderInfo,
+      backGroundColor: colors.alertBackgroundInfo,
+    },
+  };
+
+  return variantList[variant];
+}
+
+export function Alert(props: AlertProps & React.HTMLAttributes<HTMLDivElement>) {
+  const theme = useTheme();
+  const { className, display, variant, ...domProps } = props;
+  const isInline = display === 'inline';
+  const isBanner = display === 'banner';
+  const variantInfo = getAlertVariantInfo(theme, variant);
+
+  return (
+    <StyledAlert
+      className={classNames('alert', className)}
+      isInline={isInline}
+      role="alert"
+      variantInfo={variantInfo}
+      {...domProps}>
+      <StyledAlertInner isBanner={isBanner}>
+        <StyledAlertIcon
+          aria-label={translate('alert.tooltip', variant)}
+          isBanner={isBanner}
+          variantInfo={variantInfo}>
+          {variantInfo.icon}
+        </StyledAlertIcon>
+        <StyledAlertContent className="alert-content">{props.children}</StyledAlertContent>
+      </StyledAlertInner>
+    </StyledAlert>
+  );
+}
diff --git a/server/sonar-ui-common/components/ui/AutoEllipsis.tsx b/server/sonar-ui-common/components/ui/AutoEllipsis.tsx
new file mode 100644 (file)
index 0000000..a6a9a89
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import classNames from 'classnames';
+import * as React from 'react';
+
+type EllipsisPredicate = (
+  node: HTMLElement,
+  props: Omit<AutoEllipsisProps, 'customShouldEllipsis'>
+) => boolean;
+
+interface AutoEllipsisProps {
+  customShouldEllipsis?: EllipsisPredicate;
+  maxHeight?: number;
+  maxWidth?: number;
+  useParent?: boolean;
+}
+
+interface Props extends AutoEllipsisProps {
+  children: React.ReactElement;
+}
+
+/*
+ * This component allows to automatically add the .text-ellipsis class on it's children if this one
+ * might overflow the max width/height passed as props.
+ * If one of maxHeight or maxWidth is not specified, they will be ignored in the conditions to add the ellipsis class.
+ * If useParent is true, then the parent size will be used instead of the undefined maxHeight/maxWidth
+ */
+export default function AutoEllipsis(props: Props) {
+  const { children, ...autoEllipsisProps } = props;
+  const [autoEllispis, ref] = useAutoEllipsis(autoEllipsisProps);
+
+  return React.cloneElement(children, {
+    className: classNames(children.props.className, { 'text-ellipsis': autoEllispis }),
+    ref,
+  });
+}
+
+export function useAutoEllipsis(props: AutoEllipsisProps): [boolean, (node: HTMLElement) => void] {
+  const [autoEllipsis, setAutoEllipsis] = React.useState(false);
+
+  // useCallback instead of useRef to be able to compute if the flag is needed as soon as the ref is attached
+  // useRef doesn't accept a callback to notify us that the current ref value was attached,
+  // see https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node for more info on this.
+  const ref = React.useCallback(
+    (node: HTMLElement) => {
+      if (!autoEllipsis && node) {
+        const shouldEllipsis = props.customShouldEllipsis ?? defaultShouldEllipsis;
+        setAutoEllipsis(shouldEllipsis(node, props));
+      }
+    },
+    // We don't want to apply this effect when ellipsis state change, only this effect can change it
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+    [props.customShouldEllipsis, props.maxHeight, props.maxWidth, props.useParent]
+  );
+
+  return [autoEllipsis, ref];
+}
+
+export const defaultShouldEllipsis: EllipsisPredicate = (
+  node,
+  { useParent = true, maxWidth, maxHeight }
+) => {
+  if (node.parentElement && useParent) {
+    maxWidth = maxWidth ?? node.parentElement.clientWidth;
+    maxHeight = maxHeight ?? node.parentElement.clientHeight;
+  }
+  return (
+    (maxWidth !== undefined && node.clientWidth > maxWidth) ||
+    (maxHeight !== undefined && node.clientHeight > maxHeight)
+  );
+};
diff --git a/server/sonar-ui-common/components/ui/ContextNavBar.css b/server/sonar-ui-common/components/ui/ContextNavBar.css
new file mode 100644 (file)
index 0000000..7055704
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.navbar-context,
+.navbar-context .navbar-inner {
+  background-color: #fff;
+  z-index: var(--contextbarZIndex);
+}
+
+.navbar-context .navbar-inner {
+  padding-top: var(--gridSize);
+  border-bottom: 1px solid var(--barBorderColor);
+}
+
+.navbar-context .navbar-inner-with-notif {
+  border-bottom: none;
+}
+
+.navbar-context-justified {
+  display: flex;
+  justify-content: space-between;
+}
+
+/* use `min-width: 0` to cut breadcrumb links (to end with "...") */
+/* https://stackoverflow.com/questions/38223879/white-space-nowrap-breaks-flexbox-layout */
+.navbar-context-header {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+  height: calc(4 * var(--gridSize));
+  font-size: var(--bigFontSize);
+}
+
+/* disallow icons and slash separators to shrink */
+.navbar-context-header > *:not(.navbar-context-header-breadcrumb-link) {
+  flex-shrink: 0;
+}
+
+.navbar-context-header-breadcrumb-link {
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.navbar-context-header .slash-separator {
+  margin-left: var(--gridSize);
+  margin-right: var(--gridSize);
+  font-size: 24px;
+}
+
+.navbar-context-header .slash-separator::after {
+  color: rgba(68, 68, 68, 0.2);
+}
+
+/* set `min-width: 0` to allow flexbox item to shrink */
+/* https://stackoverflow.com/questions/38223879/white-space-nowrap-breaks-flexbox-layout */
+.navbar-context-meta {
+  display: flex;
+  align-items: center;
+  height: calc(4 * var(--gridSize));
+  padding-left: 20px;
+  color: var(--secondFontColor);
+  font-size: var(--smallFontSize);
+  text-align: right;
+}
+
+.navbar-context-meta-secondary {
+  position: absolute;
+  top: 34px;
+  right: 0;
+  padding: 0 20px;
+  white-space: nowrap;
+}
+
+.navbar-context-description {
+  display: inline-block;
+  line-height: var(--controlHeight);
+  margin-left: var(--gridSize);
+  padding-top: 4px;
+  color: var(--secondFontColor);
+  font-size: var(--smallFontSize);
+}
diff --git a/server/sonar-ui-common/components/ui/ContextNavBar.tsx b/server/sonar-ui-common/components/ui/ContextNavBar.tsx
new file mode 100644 (file)
index 0000000..abae91c
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import './ContextNavBar.css';
+import NavBar from './NavBar';
+
+interface Props {
+  className?: string;
+  height: number;
+  [attr: string]: any;
+}
+
+export default function ContextNavBar({ className, ...other }: Props) {
+  return <NavBar className={classNames('navbar-context', className)} {...other} />;
+}
diff --git a/server/sonar-ui-common/components/ui/DeferredSpinner.css b/server/sonar-ui-common/components/ui/DeferredSpinner.css
new file mode 100644 (file)
index 0000000..7ee7610
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.deferred-spinner {
+  position: relative;
+  vertical-align: middle;
+  width: 16px;
+  height: 16px;
+  border: 2px solid var(--blue);
+  border-radius: 50%;
+  animation: spin 0.75s infinite linear;
+}
+
+.deferred-spinner:before,
+.deferred-spinner:after {
+  left: -2px;
+  top: -2px;
+  display: none;
+  position: absolute;
+  content: '';
+  width: inherit;
+  height: inherit;
+  border: inherit;
+  border-radius: inherit;
+}
+
+.deferred-spinner,
+.deferred-spinner:before,
+.deferred-spinner:after {
+  display: inline-block;
+  box-sizing: border-box;
+  border-color: transparent;
+  border-top-color: var(--blue);
+  animation-duration: 1.2s;
+}
+
+.deferred-spinner:before {
+  transform: rotate(120deg);
+}
+
+.deferred-spinner:after {
+  transform: rotate(240deg);
+}
+
+.deferred-spinner-placeholder {
+  position: relative;
+  display: inline-block;
+  vertical-align: middle;
+  width: 16px;
+  height: 16px;
+  visibility: hidden;
+}
+
+@keyframes spin {
+  from {
+    transform: rotate(0deg);
+  }
+
+  to {
+    transform: rotate(360deg);
+  }
+}
diff --git a/server/sonar-ui-common/components/ui/DeferredSpinner.tsx b/server/sonar-ui-common/components/ui/DeferredSpinner.tsx
new file mode 100644 (file)
index 0000000..1b972fe
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import './DeferredSpinner.css';
+
+interface Props {
+  children?: React.ReactNode;
+  className?: string;
+  customSpinner?: JSX.Element;
+  loading?: boolean;
+  placeholder?: boolean;
+  timeout?: number;
+}
+
+interface State {
+  showSpinner: boolean;
+}
+
+const DEFAULT_TIMEOUT = 100;
+
+export default class DeferredSpinner extends React.PureComponent<Props, State> {
+  timer?: number;
+
+  state: State = { showSpinner: false };
+
+  componentDidMount() {
+    if (this.props.loading == null || this.props.loading === true) {
+      this.startTimer();
+    }
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.loading === false && this.props.loading === true) {
+      this.stopTimer();
+      this.startTimer();
+    }
+    if (prevProps.loading === true && this.props.loading === false) {
+      this.stopTimer();
+      this.setState({ showSpinner: false });
+    }
+  }
+
+  componentWillUnmount() {
+    this.stopTimer();
+  }
+
+  startTimer = () => {
+    this.timer = window.setTimeout(
+      () => this.setState({ showSpinner: true }),
+      this.props.timeout || DEFAULT_TIMEOUT
+    );
+  };
+
+  stopTimer = () => {
+    window.clearTimeout(this.timer);
+  };
+
+  render() {
+    if (this.state.showSpinner) {
+      return (
+        this.props.customSpinner || (
+          <i className={classNames('deferred-spinner', this.props.className)} />
+        )
+      );
+    }
+    return (
+      this.props.children ||
+      (this.props.placeholder ? (
+        <i className={classNames('deferred-spinner-placeholder', this.props.className)} />
+      ) : null)
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/ui/DuplicationsRating.css b/server/sonar-ui-common/components/ui/DuplicationsRating.css
new file mode 100644 (file)
index 0000000..0541c36
--- /dev/null
@@ -0,0 +1,171 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.duplications-rating {
+  position: relative;
+  display: inline-flex;
+  vertical-align: top;
+  justify-content: center;
+  align-items: center;
+  width: var(--controlHeight);
+  height: var(--controlHeight);
+  border: 3px solid var(--orange);
+  border-radius: var(--controlHeight);
+  box-sizing: border-box;
+}
+
+.duplications-rating-small {
+  width: 16px;
+  height: 16px;
+  border-width: 2px;
+}
+
+.duplications-rating-big {
+  width: 40px;
+  height: 40px;
+  border-width: 3px;
+}
+
+.duplications-rating-huge {
+  width: 60px;
+  height: 60px;
+  border-width: 4px;
+  border-radius: 30px;
+}
+
+.duplications-rating-muted {
+  border-color: #bdbdbd !important;
+}
+
+.duplications-rating-muted:after {
+  background-color: #bdbdbd !important;
+}
+
+.duplications-rating:after {
+  border-radius: var(--controlHeight);
+  content: '';
+}
+
+.duplications-rating-A {
+  border-color: var(--green);
+}
+
+.duplications-rating-A:after {
+  display: none;
+}
+
+.duplications-rating-B {
+  border-color: var(--lightGreen);
+}
+
+.duplications-rating-B:after {
+  width: 6px;
+  height: 6px;
+  background-color: var(--lightGreen);
+}
+
+.duplications-rating-small.duplications-rating-B:after {
+  width: 2px;
+  height: 2px;
+}
+
+.duplications-rating-big.duplications-rating-B:after {
+  width: var(--smallFontSize);
+  height: var(--smallFontSize);
+}
+
+.duplications-rating-huge.duplications-rating-B:after {
+  width: 18px;
+  height: 18px;
+}
+
+.duplications-rating-C {
+  border-color: var(--yellow);
+}
+
+.duplications-rating-C:after {
+  width: 8px;
+  height: 8px;
+  background-color: var(--yellow);
+}
+
+.duplications-rating-small.duplications-rating-C:after {
+  width: 6px;
+  height: 6px;
+}
+
+.duplications-rating-big.duplications-rating-C:after {
+  width: 16px;
+  height: 16px;
+}
+
+.duplications-rating-huge.duplications-rating-C:after {
+  width: var(--controlHeight);
+  height: var(--controlHeight);
+}
+
+.duplications-rating-D {
+  border-color: var(--orange);
+}
+
+.duplications-rating-D:after {
+  width: var(--smallFontSize);
+  height: var(--smallFontSize);
+  background-color: var(--orange);
+}
+
+.duplications-rating-small.duplications-rating-D:after {
+  width: 8px;
+  height: 8px;
+}
+
+.duplications-rating-big.duplications-rating-D:after {
+  width: var(--controlHeight);
+  height: var(--controlHeight);
+}
+
+.duplications-rating-huge.duplications-rating-D:after {
+  width: 36px;
+  height: 36px;
+}
+
+.duplications-rating-E {
+  border-color: var(--red);
+}
+
+.duplications-rating-E:after {
+  width: 14px;
+  height: 14px;
+  background-color: var(--red);
+}
+
+.duplications-rating-small.duplications-rating-E:after {
+  width: 10px;
+  height: 10px;
+}
+
+.duplications-rating-big.duplications-rating-E:after {
+  width: 28px;
+  height: 28px;
+}
+
+.duplications-rating-huge.duplications-rating-E:after {
+  width: 42px;
+  height: 42px;
+}
diff --git a/server/sonar-ui-common/components/ui/DuplicationsRating.tsx b/server/sonar-ui-common/components/ui/DuplicationsRating.tsx
new file mode 100644 (file)
index 0000000..6d2c6df
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import { inRange } from 'lodash';
+import * as React from 'react';
+import './DuplicationsRating.css';
+
+interface Props {
+  muted?: boolean;
+  size?: 'small' | 'normal' | 'big' | 'huge';
+  value: number | null | undefined;
+}
+
+export default function DuplicationsRating({ muted = false, size = 'normal', value }: Props) {
+  const className = classNames('duplications-rating', {
+    'duplications-rating-small': size === 'small',
+    'duplications-rating-big': size === 'big',
+    'duplications-rating-huge': size === 'huge',
+    'duplications-rating-muted': muted || value == null || isNaN(value),
+    'duplications-rating-A': inRange(value || 0, 0, 3),
+    'duplications-rating-B': inRange(value || 0, 3, 5),
+    'duplications-rating-C': inRange(value || 0, 5, 10),
+    'duplications-rating-D': inRange(value || 0, 10, 20),
+    'duplications-rating-E': (value || 0) >= 20,
+  });
+
+  return <div className={className} />;
+}
diff --git a/server/sonar-ui-common/components/ui/FilesCounter.tsx b/server/sonar-ui-common/components/ui/FilesCounter.tsx
new file mode 100644 (file)
index 0000000..8303e4f
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import { formatMeasure } from '../../helpers/measures';
+
+interface Props {
+  className?: string;
+  current?: number;
+  total: number;
+}
+
+export default function FilesCounter({ className, current, total }: Props) {
+  return (
+    <span className={className}>
+      <strong>
+        {current !== undefined && (
+          <span>
+            {formatMeasure(current, 'INT')}
+            {' / '}
+          </span>
+        )}
+        {formatMeasure(total, 'INT')}
+      </strong>{' '}
+      {translate('component_measures.files')}
+    </span>
+  );
+}
diff --git a/server/sonar-ui-common/components/ui/GenericAvatar.tsx b/server/sonar-ui-common/components/ui/GenericAvatar.tsx
new file mode 100644 (file)
index 0000000..9221bdc
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import { getTextColor, stringToColor } from '../../helpers/colors';
+
+interface Props {
+  className?: string;
+  name: string;
+  round?: boolean;
+  size: number;
+}
+
+export default function GenericAvatar({ className, name, round, size }: Props) {
+  const color = stringToColor(name);
+
+  let text = '';
+  const words = name.split(/\s+/).filter((word) => word.length > 0);
+  if (words.length >= 2) {
+    text = words[0][0] + words[1][0];
+  } else if (name.length > 0) {
+    text = name[0];
+  }
+
+  return (
+    <div
+      className={classNames(className, 'rounded')}
+      style={{
+        backgroundColor: color,
+        borderRadius: round ? '50%' : undefined,
+        color: getTextColor(color),
+        display: 'inline-block',
+        fontSize: Math.min(size / 2, 14),
+        fontWeight: 'normal',
+        height: size,
+        lineHeight: `${size}px`,
+        textAlign: 'center',
+        verticalAlign: 'top',
+        width: size,
+      }}>
+      {text.toUpperCase()}
+    </div>
+  );
+}
diff --git a/server/sonar-ui-common/components/ui/Level.css b/server/sonar-ui-common/components/ui/Level.css
new file mode 100644 (file)
index 0000000..ec4e830
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.level {
+  display: inline-block;
+  width: auto;
+  min-width: 80px;
+  padding-left: 9px;
+  padding-right: 9px;
+  height: var(--controlHeight);
+  line-height: var(--controlHeight);
+  border-radius: var(--controlHeight);
+  box-sizing: border-box;
+  color: #fff;
+  letter-spacing: 0.02em;
+  font-size: var(--baseFontSize);
+  font-weight: 400;
+  text-align: center;
+  text-shadow: 0 0 1px rgba(0, 0, 0, 0.35);
+}
+
+.level-small {
+  width: auto;
+  min-width: 64px;
+  padding-left: 9px;
+  padding-right: 9px;
+  margin-top: -1px;
+  margin-bottom: -1px;
+  height: var(--smallControlHeight);
+  line-height: var(--smallControlHeight);
+  font-size: var(--smallFontSize);
+}
+
+.level-muted {
+  background-color: #bdbdbd !important;
+}
+
+a > .level {
+  margin-bottom: -1px;
+  border-bottom: 1px solid;
+  transition: all 0.2s ease;
+}
+
+a > .level:hover {
+  opacity: 0.8;
+}
+
+.level-OK {
+  background-color: var(--green);
+}
+
+.level-WARN {
+  background-color: var(--orange);
+}
+
+.level-ERROR {
+  background-color: var(--red);
+}
+
+.level-NONE {
+  background-color: var(--gray71);
+}
+
+.level-NOT_COMPUTED {
+  background-color: var(--gray40);
+}
diff --git a/server/sonar-ui-common/components/ui/Level.tsx b/server/sonar-ui-common/components/ui/Level.tsx
new file mode 100644 (file)
index 0000000..0e384f3
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import { formatMeasure } from '../../helpers/measures';
+import './Level.css';
+
+export interface LevelProps {
+  'aria-label'?: string;
+  'aria-labelledby'?: string;
+  className?: string;
+  level: string;
+  small?: boolean;
+  muted?: boolean;
+}
+
+export default function Level(props: LevelProps) {
+  const formatted = formatMeasure(props.level, 'LEVEL');
+  const className = classNames(props.className, 'level', 'level-' + props.level, {
+    'level-small': props.small,
+    'level-muted': props.muted,
+  });
+
+  return (
+    <span
+      aria-label={props['aria-label']}
+      aria-labelledby={props['aria-labelledby']}
+      className={className}>
+      {formatted}
+    </span>
+  );
+}
diff --git a/server/sonar-ui-common/components/ui/MandatoryFieldMarker.tsx b/server/sonar-ui-common/components/ui/MandatoryFieldMarker.tsx
new file mode 100644 (file)
index 0000000..9d1e2b8
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
+
+export interface MandatoryFieldMarkerProps {
+  className?: string;
+}
+
+export default function MandatoryFieldMarker({ className }: MandatoryFieldMarkerProps) {
+  return (
+    <em
+      aria-label={translate('field_required')}
+      className={classNames('mandatory little-spacer-left', className)}>
+      *
+    </em>
+  );
+}
diff --git a/server/sonar-ui-common/components/ui/MandatoryFieldsExplanation.tsx b/server/sonar-ui-common/components/ui/MandatoryFieldsExplanation.tsx
new file mode 100644 (file)
index 0000000..958db7d
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { translate } from '../../helpers/l10n';
+
+export interface MandatoryFieldsExplanationProps {
+  className?: string;
+}
+
+export default function MandatoryFieldsExplanation({ className }: MandatoryFieldsExplanationProps) {
+  return (
+    <div aria-hidden={true} className={classNames('text-muted', className)}>
+      <FormattedMessage
+        id="fields_marked_with_x_required"
+        defaultMessage={translate('fields_marked_with_x_required')}
+        values={{ star: <em className="mandatory">*</em> }}
+      />
+    </div>
+  );
+}
diff --git a/server/sonar-ui-common/components/ui/NavBar.css b/server/sonar-ui-common/components/ui/NavBar.css
new file mode 100644 (file)
index 0000000..030cc57
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.navbar,
+[class^='navbar-'],
+[class*=' navbar-'] {
+  box-sizing: border-box;
+}
+
+.navbar {
+}
+
+.navbar-inner {
+  position: fixed;
+  left: 0;
+  right: 0;
+}
+
+.navbar-inner > div {
+  position: relative;
+  min-width: var(--minPageWidth);
+  padding-left: var(--pagePadding);
+  padding-right: var(--pagePadding);
+}
+
+.navbar-limited {
+  max-width: var(--maxPageWidth);
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.ReactModal__Body--open .navbar-inner {
+  padding-right: var(--sbw);
+}
diff --git a/server/sonar-ui-common/components/ui/NavBar.tsx b/server/sonar-ui-common/components/ui/NavBar.tsx
new file mode 100644 (file)
index 0000000..5668bf4
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import { throttle } from 'lodash';
+import * as React from 'react';
+import './NavBar.css';
+
+interface Props extends React.HTMLProps<HTMLDivElement> {
+  children?: React.ReactNode;
+  className?: string;
+  height: number;
+  limited?: boolean;
+  top?: number;
+  notif?: React.ReactNode;
+}
+
+interface State {
+  left: number;
+}
+
+export default class NavBar extends React.PureComponent<Props, State> {
+  throttledFollowHorizontalScroll: () => void;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = { left: 0 };
+    this.throttledFollowHorizontalScroll = throttle(this.followHorizontalScroll, 10);
+  }
+
+  componentDidMount() {
+    document.addEventListener('scroll', this.throttledFollowHorizontalScroll);
+  }
+
+  componentWillUnmount() {
+    document.removeEventListener('scroll', this.throttledFollowHorizontalScroll);
+  }
+
+  followHorizontalScroll = () => {
+    if (document.documentElement) {
+      this.setState({ left: -document.documentElement.scrollLeft });
+    }
+  };
+
+  render() {
+    const { children, className, height, limited = true, top, notif, ...other } = this.props;
+    return (
+      <nav {...other} className={classNames('navbar', className)} style={{ height, top }}>
+        <div
+          className={classNames('navbar-inner', { 'navbar-inner-with-notif': notif != null })}
+          style={{ height, left: this.state.left }}>
+          <div className={classNames('clearfix', { 'navbar-limited': limited })}>{children}</div>
+          {notif}
+        </div>
+      </nav>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/ui/NavBarTabs.css b/server/sonar-ui-common/components/ui/NavBarTabs.css
new file mode 100644 (file)
index 0000000..6b7bfe8
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.navbar-tabs {
+  display: flex;
+  align-items: center;
+  clear: left;
+  height: var(--controlHeight);
+  margin-top: var(--gridSize);
+}
+
+.navbar-tabs > li + li {
+  margin-left: 20px;
+}
+
+.navbar-tabs > li > a {
+  display: block;
+  height: var(--controlHeight);
+  line-height: 16px;
+  padding-top: 2px;
+  border-bottom: 3px solid transparent;
+  box-sizing: border-box;
+  color: var(--baseFontColor);
+  transition: none;
+}
+
+.navbar-tabs > li > a.active,
+.navbar-tabs > li > a:hover,
+.navbar-tabs > li > a:focus {
+  border-bottom-color: var(--blue);
+}
diff --git a/server/sonar-ui-common/components/ui/NavBarTabs.tsx b/server/sonar-ui-common/components/ui/NavBarTabs.tsx
new file mode 100644 (file)
index 0000000..af79655
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import './NavBarTabs.css';
+
+interface Props {
+  children?: any;
+  className?: string;
+  [attr: string]: any;
+}
+
+export default function NavBarTabs({ children, className, ...other }: Props) {
+  return (
+    <ul {...other} className={classNames('navbar-tabs', className)}>
+      {children}
+    </ul>
+  );
+}
diff --git a/server/sonar-ui-common/components/ui/NewsBox.css b/server/sonar-ui-common/components/ui/NewsBox.css
new file mode 100644 (file)
index 0000000..0465f2f
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.news-box {
+  border: 1px solid var(--alertBorderInfo);
+  border-radius: 2px;
+  background-color: var(--veryLightBlue);
+  padding: var(--gridSize);
+}
+
+.news-box-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
diff --git a/server/sonar-ui-common/components/ui/NewsBox.tsx b/server/sonar-ui-common/components/ui/NewsBox.tsx
new file mode 100644 (file)
index 0000000..67bf921
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import { ClearButton } from '../controls/buttons';
+import './NewsBox.css';
+
+export interface Props {
+  children: React.ReactNode;
+  className?: string;
+  onClose: () => void;
+  title: string;
+}
+
+export default function NewsBox({ children, className, onClose, title }: Props) {
+  return (
+    <div className={classNames('news-box', className)} role="alert">
+      <div className="news-box-header">
+        <div className="display-flex-center">
+          <span className="badge badge-info spacer-right">{translate('new')}</span>
+          <strong>{title}</strong>
+        </div>
+        <ClearButton
+          className="button-tiny"
+          iconProps={{ size: 12, thin: true }}
+          onClick={onClose}
+        />
+      </div>
+      <div className="big-spacer-top note">{children}</div>
+    </div>
+  );
+}
diff --git a/server/sonar-ui-common/components/ui/PageActions.tsx b/server/sonar-ui-common/components/ui/PageActions.tsx
new file mode 100644 (file)
index 0000000..1785393
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import FilesCounter from './FilesCounter';
+
+export interface Props {
+  current?: number;
+  showShortcuts?: boolean;
+  total?: number;
+}
+
+export default function PageActions(props: Props) {
+  const { current, showShortcuts, total = 0 } = props;
+
+  return (
+    <div className="page-actions display-flex-center">
+      {showShortcuts && (
+        <span className="note nowrap">
+          <span className="big-spacer-right">
+            <span className="shortcut-button little-spacer-right">↑</span>
+            <span className="shortcut-button little-spacer-right">↓</span>
+            {translate('component_measures.to_select_files')}
+          </span>
+
+          <span>
+            <span className="shortcut-button little-spacer-right">←</span>
+            <span className="shortcut-button little-spacer-right">→</span>
+            {translate('component_measures.to_navigate')}
+          </span>
+        </span>
+      )}
+      {total > 0 && (
+        <div className="nowrap">
+          <FilesCounter className="big-spacer-left" current={current} total={total} />
+        </div>
+      )}
+    </div>
+  );
+}
diff --git a/server/sonar-ui-common/components/ui/Rating.css b/server/sonar-ui-common/components/ui/Rating.css
new file mode 100644 (file)
index 0000000..82b9604
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.rating {
+  display: inline-block;
+  width: var(--controlHeight);
+  height: var(--controlHeight);
+  line-height: var(--controlHeight);
+  border-radius: var(--controlHeight);
+  box-sizing: border-box;
+  color: #fff;
+  font-size: var(--bigFontSize);
+  font-weight: 400;
+  text-align: center;
+  text-shadow: 0 0 1px rgba(0, 0, 0, 0.35);
+}
+
+.rating-muted {
+  background-color: #bdbdbd !important;
+  color: #fff !important;
+  text-shadow: 0 0 1px rgba(0, 0, 0, 0.35) !important;
+}
+
+a > .rating {
+  margin-bottom: -1px;
+  border-bottom: 1px solid;
+  transition: all 0.2s ease;
+}
+
+a > .rating:hover {
+  opacity: 0.8;
+}
+
+.rating-A {
+  line-height: 23px;
+  background-color: var(--green);
+}
+
+a > .rating-A {
+  border-bottom-color: var(--green);
+}
+
+.rating-B {
+  background-color: var(--lightGreen);
+}
+
+a .rating-B {
+  border-bottom-color: var(--lightGreen);
+}
+
+.rating-C {
+  background-color: var(--yellow);
+}
+
+a .rating-C {
+  border-bottom-color: var(--yellow);
+}
+
+.rating-D {
+  background-color: var(--orange);
+}
+
+a .rating-D {
+  border-bottom-color: var(--orange);
+}
+
+.rating-E {
+  background-color: var(--red);
+}
+
+a .rating-E {
+  border-bottom-color: var(--red);
+}
+
+.rating-small {
+  width: 18px;
+  height: 18px;
+  line-height: 18px;
+  margin-top: -1px;
+  margin-bottom: -1px;
+  font-size: var(--smallFontSize);
+}
diff --git a/server/sonar-ui-common/components/ui/Rating.tsx b/server/sonar-ui-common/components/ui/Rating.tsx
new file mode 100644 (file)
index 0000000..69279b2
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import * as React from 'react';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { formatMeasure } from '../../helpers/measures';
+import './Rating.css';
+
+interface Props extends React.AriaAttributes {
+  className?: string;
+  muted?: boolean;
+  small?: boolean;
+  value: string | number | undefined;
+}
+
+export default function Rating({
+  className,
+  muted = false,
+  small = false,
+  value,
+  ...ariaAttrs
+}: Props) {
+  if (value === undefined) {
+    return (
+      <span aria-label={translate('metric.no_rating')} {...ariaAttrs}>
+        –
+      </span>
+    );
+  }
+  const formatted = formatMeasure(value, 'RATING');
+  return (
+    <span
+      aria-label={translateWithParameters('metric.has_rating_X', formatted)}
+      className={classNames(
+        'rating',
+        `rating-${formatted}`,
+        { 'rating-small': small, 'rating-muted': muted },
+        className
+      )}
+      {...ariaAttrs}>
+      {formatted}
+    </span>
+  );
+}
diff --git a/server/sonar-ui-common/components/ui/SizeRating.css b/server/sonar-ui-common/components/ui/SizeRating.css
new file mode 100644 (file)
index 0000000..2a08587
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.size-rating {
+  display: inline-block;
+  vertical-align: top;
+  width: var(--controlHeight);
+  height: var(--controlHeight);
+  line-height: var(--controlHeight);
+  border-radius: var(--controlHeight);
+  background-color: var(--blue);
+  color: #fff;
+  font-size: var(--smallFontSize);
+  text-align: center;
+  text-shadow: 0 0 1px rgba(0, 0, 0, 0.35);
+}
+
+.size-rating-small {
+  width: 18px;
+  height: 18px;
+  line-height: 18px;
+  margin-top: -1px;
+  margin-bottom: -1px;
+  font-size: 10px;
+}
+
+.size-rating-muted {
+  background-color: #bdbdbd;
+}
diff --git a/server/sonar-ui-common/components/ui/SizeRating.tsx b/server/sonar-ui-common/components/ui/SizeRating.tsx
new file mode 100644 (file)
index 0000000..b76a79a
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import { inRange } from 'lodash';
+import * as React from 'react';
+import './SizeRating.css';
+
+export interface Props {
+  muted?: boolean;
+  small?: boolean;
+  value: number | null | undefined;
+}
+
+export default function SizeRating({ small = false, muted = false, value }: Props) {
+  if (value == null) {
+    return <div className="size-rating size-rating-muted">&nbsp;</div>;
+  }
+
+  let letter;
+  if (inRange(value, 0, 1000)) {
+    letter = 'XS';
+  } else if (inRange(value, 1000, 10000)) {
+    letter = 'S';
+  } else if (inRange(value, 10000, 100000)) {
+    letter = 'M';
+  } else if (inRange(value, 100000, 500000)) {
+    letter = 'L';
+  } else if (value >= 500000) {
+    letter = 'XL';
+  }
+
+  const className = classNames('size-rating', {
+    'size-rating-small': small,
+    'size-rating-muted': muted,
+  });
+
+  return (
+    <div aria-hidden="true" className={className}>
+      {letter}
+    </div>
+  );
+}
diff --git a/server/sonar-ui-common/components/ui/__tests__/Alert-test.tsx b/server/sonar-ui-common/components/ui/__tests__/Alert-test.tsx
new file mode 100644 (file)
index 0000000..6355d5f
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { Alert, AlertProps } from '../Alert';
+
+it('should render properly', () => {
+  expect(shallowRender({ variant: 'error' })).toMatchSnapshot();
+});
+
+it('verification of all variants of alert', () => {
+  const variants: AlertProps['variant'][] = ['error', 'warning', 'success', 'info', 'loading'];
+  variants.forEach((variant) => {
+    const wrapper = shallowRender({ variant });
+    expect(wrapper.prop('variantInfo')).toMatchSnapshot();
+  });
+});
+
+it('should render inline alert', () => {
+  expect(shallowRender({ display: 'inline' }).find('Styled(div)[isInline=true]').exists()).toBe(
+    true
+  );
+});
+
+it('should render banner alert', () => {
+  expect(shallowRender({ display: 'banner' }).find('Styled(div)[isBanner=true]').exists()).toBe(
+    true
+  );
+});
+
+it('should render banner alert with correct css', () => {
+  expect(shallowRender({ display: 'banner' }).render()).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<AlertProps>) {
+  return shallow(
+    <Alert className="alert-test" id="error-message" variant="error" {...props}>
+      This is an error!
+    </Alert>
+  );
+}
diff --git a/server/sonar-ui-common/components/ui/__tests__/AutoEllipsis-test.tsx b/server/sonar-ui-common/components/ui/__tests__/AutoEllipsis-test.tsx
new file mode 100644 (file)
index 0000000..c01d263
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { mount, shallow } from 'enzyme';
+import * as React from 'react';
+import AutoEllipsis, { defaultShouldEllipsis } from '../AutoEllipsis';
+
+it('should render', () => {
+  const wrapper = shallow(
+    <AutoEllipsis maxWidth={5} useParent={false}>
+      <span className="medium">my test text</span>
+    </AutoEllipsis>
+  );
+
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render with text-ellipsis class', () => {
+  const wrapper = mount(
+    <AutoEllipsis customShouldEllipsis={() => true} maxWidth={5} useParent={false}>
+      <span className="medium">my test text</span>
+    </AutoEllipsis>
+  );
+
+  expect(wrapper.find('span').hasClass('medium')).toBe(true);
+  expect(wrapper.find('span').hasClass('text-ellipsis')).toBe(true);
+});
+
+const node5 = { clientWidth: 5, clientHeight: 5 } as any;
+const node10 = { clientWidth: 10, clientHeight: 10 } as any;
+const nodeParentSmaller = { ...node10, parentElement: node5 };
+const nodeParentBigger = { ...node5, parentElement: node10 };
+
+it('should correctly compute the auto-ellipsis', () => {
+  expect(defaultShouldEllipsis(node10, { maxWidth: 5, useParent: false })).toBe(true);
+  expect(defaultShouldEllipsis(node10, { maxHeight: 5, useParent: false })).toBe(true);
+  expect(defaultShouldEllipsis(node10, { maxWidth: 5, maxHeight: 5, useParent: false })).toBe(true);
+  expect(defaultShouldEllipsis(node10, { maxWidth: 5, maxHeight: 10, useParent: false })).toBe(
+    true
+  );
+  expect(defaultShouldEllipsis(node10, { maxWidth: 10, maxHeight: 5, useParent: false })).toBe(
+    true
+  );
+  expect(defaultShouldEllipsis(node10, { maxWidth: 10, useParent: false })).toBe(false);
+  expect(defaultShouldEllipsis(node10, { maxHeight: 10, useParent: false })).toBe(false);
+
+  expect(defaultShouldEllipsis(nodeParentSmaller, { maxWidth: 10, useParent: false })).toBe(false);
+  expect(defaultShouldEllipsis(nodeParentSmaller, { maxHeight: 10, useParent: false })).toBe(false);
+});
+
+it('should correctly compute the auto-ellipsis with a parent node', () => {
+  expect(defaultShouldEllipsis(nodeParentSmaller, {})).toBe(true);
+  expect(defaultShouldEllipsis(nodeParentSmaller, { maxWidth: 10 })).toBe(true);
+  expect(defaultShouldEllipsis(nodeParentSmaller, { maxHeight: 10 })).toBe(true);
+  expect(defaultShouldEllipsis(nodeParentSmaller, { maxWidth: 10, maxHeight: 10 })).toBe(false);
+  expect(defaultShouldEllipsis(nodeParentBigger, {})).toBe(false);
+  expect(defaultShouldEllipsis(nodeParentBigger, { maxWidth: 2 })).toBe(true);
+  expect(defaultShouldEllipsis(nodeParentBigger, { maxHeight: 2 })).toBe(true);
+});
diff --git a/server/sonar-ui-common/components/ui/__tests__/DeferredSpinner-test.tsx b/server/sonar-ui-common/components/ui/__tests__/DeferredSpinner-test.tsx
new file mode 100644 (file)
index 0000000..6404ff6
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { mount } from 'enzyme';
+import * as React from 'react';
+import DeferredSpinner from '../DeferredSpinner';
+
+jest.useFakeTimers();
+
+it('renders spinner after timeout', () => {
+  const spinner = mount(<DeferredSpinner />);
+  expect(spinner).toMatchSnapshot();
+  jest.runAllTimers();
+  spinner.update();
+  expect(spinner).toMatchSnapshot();
+});
+
+it('add custom className', () => {
+  const spinner = mount(<DeferredSpinner className="foo" />);
+  jest.runAllTimers();
+  spinner.update();
+  expect(spinner).toMatchSnapshot();
+});
+
+it('renders children before timeout', () => {
+  const spinner = mount(
+    <DeferredSpinner>
+      <div>foo</div>
+    </DeferredSpinner>
+  );
+  expect(spinner).toMatchSnapshot();
+  jest.runAllTimers();
+  spinner.update();
+  expect(spinner).toMatchSnapshot();
+});
+
+it('is controlled by loading prop', () => {
+  const spinner = mount(
+    <DeferredSpinner loading={false}>
+      <div>foo</div>
+    </DeferredSpinner>
+  );
+  expect(spinner).toMatchSnapshot();
+  spinner.setProps({ loading: true });
+  expect(spinner).toMatchSnapshot();
+  jest.runAllTimers();
+  spinner.update();
+  expect(spinner).toMatchSnapshot();
+  spinner.setProps({ loading: false });
+  spinner.update();
+  expect(spinner).toMatchSnapshot();
+});
+
+it('renders a placeholder while waiting', () => {
+  const spinner = mount(<DeferredSpinner placeholder={true} />);
+  expect(spinner).toMatchSnapshot();
+});
diff --git a/server/sonar-ui-common/components/ui/__tests__/FilesCounter-test.tsx b/server/sonar-ui-common/components/ui/__tests__/FilesCounter-test.tsx
new file mode 100644 (file)
index 0000000..90c65e2
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import FilesCounter from '../FilesCounter';
+
+it('should display x files on y total', () => {
+  expect(shallow(<FilesCounter current={12} total={123455} />)).toMatchSnapshot();
+});
+
+it('should display only total of files', () => {
+  expect(shallow(<FilesCounter current={undefined} total={123455} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-ui-common/components/ui/__tests__/GenericAvatar-test.tsx b/server/sonar-ui-common/components/ui/__tests__/GenericAvatar-test.tsx
new file mode 100644 (file)
index 0000000..468b063
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import GenericAvatar from '../GenericAvatar';
+
+it('should render properly', () => {
+  expect(shallow(<GenericAvatar name="foo" size={40} />)).toMatchSnapshot();
+  expect(shallow(<GenericAvatar name="foo" size={40} round={true} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-ui-common/components/ui/__tests__/Level-test.tsx b/server/sonar-ui-common/components/ui/__tests__/Level-test.tsx
new file mode 100644 (file)
index 0000000..6cc7904
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import Level, { LevelProps } from '../Level';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('default ok');
+  expect(shallowRender({ level: 'ERROR' })).toMatchSnapshot('default error');
+  expect(shallowRender({ muted: true, small: true })).toMatchSnapshot('muted and small');
+  expect(shallowRender({ 'aria-label': 'ARIA Label' })).toMatchSnapshot('with aria-label');
+  expect(shallowRender({ 'aria-labelledby': 'element-id' })).toMatchSnapshot(
+    'with aria-labelledby'
+  );
+});
+
+function shallowRender(props: Partial<LevelProps> = {}) {
+  return shallow(<Level className="foo" level="OK" {...props} />);
+}
diff --git a/server/sonar-ui-common/components/ui/__tests__/MandatoryFieldMarker-test.tsx b/server/sonar-ui-common/components/ui/__tests__/MandatoryFieldMarker-test.tsx
new file mode 100644 (file)
index 0000000..dc34539
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import MandatoryFieldMarker, { MandatoryFieldMarkerProps } from '../MandatoryFieldMarker';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('default');
+  expect(shallowRender({ className: 'foo-bar' })).toMatchSnapshot('with className');
+});
+
+function shallowRender(props: Partial<MandatoryFieldMarkerProps> = {}) {
+  return shallow<MandatoryFieldMarkerProps>(<MandatoryFieldMarker {...props} />);
+}
diff --git a/server/sonar-ui-common/components/ui/__tests__/MandatoryFieldsExplanation-test.tsx b/server/sonar-ui-common/components/ui/__tests__/MandatoryFieldsExplanation-test.tsx
new file mode 100644 (file)
index 0000000..1fd156d
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import MandatoryFieldsExplanation, {
+  MandatoryFieldsExplanationProps,
+} from '../MandatoryFieldsExplanation';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('default');
+  expect(shallowRender({ className: 'foo-bar' })).toMatchSnapshot('with className');
+});
+
+function shallowRender(props: Partial<MandatoryFieldsExplanationProps> = {}) {
+  return shallow<MandatoryFieldsExplanationProps>(<MandatoryFieldsExplanation {...props} />);
+}
diff --git a/server/sonar-ui-common/components/ui/__tests__/NavBar-test.tsx b/server/sonar-ui-common/components/ui/__tests__/NavBar-test.tsx
new file mode 100644 (file)
index 0000000..7f4e78e
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import NavBar from '../NavBar';
+
+it('should render correctly', () => {
+  const wrapper = shallowRender();
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render correctly with notif and not limited', () => {
+  const wrapper = shallowRender({ limited: false, notif: <div className="my-notifs" /> });
+  expect(wrapper).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<NavBar['props']> = {}) {
+  return shallow(
+    <NavBar height={42} {...props}>
+      <div className="my-navbar-content" />
+    </NavBar>
+  );
+}
diff --git a/server/sonar-ui-common/components/ui/__tests__/NewsBox-test.tsx b/server/sonar-ui-common/components/ui/__tests__/NewsBox-test.tsx
new file mode 100644 (file)
index 0000000..2569341
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { click } from '../../../helpers/testUtils';
+import NewsBox, { Props } from '../NewsBox';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should call onClose', () => {
+  const onClose = jest.fn();
+  const wrapper = shallowRender({ onClose });
+
+  click(wrapper.find('ClearButton'));
+  expect(onClose).toBeCalled();
+});
+
+function shallowRender(props: Partial<Props> = {}) {
+  return shallow(
+    <NewsBox onClose={jest.fn()} title="title" {...props}>
+      <div>description</div>
+    </NewsBox>
+  );
+}
diff --git a/server/sonar-ui-common/components/ui/__tests__/PageActions-test.tsx b/server/sonar-ui-common/components/ui/__tests__/PageActions-test.tsx
new file mode 100644 (file)
index 0000000..f144f88
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import PageActions, { Props } from '../PageActions';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+  expect(shallowRender({ total: 10 })).toMatchSnapshot();
+  expect(shallowRender({ current: 12, showShortcuts: false, total: 120 })).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<Props> = {}) {
+  return shallow(<PageActions showShortcuts={true} {...props} />);
+}
diff --git a/server/sonar-ui-common/components/ui/__tests__/Rating-test.tsx b/server/sonar-ui-common/components/ui/__tests__/Rating-test.tsx
new file mode 100644 (file)
index 0000000..55293f5
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import Rating from '../Rating';
+
+it('renders numeric value', () => {
+  expect(shallow(<Rating value={2} />)).toMatchSnapshot();
+});
+
+it('renders string value', () => {
+  expect(shallow(<Rating value="2.0" muted={true} small={true} />)).toMatchSnapshot();
+});
+
+it('renders undefined value', () => {
+  expect(shallow(<Rating value={undefined} muted={true} small={true} />)).toMatchSnapshot();
+});
+
+it('renders with a custom aria-label', () => {
+  expect(shallow(<Rating aria-label="custom" aria-hidden={false} value="2.0" />)).toMatchSnapshot();
+  expect(
+    shallow(<Rating aria-label="custom" aria-hidden={false} value={undefined} />)
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-ui-common/components/ui/__tests__/SizeRating-test.tsx b/server/sonar-ui-common/components/ui/__tests__/SizeRating-test.tsx
new file mode 100644 (file)
index 0000000..43eb09c
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import SizeRating, { Props } from '../SizeRating';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+  expect(shallowRender({ muted: true, small: true, value: 1000 })).toMatchSnapshot();
+  expect(shallowRender({ value: 10000 })).toMatchSnapshot();
+  expect(shallowRender({ value: 100000 })).toMatchSnapshot();
+  expect(shallowRender({ value: 500000 })).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<Props> = {}) {
+  return shallow(<SizeRating value={100} {...props} />);
+}
diff --git a/server/sonar-ui-common/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap b/server/sonar-ui-common/components/ui/__tests__/__snapshots__/Alert-test.tsx.snap
new file mode 100644 (file)
index 0000000..893212c
--- /dev/null
@@ -0,0 +1,207 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render banner alert with correct css 1`] = `
+.emotion-3 {
+  border: 1px solid;
+  border-radius: 2px;
+  margin-bottom: 8px;
+  border-color: #f4b1b0;
+  background-color: #f2dede;
+  color: #862422;
+  display: block;
+}
+
+.emotion-3:empty {
+  display: none;
+}
+
+.emotion-3 a,
+.emotion-3 .button-link {
+  border-color: #236a97;
+}
+
+.emotion-2 {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-align-items: stretch;
+  -webkit-box-align: stretch;
+  -ms-flex-align: stretch;
+  align-items: stretch;
+  min-width: 1080px;
+  max-width: 1320px;
+  margin-left: auto;
+  margin-right: auto;
+  padding-left: 20px;
+  padding-right: 20px;
+  box-sizing: border-box;
+}
+
+.emotion-0 {
+  -webkit-flex: 0 0 auto;
+  -ms-flex: 0 0 auto;
+  flex: 0 0 auto;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-pack: center;
+  -webkit-justify-content: center;
+  -ms-flex-pack: center;
+  justify-content: center;
+  -webkit-align-items: center;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+  width: calc(2 * 8px);
+  border-right: none;
+  border-color: #f4b1b0;
+}
+
+.emotion-1 {
+  -webkit-flex: 1 1 auto;
+  -ms-flex: 1 1 auto;
+  flex: 1 1 auto;
+  overflow: auto;
+  text-align: left;
+  padding: 8px calc(2 * 8px);
+}
+
+<div
+  class="alert alert-test emotion-3"
+  id="error-message"
+  role="alert"
+>
+  <div
+    class="emotion-2"
+  >
+    <div
+      aria-label="alert.tooltip.error"
+      class="emotion-0"
+    >
+      <svg
+        height="16"
+        space="preserve"
+        style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421"
+        version="1.1"
+        viewBox="0 0 16 16"
+        width="16"
+        xlink="http://www.w3.org/1999/xlink"
+      >
+        <path
+          d="M11.402 10.018q0-0.232-0.17-0.402l-1.616-1.616 1.616-1.616q0.17-0.17 0.17-0.402 0-0.241-0.17-0.411l-0.804-0.804q-0.17-0.17-0.411-0.17-0.232 0-0.402 0.17l-1.616 1.616-1.616-1.616q-0.17-0.17-0.402-0.17-0.241 0-0.411 0.17l-0.804 0.804q-0.17 0.17-0.17 0.411 0 0.232 0.17 0.402l1.616 1.616-1.616 1.616q-0.17 0.17-0.17 0.402 0 0.241 0.17 0.411l0.804 0.804q0.17 0.17 0.411 0.17 0.232 0 0.402-0.17l1.616-1.616 1.616 1.616q0.17 0.17 0.402 0.17 0.241 0 0.411-0.17l0.804-0.804q0.17-0.17 0.17-0.411zM14.857 8q0 1.866-0.92 3.442t-2.496 2.496-3.442 0.92-3.442-0.92-2.496-2.496-0.92-3.442 0.92-3.442 2.496-2.496 3.442-0.92 3.442 0.92 2.496 2.496 0.92 3.442z"
+          style="fill:#a4030f"
+        />
+      </svg>
+    </div>
+    <div
+      class="alert-content emotion-1"
+    >
+      This is an error!
+    </div>
+  </div>
+</div>
+`;
+
+exports[`should render properly 1`] = `
+<Styled(div)
+  className="alert alert-test"
+  id="error-message"
+  isInline={false}
+  role="alert"
+  variantInfo={
+    Object {
+      "backGroundColor": "#f2dede",
+      "borderColor": "#f4b1b0",
+      "color": "#862422",
+      "icon": <AlertErrorIcon
+        fill="#a4030f"
+      />,
+    }
+  }
+>
+  <Styled(div)
+    isBanner={false}
+  >
+    <Styled(div)
+      aria-label="alert.tooltip.error"
+      isBanner={false}
+      variantInfo={
+        Object {
+          "backGroundColor": "#f2dede",
+          "borderColor": "#f4b1b0",
+          "color": "#862422",
+          "icon": <AlertErrorIcon
+            fill="#a4030f"
+          />,
+        }
+      }
+    >
+      <AlertErrorIcon
+        fill="#a4030f"
+      />
+    </Styled(div)>
+    <Styled(div)
+      className="alert-content"
+    >
+      This is an error!
+    </Styled(div)>
+  </Styled(div)>
+</Styled(div)>
+`;
+
+exports[`verification of all variants of alert 1`] = `
+Object {
+  "backGroundColor": "#f2dede",
+  "borderColor": "#f4b1b0",
+  "color": "#862422",
+  "icon": <AlertErrorIcon
+    fill="#a4030f"
+  />,
+}
+`;
+
+exports[`verification of all variants of alert 2`] = `
+Object {
+  "backGroundColor": "#fcf8e3",
+  "borderColor": "#faebcc",
+  "color": "#6f4f17",
+  "icon": <AlertWarnIcon
+    fill="#db781a"
+  />,
+}
+`;
+
+exports[`verification of all variants of alert 3`] = `
+Object {
+  "backGroundColor": "#dff0d8",
+  "borderColor": "#d6e9c6",
+  "color": "#215821",
+  "icon": <AlertSuccessIcon
+    fill="#6d9867"
+  />,
+}
+`;
+
+exports[`verification of all variants of alert 4`] = `
+Object {
+  "backGroundColor": "#d9edf7",
+  "borderColor": "#b1dff3",
+  "color": "#0e516f",
+  "icon": <InfoIcon
+    fill="#0271b9"
+  />,
+}
+`;
+
+exports[`verification of all variants of alert 5`] = `
+Object {
+  "backGroundColor": "#d9edf7",
+  "borderColor": "#b1dff3",
+  "color": "#0e516f",
+  "icon": <DeferredSpinner
+    timeout={0}
+  />,
+}
+`;
diff --git a/server/sonar-ui-common/components/ui/__tests__/__snapshots__/AutoEllipsis-test.tsx.snap b/server/sonar-ui-common/components/ui/__tests__/__snapshots__/AutoEllipsis-test.tsx.snap
new file mode 100644 (file)
index 0000000..84212d4
--- /dev/null
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<span
+  className="medium"
+>
+  my test text
+</span>
+`;
diff --git a/server/sonar-ui-common/components/ui/__tests__/__snapshots__/DeferredSpinner-test.tsx.snap b/server/sonar-ui-common/components/ui/__tests__/__snapshots__/DeferredSpinner-test.tsx.snap
new file mode 100644 (file)
index 0000000..6822674
--- /dev/null
@@ -0,0 +1,87 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`add custom className 1`] = `
+<DeferredSpinner
+  className="foo"
+>
+  <i
+    className="deferred-spinner foo"
+  />
+</DeferredSpinner>
+`;
+
+exports[`is controlled by loading prop 1`] = `
+<DeferredSpinner
+  loading={false}
+>
+  <div>
+    foo
+  </div>
+</DeferredSpinner>
+`;
+
+exports[`is controlled by loading prop 2`] = `
+<DeferredSpinner
+  loading={true}
+>
+  <div>
+    foo
+  </div>
+</DeferredSpinner>
+`;
+
+exports[`is controlled by loading prop 3`] = `
+<DeferredSpinner
+  loading={true}
+>
+  <i
+    className="deferred-spinner"
+  />
+</DeferredSpinner>
+`;
+
+exports[`is controlled by loading prop 4`] = `
+<DeferredSpinner
+  loading={false}
+>
+  <div>
+    foo
+  </div>
+</DeferredSpinner>
+`;
+
+exports[`renders a placeholder while waiting 1`] = `
+<DeferredSpinner
+  placeholder={true}
+>
+  <i
+    className="deferred-spinner-placeholder"
+  />
+</DeferredSpinner>
+`;
+
+exports[`renders children before timeout 1`] = `
+<DeferredSpinner>
+  <div>
+    foo
+  </div>
+</DeferredSpinner>
+`;
+
+exports[`renders children before timeout 2`] = `
+<DeferredSpinner>
+  <i
+    className="deferred-spinner"
+  />
+</DeferredSpinner>
+`;
+
+exports[`renders spinner after timeout 1`] = `<DeferredSpinner />`;
+
+exports[`renders spinner after timeout 2`] = `
+<DeferredSpinner>
+  <i
+    className="deferred-spinner"
+  />
+</DeferredSpinner>
+`;
diff --git a/server/sonar-ui-common/components/ui/__tests__/__snapshots__/FilesCounter-test.tsx.snap b/server/sonar-ui-common/components/ui/__tests__/__snapshots__/FilesCounter-test.tsx.snap
new file mode 100644 (file)
index 0000000..bb01a61
--- /dev/null
@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display only total of files 1`] = `
+<span>
+  <strong>
+    123,455
+  </strong>
+   
+  component_measures.files
+</span>
+`;
+
+exports[`should display x files on y total 1`] = `
+<span>
+  <strong>
+    <span>
+      12
+       / 
+    </span>
+    123,455
+  </strong>
+   
+  component_measures.files
+</span>
+`;
diff --git a/server/sonar-ui-common/components/ui/__tests__/__snapshots__/GenericAvatar-test.tsx.snap b/server/sonar-ui-common/components/ui/__tests__/__snapshots__/GenericAvatar-test.tsx.snap
new file mode 100644 (file)
index 0000000..9c2bb0f
--- /dev/null
@@ -0,0 +1,47 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render properly 1`] = `
+<div
+  className="rounded"
+  style={
+    Object {
+      "backgroundColor": "#c68c01",
+      "borderRadius": undefined,
+      "color": "#fff",
+      "display": "inline-block",
+      "fontSize": 14,
+      "fontWeight": "normal",
+      "height": 40,
+      "lineHeight": "40px",
+      "textAlign": "center",
+      "verticalAlign": "top",
+      "width": 40,
+    }
+  }
+>
+  F
+</div>
+`;
+
+exports[`should render properly 2`] = `
+<div
+  className="rounded"
+  style={
+    Object {
+      "backgroundColor": "#c68c01",
+      "borderRadius": "50%",
+      "color": "#fff",
+      "display": "inline-block",
+      "fontSize": 14,
+      "fontWeight": "normal",
+      "height": 40,
+      "lineHeight": "40px",
+      "textAlign": "center",
+      "verticalAlign": "top",
+      "width": 40,
+    }
+  }
+>
+  F
+</div>
+`;
diff --git a/server/sonar-ui-common/components/ui/__tests__/__snapshots__/Level-test.tsx.snap b/server/sonar-ui-common/components/ui/__tests__/__snapshots__/Level-test.tsx.snap
new file mode 100644 (file)
index 0000000..089171f
--- /dev/null
@@ -0,0 +1,43 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: default error 1`] = `
+<span
+  className="foo level level-ERROR"
+>
+  ERROR
+</span>
+`;
+
+exports[`should render correctly: default ok 1`] = `
+<span
+  className="foo level level-OK"
+>
+  OK
+</span>
+`;
+
+exports[`should render correctly: muted and small 1`] = `
+<span
+  className="foo level level-OK level-small level-muted"
+>
+  OK
+</span>
+`;
+
+exports[`should render correctly: with aria-label 1`] = `
+<span
+  aria-label="ARIA Label"
+  className="foo level level-OK"
+>
+  OK
+</span>
+`;
+
+exports[`should render correctly: with aria-labelledby 1`] = `
+<span
+  aria-labelledby="element-id"
+  className="foo level level-OK"
+>
+  OK
+</span>
+`;
diff --git a/server/sonar-ui-common/components/ui/__tests__/__snapshots__/MandatoryFieldMarker-test.tsx.snap b/server/sonar-ui-common/components/ui/__tests__/__snapshots__/MandatoryFieldMarker-test.tsx.snap
new file mode 100644 (file)
index 0000000..1adb777
--- /dev/null
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: default 1`] = `
+<em
+  aria-label="field_required"
+  className="mandatory little-spacer-left"
+>
+  *
+</em>
+`;
+
+exports[`should render correctly: with className 1`] = `
+<em
+  aria-label="field_required"
+  className="mandatory little-spacer-left foo-bar"
+>
+  *
+</em>
+`;
diff --git a/server/sonar-ui-common/components/ui/__tests__/__snapshots__/MandatoryFieldsExplanation-test.tsx.snap b/server/sonar-ui-common/components/ui/__tests__/__snapshots__/MandatoryFieldsExplanation-test.tsx.snap
new file mode 100644 (file)
index 0000000..ca469b7
--- /dev/null
@@ -0,0 +1,43 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: default 1`] = `
+<div
+  aria-hidden={true}
+  className="text-muted"
+>
+  <FormattedMessage
+    defaultMessage="fields_marked_with_x_required"
+    id="fields_marked_with_x_required"
+    values={
+      Object {
+        "star": <em
+          className="mandatory"
+        >
+          *
+        </em>,
+      }
+    }
+  />
+</div>
+`;
+
+exports[`should render correctly: with className 1`] = `
+<div
+  aria-hidden={true}
+  className="text-muted foo-bar"
+>
+  <FormattedMessage
+    defaultMessage="fields_marked_with_x_required"
+    id="fields_marked_with_x_required"
+    values={
+      Object {
+        "star": <em
+          className="mandatory"
+        >
+          *
+        </em>,
+      }
+    }
+  />
+</div>
+`;
diff --git a/server/sonar-ui-common/components/ui/__tests__/__snapshots__/NavBar-test.tsx.snap b/server/sonar-ui-common/components/ui/__tests__/__snapshots__/NavBar-test.tsx.snap
new file mode 100644 (file)
index 0000000..a039d0e
--- /dev/null
@@ -0,0 +1,64 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<nav
+  className="navbar"
+  style={
+    Object {
+      "height": 42,
+      "top": undefined,
+    }
+  }
+>
+  <div
+    className="navbar-inner"
+    style={
+      Object {
+        "height": 42,
+        "left": 0,
+      }
+    }
+  >
+    <div
+      className="clearfix navbar-limited"
+    >
+      <div
+        className="my-navbar-content"
+      />
+    </div>
+  </div>
+</nav>
+`;
+
+exports[`should render correctly with notif and not limited 1`] = `
+<nav
+  className="navbar"
+  style={
+    Object {
+      "height": 42,
+      "top": undefined,
+    }
+  }
+>
+  <div
+    className="navbar-inner navbar-inner-with-notif"
+    style={
+      Object {
+        "height": 42,
+        "left": 0,
+      }
+    }
+  >
+    <div
+      className="clearfix"
+    >
+      <div
+        className="my-navbar-content"
+      />
+    </div>
+    <div
+      className="my-notifs"
+    />
+  </div>
+</nav>
+`;
diff --git a/server/sonar-ui-common/components/ui/__tests__/__snapshots__/NewsBox-test.tsx.snap b/server/sonar-ui-common/components/ui/__tests__/__snapshots__/NewsBox-test.tsx.snap
new file mode 100644 (file)
index 0000000..91e5889
--- /dev/null
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+  className="news-box"
+  role="alert"
+>
+  <div
+    className="news-box-header"
+  >
+    <div
+      className="display-flex-center"
+    >
+      <span
+        className="badge badge-info spacer-right"
+      >
+        new
+      </span>
+      <strong>
+        title
+      </strong>
+    </div>
+    <ClearButton
+      className="button-tiny"
+      iconProps={
+        Object {
+          "size": 12,
+          "thin": true,
+        }
+      }
+      onClick={[MockFunction]}
+    />
+  </div>
+  <div
+    className="big-spacer-top note"
+  >
+    <div>
+      description
+    </div>
+  </div>
+</div>
+`;
diff --git a/server/sonar-ui-common/components/ui/__tests__/__snapshots__/PageActions-test.tsx.snap b/server/sonar-ui-common/components/ui/__tests__/__snapshots__/PageActions-test.tsx.snap
new file mode 100644 (file)
index 0000000..bb220fb
--- /dev/null
@@ -0,0 +1,103 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+  className="page-actions display-flex-center"
+>
+  <span
+    className="note nowrap"
+  >
+    <span
+      className="big-spacer-right"
+    >
+      <span
+        className="shortcut-button little-spacer-right"
+      >
+        ↑
+      </span>
+      <span
+        className="shortcut-button little-spacer-right"
+      >
+        ↓
+      </span>
+      component_measures.to_select_files
+    </span>
+    <span>
+      <span
+        className="shortcut-button little-spacer-right"
+      >
+        ←
+      </span>
+      <span
+        className="shortcut-button little-spacer-right"
+      >
+        →
+      </span>
+      component_measures.to_navigate
+    </span>
+  </span>
+</div>
+`;
+
+exports[`should render correctly 2`] = `
+<div
+  className="page-actions display-flex-center"
+>
+  <span
+    className="note nowrap"
+  >
+    <span
+      className="big-spacer-right"
+    >
+      <span
+        className="shortcut-button little-spacer-right"
+      >
+        ↑
+      </span>
+      <span
+        className="shortcut-button little-spacer-right"
+      >
+        ↓
+      </span>
+      component_measures.to_select_files
+    </span>
+    <span>
+      <span
+        className="shortcut-button little-spacer-right"
+      >
+        ←
+      </span>
+      <span
+        className="shortcut-button little-spacer-right"
+      >
+        →
+      </span>
+      component_measures.to_navigate
+    </span>
+  </span>
+  <div
+    className="nowrap"
+  >
+    <FilesCounter
+      className="big-spacer-left"
+      total={10}
+    />
+  </div>
+</div>
+`;
+
+exports[`should render correctly 3`] = `
+<div
+  className="page-actions display-flex-center"
+>
+  <div
+    className="nowrap"
+  >
+    <FilesCounter
+      className="big-spacer-left"
+      current={12}
+      total={120}
+    />
+  </div>
+</div>
+`;
diff --git a/server/sonar-ui-common/components/ui/__tests__/__snapshots__/Rating-test.tsx.snap b/server/sonar-ui-common/components/ui/__tests__/__snapshots__/Rating-test.tsx.snap
new file mode 100644 (file)
index 0000000..9455ce9
--- /dev/null
@@ -0,0 +1,46 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders numeric value 1`] = `
+<span
+  aria-label="metric.has_rating_X.B"
+  className="rating rating-B"
+>
+  B
+</span>
+`;
+
+exports[`renders string value 1`] = `
+<span
+  aria-label="metric.has_rating_X.B"
+  className="rating rating-B rating-small rating-muted"
+>
+  B
+</span>
+`;
+
+exports[`renders undefined value 1`] = `
+<span
+  aria-label="metric.no_rating"
+>
+  –
+</span>
+`;
+
+exports[`renders with a custom aria-label 1`] = `
+<span
+  aria-hidden={false}
+  aria-label="custom"
+  className="rating rating-B"
+>
+  B
+</span>
+`;
+
+exports[`renders with a custom aria-label 2`] = `
+<span
+  aria-hidden={false}
+  aria-label="custom"
+>
+  –
+</span>
+`;
diff --git a/server/sonar-ui-common/components/ui/__tests__/__snapshots__/SizeRating-test.tsx.snap b/server/sonar-ui-common/components/ui/__tests__/__snapshots__/SizeRating-test.tsx.snap
new file mode 100644 (file)
index 0000000..a35517c
--- /dev/null
@@ -0,0 +1,46 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+  aria-hidden="true"
+  className="size-rating"
+>
+  XS
+</div>
+`;
+
+exports[`should render correctly 2`] = `
+<div
+  aria-hidden="true"
+  className="size-rating size-rating-small size-rating-muted"
+>
+  S
+</div>
+`;
+
+exports[`should render correctly 3`] = `
+<div
+  aria-hidden="true"
+  className="size-rating"
+>
+  M
+</div>
+`;
+
+exports[`should render correctly 4`] = `
+<div
+  aria-hidden="true"
+  className="size-rating"
+>
+  L
+</div>
+`;
+
+exports[`should render correctly 5`] = `
+<div
+  aria-hidden="true"
+  className="size-rating"
+>
+  XL
+</div>
+`;
diff --git a/server/sonar-ui-common/components/ui/__tests__/__snapshots__/popups-test.tsx.snap b/server/sonar-ui-common/components/ui/__tests__/__snapshots__/popups-test.tsx.snap
new file mode 100644 (file)
index 0000000..87b7811
--- /dev/null
@@ -0,0 +1,79 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Popup should render Popup 1`] = `
+<ClickEventBoundary>
+  <div
+    className="popup is-left-top foo"
+    style={
+      Object {
+        "left": -5,
+      }
+    }
+  >
+    <PopupArrow
+      style={
+        Object {
+          "top": -5,
+        }
+      }
+    />
+  </div>
+</ClickEventBoundary>
+`;
+
+exports[`Popup should render PopupArrow 1`] = `
+<div
+  className="popup-arrow"
+  style={
+    Object {
+      "left": -5,
+    }
+  }
+/>
+`;
+
+exports[`PortalPopup should render correctly with overlay 1`] = `
+<Fragment>
+  <div
+    id="popup-trigger"
+  />
+  <PortalWrapper>
+    <WithTheme(ScreenPositionFixer)
+      ready={true}
+    >
+      <Component />
+    </WithTheme(ScreenPositionFixer)>
+  </PortalWrapper>
+</Fragment>
+`;
+
+exports[`PortalPopup should render correctly with overlay 2`] = `
+<Popup
+  arrowStyle={
+    Object {
+      "marginLeft": 0,
+    }
+  }
+  placement="bottom"
+  style={
+    Object {
+      "height": 10,
+      "left": 0,
+      "top": 0,
+      "width": 10,
+    }
+  }
+>
+  <span
+    id="overlay"
+  />
+</Popup>
+`;
+
+exports[`PortalPopup should render correctly without overlay 1`] = `
+<Fragment>
+  <div
+    id="popup-trigger"
+  />
+</Fragment>
+`;
diff --git a/server/sonar-ui-common/components/ui/__tests__/popups-test.tsx b/server/sonar-ui-common/components/ui/__tests__/popups-test.tsx
new file mode 100644 (file)
index 0000000..777c100
--- /dev/null
@@ -0,0 +1,127 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { findDOMNode } from 'react-dom';
+import ScreenPositionFixer from '../../controls/ScreenPositionFixer';
+import { Popup, PopupArrow, PopupPlacement, PortalPopup } from '../popups';
+
+jest.mock('react-dom', () => ({
+  ...jest.requireActual('react-dom'),
+  findDOMNode: jest.fn().mockReturnValue(undefined),
+}));
+
+describe('Popup', () => {
+  it('should render Popup', () => {
+    expect(
+      shallow(
+        <Popup
+          arrowStyle={{ top: -5 }}
+          className="foo"
+          placement={PopupPlacement.LeftTop}
+          style={{ left: -5 }}
+        />
+      )
+    ).toMatchSnapshot();
+  });
+
+  it('should render PopupArrow', () => {
+    expect(shallow(<PopupArrow style={{ left: -5 }} />)).toMatchSnapshot();
+  });
+});
+
+describe('PortalPopup', () => {
+  it('should render correctly without overlay', () => {
+    expect(shallowRender({ overlay: undefined })).toMatchSnapshot();
+  });
+
+  it('should render correctly with overlay', () => {
+    const wrapper = shallowRender();
+    wrapper.setState({ left: 0, top: 0, width: 10, height: 10 });
+    expect(wrapper).toMatchSnapshot();
+    expect(wrapper.find(ScreenPositionFixer).dive().dive().dive()).toMatchSnapshot();
+  });
+
+  it('should correctly compute the popup positioning', () => {
+    const fakeDomNode = document.createElement('div');
+    fakeDomNode.getBoundingClientRect = jest
+      .fn()
+      .mockReturnValue({ left: 10, top: 10, width: 10, height: 10 });
+    (findDOMNode as jest.Mock).mockReturnValue(fakeDomNode);
+    const wrapper = shallowRender();
+    const getPlacementSpy = jest.spyOn(wrapper.instance(), 'getPlacement');
+
+    wrapper.instance().popupNode = {
+      current: {
+        getBoundingClientRect: jest.fn().mockReturnValue({ width: 8, height: 8 }),
+      } as any,
+    };
+
+    wrapper.instance().positionPopup();
+    expect(wrapper.state()).toEqual(expect.objectContaining({ left: 11, top: 20 }));
+
+    getPlacementSpy.mockReturnValue(PopupPlacement.BottomLeft);
+    wrapper.instance().positionPopup();
+    expect(wrapper.state()).toEqual(expect.objectContaining({ left: 10, top: 20 }));
+
+    getPlacementSpy.mockReturnValue(PopupPlacement.BottomRight);
+    wrapper.instance().positionPopup();
+    expect(wrapper.state()).toEqual(expect.objectContaining({ left: 12, top: 20 }));
+
+    getPlacementSpy.mockReturnValue(PopupPlacement.LeftTop);
+    wrapper.instance().positionPopup();
+    expect(wrapper.state()).toEqual(expect.objectContaining({ left: 2, top: 10 }));
+
+    getPlacementSpy.mockReturnValue(PopupPlacement.RightBottom);
+    wrapper.instance().positionPopup();
+    expect(wrapper.state()).toEqual(expect.objectContaining({ left: 20, top: 12 }));
+
+    getPlacementSpy.mockReturnValue(PopupPlacement.RightTop);
+    wrapper.instance().positionPopup();
+    expect(wrapper.state()).toEqual(expect.objectContaining({ left: 20, top: 10 }));
+
+    getPlacementSpy.mockReturnValue(PopupPlacement.TopLeft);
+    wrapper.instance().positionPopup();
+    expect(wrapper.state()).toEqual(expect.objectContaining({ left: 10, top: 2 }));
+  });
+
+  it('should correctly compute the popup arrow positioning', () => {
+    const wrapper = shallowRender({ arrowOffset: -2 });
+    const getPlacementSpy = jest.spyOn(wrapper.instance(), 'getPlacement');
+
+    expect(
+      wrapper.instance().adjustArrowPosition(PopupPlacement.BottomLeft, { leftFix: 10, topFix: 10 })
+    ).toEqual({ marginLeft: -12 });
+
+    expect(
+      wrapper
+        .instance()
+        .adjustArrowPosition(PopupPlacement.RightBottom, { leftFix: 10, topFix: 10 })
+    ).toEqual({ marginTop: -12 });
+  });
+
+  function shallowRender(props: Partial<PortalPopup['props']> = {}) {
+    return shallow<PortalPopup>(
+      <PortalPopup overlay={<span id="overlay" />} {...props}>
+        <div id="popup-trigger" />
+      </PortalPopup>
+    );
+  }
+});
diff --git a/server/sonar-ui-common/components/ui/popups.css b/server/sonar-ui-common/components/ui/popups.css
new file mode 100644 (file)
index 0000000..7c00bd4
--- /dev/null
@@ -0,0 +1,288 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+.popup {
+  position: absolute;
+  z-index: var(--popupZIndex);
+  margin-top: -16px;
+  margin-left: 8px;
+  padding: var(--gridSize);
+  border: 1px solid var(--barBorderColor);
+  border-radius: 3px;
+  box-sizing: border-box;
+  background-color: #ffffff;
+  box-shadow: var(--defaultShadow);
+  cursor: default;
+}
+
+.popup.no-padding {
+  padding: 0;
+}
+
+/* #region .popup-arrow */
+.popup-arrow,
+.popup-arrow:after {
+  position: absolute;
+  display: block;
+  width: 0;
+  height: 0;
+  border: 6px solid transparent;
+}
+
+.popup-arrow {
+  top: 15px;
+  left: -6px;
+  border-left-width: 0;
+  border-right-color: var(--barBorderColor);
+}
+
+.popup-arrow:after {
+  content: ' ';
+  left: 1px;
+  bottom: -6px;
+  border-left-width: 0;
+  border-right-color: #ffffff;
+}
+/* #endregion */
+
+/* #region .popup.is-bottom */
+.popup.is-bottom {
+  top: 100%;
+  left: 0;
+  margin: 0;
+  margin-left: 50%;
+  transform: translate(-50%, 6px);
+}
+
+.popup.is-bottom .popup-arrow {
+  top: -6px;
+  left: calc(50% - 6px);
+  border-left-width: 6px;
+  border-top-width: 0;
+  border-right-color: transparent;
+  border-bottom-color: var(--barBorderColor);
+}
+
+.popup.is-bottom .popup-arrow.is-left {
+  left: 8px;
+}
+
+.popup.is-bottom .popup-arrow:after {
+  left: -6px;
+  bottom: -7px;
+  border-left-width: 6px;
+  border-top-width: 0;
+  border-right-color: transparent;
+  border-bottom-color: #ffffff;
+}
+/* #endregion */
+
+/* #region .popup.is-bottom-right */
+.popup.is-bottom-right {
+  top: 100%;
+  right: 0;
+  margin: 0;
+
+  /* TODO Update like .is-bottom-left, currently it's */
+  transform: translateY(6px);
+}
+
+.popup.is-bottom-right .popup-arrow {
+  top: -6px;
+  left: auto;
+  right: 8px;
+  border-left-width: 6px;
+  border-top-width: 0;
+  border-right-color: transparent;
+  border-bottom-color: var(--barBorderColor);
+}
+
+.popup.is-bottom-right .popup-arrow:after {
+  left: -6px;
+  bottom: -7px;
+  border-left-width: 6px;
+  border-top-width: 0;
+  border-right-color: transparent;
+  border-bottom-color: #ffffff;
+}
+/* #endregion */
+
+/* #region .popup.is-bottom-left */
+.popup.is-bottom-left {
+  top: 100%;
+  left: 0;
+  margin: 0;
+  transform: translate(-8px, 6px);
+}
+
+.popup.is-bottom-left .popup-arrow {
+  top: -6px;
+  right: auto;
+  left: 8px;
+  border-left-width: 6px;
+  border-top-width: 0;
+  border-right-color: transparent;
+  border-bottom-color: var(--barBorderColor);
+}
+
+.popup.is-bottom-left .popup-arrow:after {
+  left: -6px;
+  bottom: -7px;
+  border-left-width: 6px;
+  border-top-width: 0;
+  border-right-color: transparent;
+  border-bottom-color: #ffffff;
+}
+/* #endregion */
+
+/* #region .popup.is-left-top */
+.popup.is-left-top {
+  top: -4px;
+  right: 100%;
+  margin: 0;
+  transform: translateX(-6px);
+}
+
+.popup.is-left-top .popup-arrow {
+  right: -6px;
+  left: auto;
+  top: 8px;
+  border-right-width: 0;
+  border-left-width: 6px;
+  border-left-color: var(--barBorderColor);
+  border-right-color: transparent;
+}
+
+.popup.is-left-top .popup-arrow:after {
+  top: -6px;
+  left: -7px;
+  border-right-width: 0;
+  border-left-width: 6px;
+  border-left-color: #ffffff;
+  border-right-color: transparent;
+}
+/* #endregion */
+
+/* #region .popup.is-right-top */
+.popup.is-right-top {
+  top: -4px;
+  left: 100%;
+  margin: 0;
+  transform: translateX(6px);
+}
+
+.popup.is-right-top .popup-arrow {
+  left: -6px;
+  right: auto;
+  top: 8px;
+  border-left-width: 0;
+  border-right-width: 6px;
+  border-right-color: var(--barBorderColor);
+  border-left-color: transparent;
+}
+
+.popup.is-right-top .popup-arrow:after {
+  top: -6px;
+  right: -7px;
+  border-left-width: 0;
+  border-right-width: 6px;
+  border-right-color: #ffffff;
+  border-left-color: transparent;
+}
+/* #endregion */
+
+/* #region .popup.is-right-bottom */
+.popup.is-right-bottom {
+  bottom: 4px;
+  left: 100%;
+  margin: 0;
+  transform: translateX(6px);
+}
+
+.popup.is-right-bottom .popup-arrow {
+  left: -6px;
+  right: auto;
+  top: calc(100% - 15px);
+  border-left-width: 0;
+  border-right-width: 6px;
+  border-right-color: var(--barBorderColor);
+  border-left-color: transparent;
+}
+
+.popup.is-right-bottom .popup-arrow:after {
+  top: -6px;
+  right: -7px;
+  border-left-width: 0;
+  border-right-width: 6px;
+  border-right-color: #ffffff;
+  border-left-color: transparent;
+}
+/* #endregion */
+
+/* #region .popup.is-top-left */
+.popup.is-top-left {
+  bottom: calc(100% + 8px);
+  left: 0;
+  margin: 0;
+  transform: translateX(-8px);
+}
+
+.popup.is-top-left .popup-arrow {
+  bottom: -6px;
+  top: auto;
+  left: 8px;
+  border-color: var(--barBorderColor) transparent transparent;
+  border-width: 6px 6px 0 6px;
+}
+
+.popup.is-top-left .popup-arrow:after {
+  left: -6px;
+  top: -7px;
+  border-width: 6px 6px 0 6px;
+  border-color: #fff transparent transparent;
+}
+/* #endregion */
+
+/* #region .popup & .menu or .multi-select */
+.popup:not(.no-padding) > .menu,
+.popup:not(.no-padding) > .multi-select {
+  margin: calc(-1 * var(--gridSize));
+}
+/* #endregion */
+
+/* #region .popup-portal override css placement */
+.popup-portal .popup.is-bottom {
+  top: unset;
+  left: unset;
+  transform: unset;
+  margin: 0;
+}
+
+.popup-portal .popup.is-bottom-left,
+.popup-portal .popup.is-bottom-right,
+.popup-portal .popup.is-top-left,
+.popup-portal .popup.is-left-top,
+.popup-portal .popup.is-right-top,
+.popup-portal .popup.is-right-bottom {
+  top: unset;
+  right: unset;
+  bottom: unset;
+  left: unset;
+}
+/* #endregion */
diff --git a/server/sonar-ui-common/components/ui/popups.tsx b/server/sonar-ui-common/components/ui/popups.tsx
new file mode 100644 (file)
index 0000000..2cb5a8b
--- /dev/null
@@ -0,0 +1,288 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as classNames from 'classnames';
+import { throttle } from 'lodash';
+import * as React from 'react';
+import { createPortal, findDOMNode } from 'react-dom';
+import ClickEventBoundary from '../controls/ClickEventBoundary';
+import ScreenPositionFixer from '../controls/ScreenPositionFixer';
+import './popups.css';
+
+/**
+ * Positioning rules:
+ * - Bottom = below the block, horizontally centered
+ * - BottomLeft = below the block, horizontally left-aligned
+ * - BottomRight = below the block, horizontally right-aligned
+ * - LeftTop = on the left-side of the block, vertically top-aligned
+ * - RightTop = on the right-side of the block, vertically top-aligned
+ * - RightBottom = on the right-side of the block, vetically bottom-aligned
+ * - TopLeft = above the block, horizontally left-aligned
+ */
+export enum PopupPlacement {
+  Bottom = 'bottom',
+  BottomLeft = 'bottom-left',
+  BottomRight = 'bottom-right',
+  LeftTop = 'left-top',
+  RightTop = 'right-top',
+  RightBottom = 'right-bottom',
+  TopLeft = 'top-left',
+}
+
+interface PopupProps {
+  arrowStyle?: React.CSSProperties;
+  children?: React.ReactNode;
+  className?: string;
+  noPadding?: boolean;
+  placement?: PopupPlacement;
+  style?: React.CSSProperties;
+}
+
+function PopupBase(props: PopupProps, ref: React.Ref<HTMLDivElement>) {
+  const { placement = PopupPlacement.Bottom } = props;
+  return (
+    <ClickEventBoundary>
+      <div
+        className={classNames(
+          'popup',
+          `is-${placement}`,
+          { 'no-padding': props.noPadding },
+          props.className
+        )}
+        ref={ref || React.createRef()}
+        style={props.style}>
+        {props.children}
+        <PopupArrow style={props.arrowStyle} />
+      </div>
+    </ClickEventBoundary>
+  );
+}
+
+const PopupWithRef = React.forwardRef(PopupBase);
+PopupWithRef.displayName = 'Popup';
+
+export const Popup = PopupWithRef;
+
+interface PopupArrowProps {
+  style?: React.CSSProperties;
+}
+
+export function PopupArrow(props: PopupArrowProps) {
+  return <div className="popup-arrow" style={props.style} />;
+}
+
+interface PortalPopupProps extends Omit<PopupProps, 'arrowStyle' | 'style'> {
+  arrowOffset?: number;
+  children: React.ReactNode;
+  overlay: React.ReactNode;
+}
+
+interface Measurements {
+  height: number;
+  left: number;
+  top: number;
+  width: number;
+}
+
+type State = Partial<Measurements>;
+
+function isMeasured(state: State): state is Measurements {
+  return state.height !== undefined;
+}
+
+export class PortalPopup extends React.Component<PortalPopupProps, State> {
+  mounted = false;
+  popupNode = React.createRef<HTMLDivElement>();
+  throttledPositionTooltip: () => void;
+
+  constructor(props: PortalPopupProps) {
+    super(props);
+    this.state = {};
+    this.throttledPositionTooltip = throttle(this.positionPopup, 10);
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+    this.positionPopup();
+    this.addEventListeners();
+  }
+
+  componentDidUpdate(prevProps: PortalPopupProps) {
+    if (this.props.placement !== prevProps.placement || this.props.overlay !== prevProps.overlay) {
+      this.positionPopup();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+    this.removeEventListeners();
+  }
+
+  addEventListeners = () => {
+    window.addEventListener('resize', this.throttledPositionTooltip);
+    window.addEventListener('scroll', this.throttledPositionTooltip);
+  };
+
+  removeEventListeners = () => {
+    window.removeEventListener('resize', this.throttledPositionTooltip);
+    window.removeEventListener('scroll', this.throttledPositionTooltip);
+  };
+
+  getPlacement = (): PopupPlacement => {
+    return this.props.placement || PopupPlacement.Bottom;
+  };
+
+  adjustArrowPosition = (
+    placement: PopupPlacement,
+    { leftFix, topFix }: { leftFix: number; topFix: number }
+  ) => {
+    const { arrowOffset = 0 } = this.props;
+    switch (placement) {
+      case PopupPlacement.Bottom:
+      case PopupPlacement.BottomLeft:
+      case PopupPlacement.BottomRight:
+      case PopupPlacement.TopLeft:
+        return { marginLeft: -leftFix + arrowOffset };
+      default:
+        return { marginTop: -topFix + arrowOffset };
+    }
+  };
+
+  positionPopup = () => {
+    // `findDOMNode(this)` will search for the DOM node for the current component
+    // first it will find a React.Fragment (see `render`),
+    // so it will get the DOM node of the first child, i.e. DOM node of `this.props.children`
+    // docs: https://reactjs.org/docs/refs-and-the-dom.html#exposing-dom-refs-to-parent-components
+
+    // eslint-disable-next-line react/no-find-dom-node
+    const toggleNode = findDOMNode(this);
+
+    if (toggleNode && toggleNode instanceof Element && this.popupNode.current) {
+      const toggleRect = toggleNode.getBoundingClientRect();
+      const { width, height } = this.popupNode.current.getBoundingClientRect();
+      let left = 0;
+      let top = 0;
+
+      switch (this.getPlacement()) {
+        case PopupPlacement.Bottom:
+          left = toggleRect.left + toggleRect.width / 2 - width / 2;
+          top = toggleRect.top + toggleRect.height;
+          break;
+        case PopupPlacement.BottomLeft:
+          left = toggleRect.left;
+          top = toggleRect.top + toggleRect.height;
+          break;
+        case PopupPlacement.BottomRight:
+          left = toggleRect.left + toggleRect.width - width;
+          top = toggleRect.top + toggleRect.height;
+          break;
+        case PopupPlacement.LeftTop:
+          left = toggleRect.left - width;
+          top = toggleRect.top;
+          break;
+        case PopupPlacement.RightTop:
+          left = toggleRect.left + toggleRect.width;
+          top = toggleRect.top;
+          break;
+        case PopupPlacement.RightBottom:
+          left = toggleRect.left + toggleRect.width;
+          top = toggleRect.top + toggleRect.height - height;
+          break;
+        case PopupPlacement.TopLeft:
+          left = toggleRect.left;
+          top = toggleRect.top - height;
+          break;
+      }
+
+      // save width and height (and later set in `render`) to avoid resizing the popup element,
+      // when it's placed close to the window edge
+      this.setState({
+        left: window.pageXOffset + left,
+        top: window.pageYOffset + top,
+        width,
+        height,
+      });
+    }
+  };
+
+  renderActual = ({ leftFix = 0, topFix = 0 }) => {
+    const { className, overlay, noPadding } = this.props;
+    const placement = this.getPlacement();
+    let arrowStyle;
+    let style;
+    if (isMeasured(this.state)) {
+      style = {
+        left: this.state.left + leftFix,
+        top: this.state.top + topFix,
+        width: this.state.width,
+        height: this.state.height,
+      };
+      arrowStyle = this.adjustArrowPosition(placement, { leftFix, topFix });
+    }
+
+    return (
+      <Popup
+        arrowStyle={arrowStyle}
+        className={className}
+        noPadding={noPadding}
+        placement={placement}
+        ref={this.popupNode}
+        style={style}>
+        {overlay}
+      </Popup>
+    );
+  };
+
+  render() {
+    return (
+      <>
+        {this.props.children}
+        {this.props.overlay && (
+          <PortalWrapper>
+            <ScreenPositionFixer ready={isMeasured(this.state)}>
+              {this.renderActual}
+            </ScreenPositionFixer>
+          </PortalWrapper>
+        )}
+      </>
+    );
+  }
+}
+
+class PortalWrapper extends React.Component {
+  el: HTMLElement;
+
+  constructor(props: {}) {
+    super(props);
+    this.el = document.createElement('div');
+    this.el.classList.add('popup-portal');
+  }
+
+  componentDidMount() {
+    document.body.appendChild(this.el);
+  }
+
+  componentWillUnmount() {
+    document.body.removeChild(this.el);
+  }
+
+  render() {
+    return createPortal(this.props.children, this.el);
+  }
+}
diff --git a/server/sonar-ui-common/components/ui/update-center/MetaData.css b/server/sonar-ui-common/components/ui/update-center/MetaData.css
new file mode 100644 (file)
index 0000000..582d663
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+.update-center-meta-data {
+  margin: 16px 0;
+  padding: 16px 16px 8px 16px;
+  background: #f9f9fb;
+  border: 1px solid #e6e6e6;
+  border-radius: 3px;
+}
+
+.update-center-meta-data a svg {
+  margin-right: 8px;
+}
+
+.update-center-meta-data-header {
+  border-bottom: 1px solid #cfd3d7;
+  padding-bottom: 16px;
+}
+
+.update-center-meta-data-header,
+.update-center-meta-data-version-release-info,
+.update-center-meta-data-version-links {
+  display: flex;
+}
+
+.update-center-meta-data-header > * + *,
+.update-center-meta-data-version-release-info > * + * {
+  margin-left: 16px;
+}
+
+.update-center-meta-data-header > * + * {
+  padding-left: 16px;
+  border-left: 1px solid #cfd3d7;
+}
+
+.update-center-meta-data-versions {
+  margin-top: 16px;
+}
+
+.update-center-meta-data-versions-show-more {
+  font-size: 14px;
+  float: right;
+  color: #51575a;
+  border-color: #7b8184;
+  border-width: 0 0 1px 0;
+  padding-left: 0;
+  padding-right: 0;
+  background: transparent;
+  cursor: pointer;
+}
+
+.update-center-meta-data-versions-show-more:hover {
+  color: #2d3032;
+  border-color: #2d3032;
+}
+
+.update-center-meta-data-version {
+  margin-bottom: 16px;
+}
+
+.update-center-meta-data-version + .update-center-meta-data-version {
+  padding-top: 8px;
+  border-top: 1px dashed #cfd3d7;
+}
+
+.update-center-meta-data-version-version {
+  font-weight: bold;
+  font-size: 18px;
+}
+
+.update-center-meta-data-version-release-info {
+  margin-top: 8px;
+  font-style: italic;
+}
+
+.update-center-meta-data-version-release-description {
+  margin-top: 8px;
+}
+
+.update-center-meta-data-version-download > a,
+.update-center-meta-data-version-release-notes > a {
+  display: inline-block;
+  margin: 8px 16px 0 0;
+}
diff --git a/server/sonar-ui-common/components/ui/update-center/MetaData.tsx b/server/sonar-ui-common/components/ui/update-center/MetaData.tsx
new file mode 100644 (file)
index 0000000..568a7fb
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import * as React from 'react';
+import './MetaData.css';
+import MetaDataVersions from './MetaDataVersions';
+import { MetaDataInformation } from './update-center-metadata';
+import { isSuccessStatus } from '../../../helpers/request';
+
+interface Props {
+  updateCenterKey?: string;
+}
+
+interface State {
+  data?: MetaDataInformation;
+}
+
+export default class MetaData extends React.Component<Props, State> {
+  mounted = false;
+  state: State = {};
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchData();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.updateCenterKey !== this.props.updateCenterKey) {
+      this.fetchData();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchData() {
+    const { updateCenterKey } = this.props;
+
+    if (updateCenterKey) {
+      window
+        .fetch(`https://update.sonarsource.org/${updateCenterKey}.json`)
+        .then((response: Response) => {
+          if (isSuccessStatus(response.status)) {
+            return response.json();
+          } else {
+            return Promise.reject(response);
+          }
+        })
+        .then((data) => {
+          if (this.mounted) {
+            this.setState({ data });
+          }
+        })
+        .catch(() => {
+          if (this.mounted) {
+            this.setState({ data: undefined });
+          }
+        });
+    } else {
+      this.setState({ data: undefined });
+    }
+  }
+
+  render() {
+    const { data } = this.state;
+
+    if (!data) {
+      return null;
+    }
+
+    const { isSonarSourceCommercial, issueTrackerURL, license, organization, versions } = data;
+
+    let vendor;
+    if (organization) {
+      vendor = organization.name;
+      if (organization.url) {
+        vendor = (
+          <a href={organization.url} rel="noopener noreferrer" target="_blank">
+            {vendor}
+          </a>
+        );
+      }
+    }
+
+    return (
+      <div className="update-center-meta-data">
+        <div className="update-center-meta-data-header">
+          {vendor && <span className="update-center-meta-data-vendor">By {vendor}</span>}
+          {license && <span className="update-center-meta-data-license">{license}</span>}
+          {issueTrackerURL && (
+            <span className="update-center-meta-data-issue-tracker">
+              <a href={issueTrackerURL} rel="noopener noreferrer" target="_blank">
+                Issue Tracker
+              </a>
+            </span>
+          )}
+          {isSonarSourceCommercial && (
+            <span className="update-center-meta-data-supported">Supported by SonarSource</span>
+          )}
+        </div>
+        {versions && versions.length > 0 && <MetaDataVersions versions={versions} />}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/ui/update-center/MetaDataVersion.tsx b/server/sonar-ui-common/components/ui/update-center/MetaDataVersion.tsx
new file mode 100644 (file)
index 0000000..f9f2155
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import classNames from 'classnames';
+import * as React from 'react';
+import { AdvancedDownloadUrl, MetaDataVersionInformation } from './update-center-metadata';
+
+export interface MetaDataVersionProps {
+  versionInformation: MetaDataVersionInformation;
+}
+
+export default function MetaDataVersion(props: MetaDataVersionProps) {
+  const {
+    versionInformation: {
+      archived,
+      changeLogUrl,
+      compatibility,
+      date,
+      description,
+      downloadURL,
+      version,
+    },
+  } = props;
+
+  const fallbackLabel = 'Download';
+
+  const advancedDownloadUrls = isAdvancedDownloadUrlArray(downloadURL)
+    ? downloadURL.map((url) => ({ ...url, label: url.label || fallbackLabel }))
+    : [{ label: fallbackLabel, url: downloadURL }];
+
+  return (
+    <div
+      className={classNames('update-center-meta-data-version', {
+        'update-center-meta-data-version-archived': archived,
+      })}>
+      <div className="update-center-meta-data-version-version">{version}</div>
+
+      <div className="update-center-meta-data-version-release-info">
+        {date && <time className="update-center-meta-data-version-release-date">{date}</time>}
+
+        {compatibility && (
+          <span className="update-center-meta-data-version-compatibility">{compatibility}</span>
+        )}
+      </div>
+
+      {description && (
+        <div className="update-center-meta-data-version-release-description">{description}</div>
+      )}
+
+      {(advancedDownloadUrls.length > 0 || changeLogUrl) && (
+        <div className="update-center-meta-data-version-release-links">
+          {advancedDownloadUrls.length > 0 &&
+            advancedDownloadUrls.map(
+              (advancedDownloadUrl, i) =>
+                advancedDownloadUrl.url && (
+                  // eslint-disable-next-line react/no-array-index-key
+                  <span className="update-center-meta-data-version-download" key={i}>
+                    <a href={advancedDownloadUrl.url} rel="noopener noreferrer" target="_blank">
+                      {advancedDownloadUrl.label}
+                    </a>
+                  </span>
+                )
+            )}
+
+          {changeLogUrl && (
+            <span className="update-center-meta-data-version-release-notes">
+              <a href={changeLogUrl} rel="noopener noreferrer" target="_blank">
+                Release notes
+              </a>
+            </span>
+          )}
+        </div>
+      )}
+    </div>
+  );
+}
+
+function isAdvancedDownloadUrlArray(
+  downloadUrl: string | AdvancedDownloadUrl[] | undefined
+): downloadUrl is AdvancedDownloadUrl[] {
+  return !!downloadUrl && typeof downloadUrl !== 'string';
+}
diff --git a/server/sonar-ui-common/components/ui/update-center/MetaDataVersions.tsx b/server/sonar-ui-common/components/ui/update-center/MetaDataVersions.tsx
new file mode 100644 (file)
index 0000000..59e3a2c
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import * as React from 'react';
+import MetaDataVersion from './MetaDataVersion';
+import { MetaDataVersionInformation } from './update-center-metadata';
+
+interface Props {
+  versions: MetaDataVersionInformation[];
+}
+
+interface State {
+  collapsed: boolean;
+}
+
+export default class MetaDataVersions extends React.Component<Props, State> {
+  state: State = {
+    collapsed: true,
+  };
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.versions !== this.props.versions) {
+      this.setState({ collapsed: true });
+    }
+  }
+
+  handleClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.setState(({ collapsed }) => ({ collapsed: !collapsed }));
+  };
+
+  render() {
+    const { versions } = this.props;
+    const { collapsed } = this.state;
+
+    const archivedVersions = versions.filter((version) => version.archived);
+    const currentVersions = versions.filter((version) => !version.archived);
+
+    return (
+      <div className="update-center-meta-data-versions">
+        {archivedVersions.length > 0 && (
+          <button
+            className="update-center-meta-data-versions-show-more"
+            onClick={this.handleClick}
+            type="button">
+            {collapsed ? 'Show more versions' : 'Show fewer versions'}
+          </button>
+        )}
+
+        {currentVersions.map((versionInformation) => (
+          <MetaDataVersion
+            key={versionInformation.version}
+            versionInformation={versionInformation}
+          />
+        ))}
+
+        {!collapsed &&
+          archivedVersions.map((archivedVersionInformation) => (
+            <MetaDataVersion
+              key={archivedVersionInformation.version}
+              versionInformation={archivedVersionInformation}
+            />
+          ))}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-ui-common/components/ui/update-center/__tests__/MetaData-test.tsx b/server/sonar-ui-common/components/ui/update-center/__tests__/MetaData-test.tsx
new file mode 100644 (file)
index 0000000..574abcb
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+import MetaData from '../MetaData';
+import { mockMetaDataInformation } from '../mocks/update-center-metadata';
+import { MetaDataInformation } from '../update-center-metadata';
+import { HttpStatus } from '../../../../helpers/request';
+
+beforeAll(() => {
+  window.fetch = jest.fn();
+});
+
+beforeEach(() => {
+  jest.resetAllMocks();
+});
+
+it('should render correctly', async () => {
+  const metaDataInfo = mockMetaDataInformation();
+  mockFetchReturnValue(metaDataInfo);
+
+  const wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render correctly with organization', async () => {
+  const metaDataInfo = mockMetaDataInformation({
+    organization: { name: 'test-org', url: 'test-org-url' },
+  });
+  mockFetchReturnValue(metaDataInfo);
+
+  const wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should not render anything if call for metadata fails', async () => {
+  const metaDataInfo = mockMetaDataInformation();
+  mockFetchReturnValue(metaDataInfo, HttpStatus.NotFound);
+
+  const wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+  expect(wrapper.type()).toBeNull();
+});
+
+it('should fetch metadata again if the update center key if modified', async () => {
+  const metaDataInfo = mockMetaDataInformation();
+  mockFetchReturnValue(metaDataInfo);
+
+  const wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+
+  expect(window.fetch).toHaveBeenCalledTimes(1);
+
+  mockFetchReturnValue(metaDataInfo);
+  wrapper.setProps({ updateCenterKey: 'abap' });
+
+  expect(window.fetch).toHaveBeenCalledTimes(2);
+});
+
+function shallowRender(props?: Partial<MetaData['props']>) {
+  return shallow<MetaData>(<MetaData updateCenterKey="apex" {...props} />);
+}
+
+function mockFetchReturnValue(metaDataInfo: MetaDataInformation, status = HttpStatus.Ok) {
+  (window.fetch as jest.Mock).mockResolvedValueOnce({ status, json: () => metaDataInfo });
+}
diff --git a/server/sonar-ui-common/components/ui/update-center/__tests__/MetaDataVersion-test.tsx b/server/sonar-ui-common/components/ui/update-center/__tests__/MetaDataVersion-test.tsx
new file mode 100644 (file)
index 0000000..eb21373
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import MetaDataVersion, { MetaDataVersionProps } from '../MetaDataVersion';
+import { mockMetaDataVersionInformation } from '../mocks/update-center-metadata';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+  expect(
+    shallowRender({
+      versionInformation: mockMetaDataVersionInformation({
+        downloadURL: [{ label: 'macos 64 bits', url: '' }],
+      }),
+    })
+  ).toMatchSnapshot('with advanced downloadUrl');
+  expect(
+    shallowRender({
+      versionInformation: { version: '2.0' },
+    })
+  ).toMatchSnapshot('with very few info');
+});
+
+function shallowRender(props?: Partial<MetaDataVersionProps>) {
+  return shallow(
+    <MetaDataVersion versionInformation={mockMetaDataVersionInformation()} {...props} />
+  );
+}
diff --git a/server/sonar-ui-common/components/ui/update-center/__tests__/MetaDataVersions-test.tsx b/server/sonar-ui-common/components/ui/update-center/__tests__/MetaDataVersions-test.tsx
new file mode 100644 (file)
index 0000000..5a4fc20
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { shallow } from 'enzyme';
+import * as React from 'react';
+import { click } from '../../../../helpers/testUtils';
+import MetaDataVersion from '../MetaDataVersion';
+import MetaDataVersions from '../MetaDataVersions';
+import { mockMetaDataVersionInformation } from '../mocks/update-center-metadata';
+
+it('should render correctly', () => {
+  const wrapper = shallowRender();
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should properly handle show more / show less', () => {
+  const wrapper = shallowRender();
+  expect(wrapper.find(MetaDataVersion).length).toBe(1);
+
+  click(wrapper.find('.update-center-meta-data-versions-show-more'));
+  expect(wrapper.find(MetaDataVersion).length).toBe(3);
+});
+
+function shallowRender(props?: Partial<MetaDataVersions['props']>) {
+  return shallow<MetaDataVersions>(
+    <MetaDataVersions
+      versions={[
+        mockMetaDataVersionInformation({ version: '3.0' }),
+        mockMetaDataVersionInformation({ version: '2.0', archived: true }),
+        mockMetaDataVersionInformation({ version: '1.0', archived: true }),
+      ]}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-ui-common/components/ui/update-center/__tests__/__snapshots__/MetaData-test.tsx.snap b/server/sonar-ui-common/components/ui/update-center/__tests__/__snapshots__/MetaData-test.tsx.snap
new file mode 100644 (file)
index 0000000..89e8d74
--- /dev/null
@@ -0,0 +1,133 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+  className="update-center-meta-data"
+>
+  <div
+    className="update-center-meta-data-header"
+  >
+    <span
+      className="update-center-meta-data-vendor"
+    >
+      By 
+      <a
+        href="http://www.sonarsource.com/"
+        rel="noopener noreferrer"
+        target="_blank"
+      >
+        SonarSource
+      </a>
+    </span>
+    <span
+      className="update-center-meta-data-license"
+    >
+      SonarSource
+    </span>
+    <span
+      className="update-center-meta-data-issue-tracker"
+    >
+      <a
+        href="https://jira.sonarsource.com/browse/SONARJAVA"
+        rel="noopener noreferrer"
+        target="_blank"
+      >
+        Issue Tracker
+      </a>
+    </span>
+    <span
+      className="update-center-meta-data-supported"
+    >
+      Supported by SonarSource
+    </span>
+  </div>
+  <MetaDataVersions
+    versions={
+      Array [
+        Object {
+          "archived": false,
+          "changeLogUrl": "https://example.com/sonar-java-plugin/release",
+          "compatibility": "6.7",
+          "date": "2019-05-31",
+          "downloadURL": "https://example.com/sonar-java-plugin-5.13.0.18197.jar",
+          "version": "2.0",
+        },
+        Object {
+          "archived": true,
+          "changeLogUrl": "https://example.com/sonar-java-plugin/release",
+          "compatibility": "6.7",
+          "date": "2019-05-31",
+          "downloadURL": "https://example.com/sonar-java-plugin-5.13.0.18197.jar",
+          "version": "1.0",
+        },
+      ]
+    }
+  />
+</div>
+`;
+
+exports[`should render correctly with organization 1`] = `
+<div
+  className="update-center-meta-data"
+>
+  <div
+    className="update-center-meta-data-header"
+  >
+    <span
+      className="update-center-meta-data-vendor"
+    >
+      By 
+      <a
+        href="test-org-url"
+        rel="noopener noreferrer"
+        target="_blank"
+      >
+        test-org
+      </a>
+    </span>
+    <span
+      className="update-center-meta-data-license"
+    >
+      SonarSource
+    </span>
+    <span
+      className="update-center-meta-data-issue-tracker"
+    >
+      <a
+        href="https://jira.sonarsource.com/browse/SONARJAVA"
+        rel="noopener noreferrer"
+        target="_blank"
+      >
+        Issue Tracker
+      </a>
+    </span>
+    <span
+      className="update-center-meta-data-supported"
+    >
+      Supported by SonarSource
+    </span>
+  </div>
+  <MetaDataVersions
+    versions={
+      Array [
+        Object {
+          "archived": false,
+          "changeLogUrl": "https://example.com/sonar-java-plugin/release",
+          "compatibility": "6.7",
+          "date": "2019-05-31",
+          "downloadURL": "https://example.com/sonar-java-plugin-5.13.0.18197.jar",
+          "version": "2.0",
+        },
+        Object {
+          "archived": true,
+          "changeLogUrl": "https://example.com/sonar-java-plugin/release",
+          "compatibility": "6.7",
+          "date": "2019-05-31",
+          "downloadURL": "https://example.com/sonar-java-plugin-5.13.0.18197.jar",
+          "version": "1.0",
+        },
+      ]
+    }
+  />
+</div>
+`;
diff --git a/server/sonar-ui-common/components/ui/update-center/__tests__/__snapshots__/MetaDataVersion-test.tsx.snap b/server/sonar-ui-common/components/ui/update-center/__tests__/__snapshots__/MetaDataVersion-test.tsx.snap
new file mode 100644 (file)
index 0000000..7fd964f
--- /dev/null
@@ -0,0 +1,113 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+  className="update-center-meta-data-version"
+>
+  <div
+    className="update-center-meta-data-version-version"
+  >
+    5.13
+  </div>
+  <div
+    className="update-center-meta-data-version-release-info"
+  >
+    <time
+      className="update-center-meta-data-version-release-date"
+    >
+      2019-05-31
+    </time>
+    <span
+      className="update-center-meta-data-version-compatibility"
+    >
+      6.7
+    </span>
+  </div>
+  <div
+    className="update-center-meta-data-version-release-links"
+  >
+    <span
+      className="update-center-meta-data-version-download"
+      key="0"
+    >
+      <a
+        href="https://example.com/sonar-java-plugin-5.13.0.18197.jar"
+        rel="noopener noreferrer"
+        target="_blank"
+      >
+        Download
+      </a>
+    </span>
+    <span
+      className="update-center-meta-data-version-release-notes"
+    >
+      <a
+        href="https://example.com/sonar-java-plugin/release"
+        rel="noopener noreferrer"
+        target="_blank"
+      >
+        Release notes
+      </a>
+    </span>
+  </div>
+</div>
+`;
+
+exports[`should render correctly: with advanced downloadUrl 1`] = `
+<div
+  className="update-center-meta-data-version"
+>
+  <div
+    className="update-center-meta-data-version-version"
+  >
+    5.13
+  </div>
+  <div
+    className="update-center-meta-data-version-release-info"
+  >
+    <time
+      className="update-center-meta-data-version-release-date"
+    >
+      2019-05-31
+    </time>
+    <span
+      className="update-center-meta-data-version-compatibility"
+    >
+      6.7
+    </span>
+  </div>
+  <div
+    className="update-center-meta-data-version-release-links"
+  >
+    <span
+      className="update-center-meta-data-version-release-notes"
+    >
+      <a
+        href="https://example.com/sonar-java-plugin/release"
+        rel="noopener noreferrer"
+        target="_blank"
+      >
+        Release notes
+      </a>
+    </span>
+  </div>
+</div>
+`;
+
+exports[`should render correctly: with very few info 1`] = `
+<div
+  className="update-center-meta-data-version"
+>
+  <div
+    className="update-center-meta-data-version-version"
+  >
+    2.0
+  </div>
+  <div
+    className="update-center-meta-data-version-release-info"
+  />
+  <div
+    className="update-center-meta-data-version-release-links"
+  />
+</div>
+`;
diff --git a/server/sonar-ui-common/components/ui/update-center/__tests__/__snapshots__/MetaDataVersions-test.tsx.snap b/server/sonar-ui-common/components/ui/update-center/__tests__/__snapshots__/MetaDataVersions-test.tsx.snap
new file mode 100644 (file)
index 0000000..109fe96
--- /dev/null
@@ -0,0 +1,28 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+  className="update-center-meta-data-versions"
+>
+  <button
+    className="update-center-meta-data-versions-show-more"
+    onClick={[Function]}
+    type="button"
+  >
+    Show more versions
+  </button>
+  <MetaDataVersion
+    key="3.0"
+    versionInformation={
+      Object {
+        "archived": false,
+        "changeLogUrl": "https://example.com/sonar-java-plugin/release",
+        "compatibility": "6.7",
+        "date": "2019-05-31",
+        "downloadURL": "https://example.com/sonar-java-plugin-5.13.0.18197.jar",
+        "version": "3.0",
+      }
+    }
+  />
+</div>
+`;
diff --git a/server/sonar-ui-common/components/ui/update-center/mocks/update-center-metadata.ts b/server/sonar-ui-common/components/ui/update-center/mocks/update-center-metadata.ts
new file mode 100644 (file)
index 0000000..0da8569
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { MetaDataInformation, MetaDataVersionInformation } from '../update-center-metadata';
+
+export function mockMetaDataVersionInformation(
+  overrides?: Partial<MetaDataVersionInformation>
+): MetaDataVersionInformation {
+  return {
+    version: '5.13',
+    date: '2019-05-31',
+    compatibility: '6.7',
+    archived: false,
+    downloadURL: 'https://example.com/sonar-java-plugin-5.13.0.18197.jar',
+    changeLogUrl: 'https://example.com/sonar-java-plugin/release',
+    ...overrides,
+  };
+}
+
+export function mockMetaDataInformation(
+  overrides?: Partial<MetaDataInformation>
+): MetaDataInformation {
+  return {
+    name: 'SonarJava',
+    key: 'java',
+    isSonarSourceCommercial: true,
+    organization: {
+      name: 'SonarSource',
+      url: 'http://www.sonarsource.com/',
+    },
+    category: 'Languages',
+    license: 'SonarSource',
+    issueTrackerURL: 'https://jira.sonarsource.com/browse/SONARJAVA',
+    sourcesURL: 'https://github.com/SonarSource/sonar-java',
+    versions: [
+      mockMetaDataVersionInformation({ version: '2.0' }),
+      mockMetaDataVersionInformation({ version: '1.0', archived: true }),
+    ],
+    ...overrides,
+  };
+}
diff --git a/server/sonar-ui-common/components/ui/update-center/update-center-metadata.ts b/server/sonar-ui-common/components/ui/update-center/update-center-metadata.ts
new file mode 100644 (file)
index 0000000..00e1d96
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+export interface MetaDataInformation {
+  category?: string;
+  isSonarSourceCommercial?: boolean;
+  issueTrackerURL?: string;
+  key?: string;
+  license?: string;
+  name: string;
+  organization?: {
+    name: string;
+    url?: string;
+  };
+  sourcesURL?: string;
+  versions?: MetaDataVersionInformation[];
+}
+
+export interface MetaDataVersionInformation {
+  archived?: boolean;
+  changeLogUrl?: string;
+  compatibility?: string;
+  date?: string;
+  description?: string;
+  downloadURL?: string | AdvancedDownloadUrl[];
+  version: string;
+}
+
+export interface AdvancedDownloadUrl {
+  label?: string;
+  url: string;
+}
diff --git a/server/sonar-ui-common/config/jest/CSSStub.js b/server/sonar-ui-common/config/jest/CSSStub.js
new file mode 100644 (file)
index 0000000..ca0a666
--- /dev/null
@@ -0,0 +1,20 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+module.exports = {};
diff --git a/server/sonar-ui-common/config/jest/FileStub.js b/server/sonar-ui-common/config/jest/FileStub.js
new file mode 100644 (file)
index 0000000..8ed5f4c
--- /dev/null
@@ -0,0 +1,20 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+module.exports = 'test-file-stub';
diff --git a/server/sonar-ui-common/config/jest/SetupEnzyme.js b/server/sonar-ui-common/config/jest/SetupEnzyme.js
new file mode 100644 (file)
index 0000000..89c37ee
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+const Enzyme = require('enzyme');
+const Adapter = require('enzyme-adapter-react-16');
+
+Enzyme.configure({ adapter: new Adapter() });
diff --git a/server/sonar-ui-common/config/jest/SetupSUC.ts b/server/sonar-ui-common/config/jest/SetupSUC.ts
new file mode 100644 (file)
index 0000000..1ce2eb3
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import ThemeContext from '../../components/theme';
+import Initializer, { DEFAULT_LOCALE } from '../../helpers/init';
+import testTheme from './testTheme';
+
+Initializer.setLocale(DEFAULT_LOCALE).setMessages({}).setUrlContext('');
+
+// Hack : override the default value of the context used for theme by emotion
+// This allows tests to get the theme value without specifiying a theme provider
+ThemeContext['_currentValue'] = testTheme;
+ThemeContext['_currentValue2'] = testTheme;
diff --git a/server/sonar-ui-common/config/jest/SetupTestEnvironment.js b/server/sonar-ui-common/config/jest/SetupTestEnvironment.js
new file mode 100644 (file)
index 0000000..432cbb1
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+require('whatwg-fetch');
+
+const content = document.createElement('div');
+content.id = 'content';
+document.documentElement.appendChild(content);
diff --git a/server/sonar-ui-common/config/jest/testTheme.ts b/server/sonar-ui-common/config/jest/testTheme.ts
new file mode 100644 (file)
index 0000000..91a4f66
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+const grid = 8;
+
+export default {
+  colors: {
+    blue: '#4b9fd5',
+    veryLightBlue: '#f2faff',
+    lightBlue: '#cae3f2',
+    darkBlue: '#236a97',
+    veryDarkBlue: '#0E516F',
+    green: '#00aa00',
+    lightGreen: '#b0d513',
+    veryLightGreen: '#f5f9fc',
+    yellow: '#eabe06',
+    orange: '#ed7d20',
+    red: '#d4333f',
+    purple: '#9139d4',
+    white: '#ffffff',
+
+    gray94: '#efefef',
+    gray80: '#cdcdcd',
+    gray71: '#b4b4b4',
+    gray67: '#aaa',
+    gray60: '#999',
+    gray40: '#404040',
+
+    transparentWhite: 'rgba(255,255,255,0.62)',
+    transparentGray: 'rgba(200, 200, 200, 0.5)',
+    transparentBlack: 'rgba(0, 0, 0, 0.25)',
+
+    disableGrayText: '#bbb',
+    disableGrayBorder: '#ddd',
+    disableGrayBg: '#ebebeb',
+
+    barBackgroundColor: '#f3f3f3',
+    barBackgroundColorHighlight: '#f8f8f8',
+    barBorderColor: '#e6e6e6',
+
+    globalNavBarBg: '#262626',
+
+    // fonts
+    baseFontColor: '#444',
+    secondFontColor: '#777',
+
+    // forms
+    mandatoryFieldColor: '#a4030f',
+
+    // leak
+    leakPrimaryColor: '#fbf3d5',
+    leakSecondaryColor: '#f1e8cb',
+
+    // issues
+    issueBgColor: '#f2dede',
+    hotspotBgColor: '#eeeff4',
+    issueLocationSelected: '#f4b1b0',
+    issueLocationHighlighted: '#e1e1f2',
+    conciseIssueRed: '#d18582',
+    conciseIssueRedSelected: '#a4030f',
+
+    // coverage
+    lineCoverageRed: '#a4030f',
+    lineCoverageGreen: '#b4dd78',
+
+    // alerts
+    warningIconColor: '#eabe06',
+
+    alertBorderError: '#f4b1b0',
+    alertBackgroundError: '#f2dede',
+    alertTextError: '#862422',
+    alertIconError: '#a4030f',
+
+    alertBorderWarning: '#faebcc',
+    alertBackgroundWarning: '#fcf8e3',
+    alertTextWarning: '#6f4f17',
+    alertIconWarning: '#db781a',
+
+    alertBorderSuccess: '#d6e9c6',
+    alertBackgroundSuccess: '#dff0d8',
+    alertTextSuccess: '#215821',
+    alertIconSuccess: '#6d9867',
+
+    alertBorderInfo: '#b1dff3',
+    alertBackgroundInfo: '#d9edf7',
+    alertTextInfo: '#0e516f',
+    alertIconInfo: '#0271b9',
+  },
+
+  sizes: {
+    gridSize: `${grid}px`,
+
+    baseFontSize: '13px',
+    verySmallFontSize: '10px',
+    smallFontSize: '12px',
+    mediumFontSize: '14px',
+    bigFontSize: '16px',
+    hugeFontSize: '24px',
+
+    hugeControlHeight: `${5 * grid}px`,
+    largeControlHeight: `${4 * grid}px`,
+    controlHeight: `${3 * grid}px`,
+    smallControlHeight: `${2.5 * grid}px`,
+    tinyControlHeight: `${2 * grid}px`,
+
+    globalNavHeight: `${6 * grid}px`,
+
+    globalNavContentHeight: `${4 * grid}px`,
+
+    maxPageWidth: '1320px',
+    minPageWidth: '1080px',
+    pagePadding: '20px',
+  },
+
+  rawSizes: {
+    grid,
+    globalNavHeightRaw: 6 * grid,
+    globalNavContentHeightRaw: 4 * grid,
+    contextNavHeightRaw: 9 * grid,
+  },
+
+  fonts: {
+    baseFontFamily: "'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif",
+    systemFontFamily:
+      "-apple-system,'BlinkMacSystemFont','Segoe UI','Helvetica','Arial',sans-serif",
+    sonarcloudFontFamily:
+      "Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif",
+  },
+
+  // z-index
+  // =======
+  //    1 -  100  for page elements (e.g. sidebars, panels)
+  //  101 -  500  for generic page fixed elements (e.g. navigation, workspace)
+  //  501 - 3000  for page ui elements
+  // 3001 - 8000  for generic ui elements (e.g. dropdowns, tooltips)
+  zIndexes: {
+    // common
+    aboveNormalZIndex: '3',
+    normalZIndex: '2',
+    belowNormalZIndex: '1',
+
+    // page elements
+    pageMainZIndex: '50',
+
+    // generic page fixed elements
+    contextbarZIndex: '420',
+
+    // generic ui elements
+    popupZIndex: '5000',
+
+    modalZIndex: '6001',
+    modalOverlayZIndex: '6000',
+
+    processContainerZIndex: '7000',
+
+    dropdownMenuZIndex: '7500',
+
+    tooltipZIndex: '8000',
+  },
+
+  others: {
+    defaultShadow: '0 6px 12px rgba(0, 0, 0, 0.175)',
+  },
+};
diff --git a/server/sonar-ui-common/helpers/__tests__/__snapshots__/query-test.ts.snap b/server/sonar-ui-common/helpers/__tests__/__snapshots__/query-test.ts.snap
new file mode 100644 (file)
index 0000000..4f0b553
--- /dev/null
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`cleanQuery should remove undefined and null query items 1`] = `
+Object {
+  "a": "b",
+  "d": "",
+  "e": 0,
+}
+`;
+
+exports[`parseAsDate should parse string date correctly 1`] = `2016-06-20T13:09:48.256Z`;
diff --git a/server/sonar-ui-common/helpers/__tests__/colors-test.ts b/server/sonar-ui-common/helpers/__tests__/colors-test.ts
new file mode 100644 (file)
index 0000000..ccc58d3
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as colors from '../colors';
+
+describe('#stringToColor', () => {
+  it('should return a color for a text', () => {
+    expect(colors.stringToColor('skywalker')).toBe('#97f047');
+  });
+});
+
+describe('#isDarkColor', () => {
+  it('should be dark', () => {
+    expect(colors.isDarkColor('#000000')).toBe(true);
+    expect(colors.isDarkColor('#222222')).toBe(true);
+    expect(colors.isDarkColor('#000')).toBe(true);
+  });
+  it('should be light', () => {
+    expect(colors.isDarkColor('#FFFFFF')).toBe(false);
+    expect(colors.isDarkColor('#CDCDCD')).toBe(false);
+    expect(colors.isDarkColor('#FFF')).toBe(false);
+  });
+});
+
+describe('#getTextColor', () => {
+  it('should return dark color', () => {
+    expect(colors.getTextColor('#FFF', 'dark', 'light')).toBe('dark');
+    expect(colors.getTextColor('#FFF')).toBe('#222');
+  });
+  it('should return light color', () => {
+    expect(colors.getTextColor('#000', 'dark', 'light')).toBe('light');
+    expect(colors.getTextColor('#000')).toBe('#fff');
+  });
+});
diff --git a/server/sonar-ui-common/helpers/__tests__/dates-test.ts b/server/sonar-ui-common/helpers/__tests__/dates-test.ts
new file mode 100644 (file)
index 0000000..688fca9
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as dates from '../dates';
+
+const { parseDate } = dates;
+const recentDate = parseDate('2017-08-16T12:00:00.000Z');
+
+it('toShortNotSoISOString', () => {
+  expect(dates.toShortNotSoISOString(recentDate)).toBe('2017-08-16');
+});
+
+it('toNotSoISOString', () => {
+  expect(dates.toNotSoISOString(recentDate)).toBe('2017-08-16T12:00:00+0000');
+});
+
+it('isValidDate', () => {
+  expect(dates.isValidDate(recentDate)).toBe(true);
+  expect(dates.isValidDate(new Date())).toBe(true);
+  expect(dates.isValidDate(parseDate('foo'))).toBe(false);
+});
diff --git a/server/sonar-ui-common/helpers/__tests__/handleRequiredAuthentication-test.ts b/server/sonar-ui-common/helpers/__tests__/handleRequiredAuthentication-test.ts
new file mode 100644 (file)
index 0000000..fa90439
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import getHistory from '../getHistory';
+import handleRequiredAuthentication from '../handleRequiredAuthentication';
+
+jest.mock('../getHistory', () => ({
+  default: jest.fn(),
+}));
+
+it('should not render for anonymous user', () => {
+  const replace = jest.fn();
+  (getHistory as jest.Mock<any>).mockReturnValue({ replace });
+  handleRequiredAuthentication();
+  expect(replace).toBeCalledWith(expect.objectContaining({ pathname: '/sessions/new' }));
+});
diff --git a/server/sonar-ui-common/helpers/__tests__/init-test.ts b/server/sonar-ui-common/helpers/__tests__/init-test.ts
new file mode 100644 (file)
index 0000000..44d2dbe
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+/* eslint-disable no-console */
+import Initializer, {
+  DEFAULT_LOCALE,
+  DEFAULT_MESSAGES,
+  getLocale,
+  getMessages,
+  getReactDomContainerSelector,
+  getUrlContext,
+} from '../init';
+
+const originalConsoleWarn = console.warn;
+console.warn = jest.fn();
+
+beforeEach(() => {
+  (console.warn as jest.Mock).mockClear();
+});
+
+afterAll(() => {
+  Initializer.setLocale('en').setMessages({}).setUrlContext('');
+  // @ts-ignore: initialize everything to undefined, not possible by respecting types
+  Initializer.setReactDomContainer(undefined);
+  console.warn = originalConsoleWarn;
+});
+
+it('should throw when trying to get a value without initializing first', () => {
+  // @ts-ignore: initialize react dom container to undefined, not possible by respecting types
+  Initializer.setLocale(undefined).setMessages(undefined).setUrlContext(undefined);
+
+  expect(getLocale()).toBe(DEFAULT_LOCALE);
+  expect(console.warn).toHaveBeenLastCalledWith(
+    expect.stringContaining('L10n locale is not initialized')
+  );
+
+  expect(getMessages()).toBe(DEFAULT_MESSAGES);
+  expect(console.warn).toHaveBeenLastCalledWith(
+    expect.stringContaining('L10n messages are not initialized')
+  );
+
+  expect(getUrlContext).toThrowErrorMatchingInlineSnapshot(
+    `"sonar-ui-common init: web context needs to be initialized by Initializer.setUrlContext before being used"`
+  );
+});
+
+it('should return the initialized values', () => {
+  const locale = 'ru';
+  const messages = { any: 'Any' };
+  const urlContext = '/context';
+  const reactDomContainerSelector = '#custom';
+
+  Initializer.setLocale(locale)
+    .setMessages(messages)
+    .setUrlContext(urlContext)
+    .setReactDomContainer(reactDomContainerSelector);
+
+  expect(getLocale()).toBe(locale);
+  expect(getMessages()).toBe(messages);
+  expect(getUrlContext()).toBe(urlContext);
+  expect(getReactDomContainerSelector()).toBe(reactDomContainerSelector);
+  expect(console.warn).not.toBeCalled();
+});
+
+it('should have a default react dom container selector without warning', () => {
+  // @ts-ignore: initialize react dom container to undefined, not possible by respecting types
+  Initializer.setReactDomContainer(undefined);
+
+  expect(getReactDomContainerSelector()).toBe('#content');
+  expect(console.warn).not.toBeCalled();
+});
diff --git a/server/sonar-ui-common/helpers/__tests__/l10n-test.ts b/server/sonar-ui-common/helpers/__tests__/l10n-test.ts
new file mode 100644 (file)
index 0000000..3252baf
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+/* eslint-disable camelcase */
+import Initializer, { getMessages } from '../init';
+import { hasMessage, translate, translateWithParameters } from '../l10n';
+
+const originalMessages = getMessages();
+const MSG = 'my_message';
+
+afterEach(() => {
+  Initializer.setMessages(originalMessages);
+});
+
+describe('translate', () => {
+  it('should translate simple message', () => {
+    Initializer.setMessages({ my_key: MSG });
+    expect(translate('my_key')).toBe(MSG);
+  });
+
+  it('should translate message with composite key', () => {
+    Initializer.setMessages({ 'my.composite.message': MSG });
+    expect(translate('my', 'composite', 'message')).toBe(MSG);
+    expect(translate('my.composite', 'message')).toBe(MSG);
+    expect(translate('my', 'composite.message')).toBe(MSG);
+    expect(translate('my.composite.message')).toBe(MSG);
+  });
+
+  it('should not translate message but return its key', () => {
+    expect(translate('random')).toBe('random');
+    expect(translate('random', 'key')).toBe('random.key');
+    expect(translate('composite.random', 'key')).toBe('composite.random.key');
+  });
+});
+
+describe('translateWithParameters', () => {
+  it('should translate message with one parameter in the beginning', () => {
+    Initializer.setMessages({ x_apples: '{0} apples' });
+    expect(translateWithParameters('x_apples', 5)).toBe('5 apples');
+  });
+
+  it('should translate message with one parameter in the middle', () => {
+    Initializer.setMessages({ x_apples: 'I have {0} apples' });
+    expect(translateWithParameters('x_apples', 5)).toBe('I have 5 apples');
+  });
+
+  it('should translate message with one parameter in the end', () => {
+    Initializer.setMessages({ x_apples: 'Apples: {0}' });
+    expect(translateWithParameters('x_apples', 5)).toBe('Apples: 5');
+  });
+
+  it('should translate message with several parameters', () => {
+    Initializer.setMessages({ x_apples: '{0}: I have {2} apples in my {1} baskets - {3}' });
+    expect(translateWithParameters('x_apples', 1, 2, 3, 4)).toBe(
+      '1: I have 3 apples in my 2 baskets - 4'
+    );
+  });
+
+  it('should not be affected by replacement pattern XSS vulnerability of String.replace', () => {
+    Initializer.setMessages({ x_apples: 'I have {0} apples' });
+    expect(translateWithParameters('x_apples', '$`')).toBe('I have $` apples');
+  });
+
+  it('should not translate message but return its key', () => {
+    expect(translateWithParameters('random', 5)).toBe('random.5');
+    expect(translateWithParameters('random', 1, 2, 3)).toBe('random.1.2.3');
+    expect(translateWithParameters('composite.random', 1, 2)).toBe('composite.random.1.2');
+  });
+});
+
+describe('hasMessage', () => {
+  it('should return that the message exists', () => {
+    Initializer.setMessages({ foo: 'Foo', 'foo.bar': 'Foo Bar' });
+    expect(hasMessage('foo')).toBe(true);
+    expect(hasMessage('foo', 'bar')).toBe(true);
+  });
+
+  it('should return that the message is missing', () => {
+    expect(hasMessage('foo')).toBe(false);
+    expect(hasMessage('foo', 'bar')).toBe(false);
+  });
+});
diff --git a/server/sonar-ui-common/helpers/__tests__/measures-test.ts b/server/sonar-ui-common/helpers/__tests__/measures-test.ts
new file mode 100644 (file)
index 0000000..346f0dd
--- /dev/null
@@ -0,0 +1,225 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import Initializer from '../init';
+import { formatMeasure, getMinDecimalsCountToBeDistinctFromThreshold } from '../measures';
+
+const HOURS_IN_DAY = 8;
+const ONE_MINUTE = 1;
+const ONE_HOUR = ONE_MINUTE * 60;
+const ONE_DAY = HOURS_IN_DAY * ONE_HOUR;
+
+beforeAll(() => {
+  Initializer.setMessages({
+    'work_duration.x_days': '{0}d',
+    'work_duration.x_hours': '{0}h',
+    'work_duration.x_minutes': '{0}min',
+    'work_duration.about': '~ {0}',
+    'metric.level.ERROR': 'Error',
+    'metric.level.WARN': 'Warning',
+    'metric.level.OK': 'Ok',
+    'short_number_suffix.g': 'G',
+    'short_number_suffix.k': 'k',
+    'short_number_suffix.m': 'M',
+  });
+});
+
+afterAll(() => {
+  Initializer.setMessages({});
+});
+
+describe('#formatMeasure()', () => {
+  it('should format INT', () => {
+    expect(formatMeasure(0, 'INT')).toBe('0');
+    expect(formatMeasure(1, 'INT')).toBe('1');
+    expect(formatMeasure(-5, 'INT')).toBe('-5');
+    expect(formatMeasure(999, 'INT')).toBe('999');
+    expect(formatMeasure(1000, 'INT')).toBe('1,000');
+    expect(formatMeasure(1529, 'INT')).toBe('1,529');
+    expect(formatMeasure(10000, 'INT')).toBe('10,000');
+    expect(formatMeasure(1234567890, 'INT')).toBe('1,234,567,890');
+  });
+
+  it('should format SHORT_INT', () => {
+    expect(formatMeasure(0, 'SHORT_INT')).toBe('0');
+    expect(formatMeasure(1, 'SHORT_INT')).toBe('1');
+    expect(formatMeasure(999, 'SHORT_INT')).toBe('999');
+    expect(formatMeasure(1000, 'SHORT_INT')).toBe('1k');
+    expect(formatMeasure(1529, 'SHORT_INT')).toBe('1.5k');
+    expect(formatMeasure(10000, 'SHORT_INT')).toBe('10k');
+    expect(formatMeasure(10678, 'SHORT_INT')).toBe('11k');
+    expect(formatMeasure(9467890, 'SHORT_INT')).toBe('9.5M');
+    expect(formatMeasure(994567890, 'SHORT_INT')).toBe('995M');
+    expect(formatMeasure(999000001, 'SHORT_INT')).toBe('999M');
+    expect(formatMeasure(999567890, 'SHORT_INT')).toBe('1G');
+    expect(formatMeasure(1234567890, 'SHORT_INT')).toBe('1.2G');
+    expect(formatMeasure(11234567890, 'SHORT_INT')).toBe('11G');
+  });
+
+  it('should format FLOAT', () => {
+    expect(formatMeasure(0.0, 'FLOAT')).toBe('0.0');
+    expect(formatMeasure(1.0, 'FLOAT')).toBe('1.0');
+    expect(formatMeasure(1.3, 'FLOAT')).toBe('1.3');
+    expect(formatMeasure(1.34, 'FLOAT')).toBe('1.34');
+    expect(formatMeasure(50.89, 'FLOAT')).toBe('50.89');
+    expect(formatMeasure(100.0, 'FLOAT')).toBe('100.0');
+    expect(formatMeasure(123.456, 'FLOAT')).toBe('123.456');
+    expect(formatMeasure(123456.7, 'FLOAT')).toBe('123,456.7');
+    expect(formatMeasure(1234567890.0, 'FLOAT')).toBe('1,234,567,890.0');
+  });
+
+  it('should respect FLOAT precision', () => {
+    expect(formatMeasure(0.1, 'FLOAT')).toBe('0.1');
+    expect(formatMeasure(0.12, 'FLOAT')).toBe('0.12');
+    expect(formatMeasure(0.12345, 'FLOAT')).toBe('0.12345');
+    expect(formatMeasure(0.123456, 'FLOAT')).toBe('0.12346');
+  });
+
+  it('should format PERCENT', () => {
+    expect(formatMeasure(0.0, 'PERCENT')).toBe('0.0%');
+    expect(formatMeasure(1.0, 'PERCENT')).toBe('1.0%');
+    expect(formatMeasure(1.3, 'PERCENT')).toBe('1.3%');
+    expect(formatMeasure(1.34, 'PERCENT')).toBe('1.3%');
+    expect(formatMeasure(50.89, 'PERCENT')).toBe('50.9%');
+    expect(formatMeasure(100.0, 'PERCENT')).toBe('100%');
+    expect(formatMeasure(50.89, 'PERCENT', { decimals: 0 })).toBe('50.9%');
+    expect(formatMeasure(50.89, 'PERCENT', { decimals: 1 })).toBe('50.9%');
+    expect(formatMeasure(50.89, 'PERCENT', { decimals: 2 })).toBe('50.89%');
+    expect(formatMeasure(50.89, 'PERCENT', { decimals: 3 })).toBe('50.890%');
+    expect(formatMeasure(50, 'PERCENT', { decimals: 0, omitExtraDecimalZeros: true })).toBe(
+      '50.0%'
+    );
+    expect(formatMeasure(50, 'PERCENT', { decimals: 1, omitExtraDecimalZeros: true })).toBe(
+      '50.0%'
+    );
+    expect(formatMeasure(50, 'PERCENT', { decimals: 3, omitExtraDecimalZeros: true })).toBe(
+      '50.0%'
+    );
+    expect(formatMeasure(50.89, 'PERCENT', { decimals: 3, omitExtraDecimalZeros: true })).toBe(
+      '50.89%'
+    );
+  });
+
+  it('should format WORK_DUR', () => {
+    expect(formatMeasure(0, 'WORK_DUR')).toBe('0');
+    expect(formatMeasure(5 * ONE_DAY, 'WORK_DUR')).toBe('5d');
+    expect(formatMeasure(2 * ONE_HOUR, 'WORK_DUR')).toBe('2h');
+    expect(formatMeasure(40 * ONE_MINUTE, 'WORK_DUR')).toBe('40min');
+    expect(formatMeasure(ONE_MINUTE, 'WORK_DUR')).toBe('1min');
+    expect(formatMeasure(5 * ONE_DAY + 2 * ONE_HOUR, 'WORK_DUR')).toBe('5d 2h');
+    expect(formatMeasure(2 * ONE_HOUR + ONE_MINUTE, 'WORK_DUR')).toBe('2h 1min');
+    expect(formatMeasure(5 * ONE_DAY + 2 * ONE_HOUR + ONE_MINUTE, 'WORK_DUR')).toBe('5d 2h');
+    expect(formatMeasure(15 * ONE_DAY + 2 * ONE_HOUR + ONE_MINUTE, 'WORK_DUR')).toBe('15d');
+    expect(formatMeasure(-5 * ONE_DAY, 'WORK_DUR')).toBe('-5d');
+    expect(formatMeasure(-2 * ONE_HOUR, 'WORK_DUR')).toBe('-2h');
+    expect(formatMeasure(-1 * ONE_MINUTE, 'WORK_DUR')).toBe('-1min');
+  });
+
+  it('should format SHORT_WORK_DUR', () => {
+    expect(formatMeasure(0, 'SHORT_WORK_DUR')).toBe('0');
+    expect(formatMeasure(5 * ONE_DAY, 'SHORT_WORK_DUR')).toBe('5d');
+    expect(formatMeasure(2 * ONE_HOUR, 'SHORT_WORK_DUR')).toBe('2h');
+    expect(formatMeasure(ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('1min');
+    expect(formatMeasure(40 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('40min');
+    expect(formatMeasure(58 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('1h');
+    expect(formatMeasure(5 * ONE_DAY + 2 * ONE_HOUR, 'SHORT_WORK_DUR')).toBe('5d');
+    expect(formatMeasure(2 * ONE_HOUR + ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('2h');
+    expect(formatMeasure(ONE_HOUR + 55 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('2h');
+    expect(formatMeasure(3 * ONE_DAY + 6 * ONE_HOUR, 'SHORT_WORK_DUR')).toBe('4d');
+    expect(formatMeasure(7 * ONE_HOUR + 59 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('1d');
+    expect(formatMeasure(5 * ONE_DAY + 2 * ONE_HOUR + ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('5d');
+    expect(formatMeasure(15 * ONE_DAY + 2 * ONE_HOUR + ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('15d');
+    expect(formatMeasure(7 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('7min');
+    expect(formatMeasure(-5 * ONE_DAY, 'SHORT_WORK_DUR')).toBe('-5d');
+    expect(formatMeasure(-2 * ONE_HOUR, 'SHORT_WORK_DUR')).toBe('-2h');
+    expect(formatMeasure(-1 * ONE_MINUTE, 'SHORT_WORK_DUR')).toBe('-1min');
+
+    expect(formatMeasure(1529 * ONE_DAY, 'SHORT_WORK_DUR')).toBe('1.5kd');
+    expect(formatMeasure(1234567 * ONE_DAY, 'SHORT_WORK_DUR')).toBe('1.2Md');
+    expect(formatMeasure(12345670 * ONE_DAY + 4 * ONE_HOUR, 'SHORT_WORK_DUR')).toBe('12Md');
+  });
+
+  it('should format RATING', () => {
+    expect(formatMeasure(1, 'RATING')).toBe('A');
+    expect(formatMeasure(2, 'RATING')).toBe('B');
+    expect(formatMeasure(3, 'RATING')).toBe('C');
+    expect(formatMeasure(4, 'RATING')).toBe('D');
+    expect(formatMeasure(5, 'RATING')).toBe('E');
+  });
+
+  it('should format LEVEL', () => {
+    expect(formatMeasure('ERROR', 'LEVEL')).toBe('Error');
+    expect(formatMeasure('WARN', 'LEVEL')).toBe('Warning');
+    expect(formatMeasure('OK', 'LEVEL')).toBe('Ok');
+    expect(formatMeasure('UNKNOWN', 'LEVEL')).toBe('UNKNOWN');
+  });
+
+  it('should format MILLISEC', () => {
+    expect(formatMeasure(0, 'MILLISEC')).toBe('0ms');
+    expect(formatMeasure(1, 'MILLISEC')).toBe('1ms');
+    expect(formatMeasure(173, 'MILLISEC')).toBe('173ms');
+    expect(formatMeasure(3649, 'MILLISEC')).toBe('4s');
+    expect(formatMeasure(893481, 'MILLISEC')).toBe('15min');
+    expect(formatMeasure(17862325, 'MILLISEC')).toBe('298min');
+  });
+
+  it('should not format unknown type', () => {
+    expect(formatMeasure('random value', 'RANDOM_TYPE')).toBe('random value');
+  });
+
+  it('should return null if value is empty string', () => {
+    expect(formatMeasure('', 'PERCENT')).toBe('');
+  });
+
+  it('should not fail with undefined', () => {
+    expect(formatMeasure(undefined, 'INT')).toBe('');
+  });
+});
+
+describe('getMinDecimalsCountToBeDistinctFromThreshold', () => {
+  it('should return default if no threshold', () => {
+    expect(getMinDecimalsCountToBeDistinctFromThreshold(2.67, undefined)).toBe(1);
+  });
+
+  it('should return 1 if delta is 0', () => {
+    expect(getMinDecimalsCountToBeDistinctFromThreshold(2.5, 2.5)).toBe(1);
+  });
+
+  it('should return 1 if the delta is larger than 0.1', () => {
+    [0.1, 0.15, 0.2, 0.5, 0.8, 1].forEach((delta) => {
+      expect(getMinDecimalsCountToBeDistinctFromThreshold(2.5 + delta, 2.5)).toBe(1);
+      expect(getMinDecimalsCountToBeDistinctFromThreshold(2.5 - delta, 2.5)).toBe(1);
+    });
+  });
+
+  it('should return enough precision to see the delta', () => {
+    expect(getMinDecimalsCountToBeDistinctFromThreshold(2.55, 2.5)).toBe(2);
+    expect(getMinDecimalsCountToBeDistinctFromThreshold(2.505, 2.5)).toBe(3);
+    expect(getMinDecimalsCountToBeDistinctFromThreshold(2.5005, 2.5)).toBe(4);
+    expect(getMinDecimalsCountToBeDistinctFromThreshold(85.01, 85)).toBe(2);
+    expect(getMinDecimalsCountToBeDistinctFromThreshold(84.95, 85)).toBe(2);
+    expect(getMinDecimalsCountToBeDistinctFromThreshold(84.999999999999554, 85)).toBe(
+      '9999999999995'.length
+    );
+    expect(getMinDecimalsCountToBeDistinctFromThreshold(85.0000000000000954, 85)).toBe(
+      '00000000000009'.length
+    );
+    expect(getMinDecimalsCountToBeDistinctFromThreshold(85.00000000000000009, 85)).toBe(1);
+  });
+});
diff --git a/server/sonar-ui-common/helpers/__tests__/path-test.ts b/server/sonar-ui-common/helpers/__tests__/path-test.ts
new file mode 100644 (file)
index 0000000..997d23e
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { collapsedDirFromPath, cutLongWords, fileFromPath } from '../path';
+
+describe('#collapsedDirFromPath()', () => {
+  it('should return null when pass null', () => {
+    expect(collapsedDirFromPath(null)).toBeNull();
+  });
+
+  it('should return "/" when pass "/"', () => {
+    expect(collapsedDirFromPath('/')).toBe('/');
+  });
+
+  it('should not cut short path', () => {
+    expect(collapsedDirFromPath('src/main/js/components/state.js')).toBe('src/main/js/components/');
+  });
+
+  it('should cut long path', () => {
+    expect(collapsedDirFromPath('src/main/js/components/navigator/app/models/state.js')).toBe(
+      'src/.../js/components/navigator/app/models/'
+    );
+  });
+
+  it('should cut very long path', () => {
+    expect(
+      collapsedDirFromPath('src/main/another/js/components/navigator/app/models/state.js')
+    ).toBe('src/.../js/components/navigator/app/models/');
+  });
+});
+
+describe('#fileFromPath()', () => {
+  it('should return null when pass null', () => {
+    expect(fileFromPath(null)).toBeNull();
+  });
+
+  it('should return empty string when pass "/"', () => {
+    expect(fileFromPath('/')).toBe('');
+  });
+
+  it('should return file name when pass only file name', () => {
+    expect(fileFromPath('file.js')).toBe('file.js');
+  });
+
+  it('should return file name when pass file path', () => {
+    expect(fileFromPath('src/main/js/file.js')).toBe('file.js');
+  });
+
+  it('should return file name when pass file name without extension', () => {
+    expect(fileFromPath('src/main/file')).toBe('file');
+  });
+});
+
+describe('#cutLongWords', () => {
+  it('should cut the long work in the middle', () => {
+    expect(cutLongWords('This is a reallylongwordthatdontexistforthe test')).toBe(
+      'This is a reallylongwordthatdontexistfor... test'
+    );
+  });
+
+  it('should not cut anything', () => {
+    expect(cutLongWords('This is a test')).toBe('This is a test');
+  });
+});
diff --git a/server/sonar-ui-common/helpers/__tests__/query-test.ts b/server/sonar-ui-common/helpers/__tests__/query-test.ts
new file mode 100644 (file)
index 0000000..1adea91
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { parseDate } from '../dates';
+import * as query from '../query';
+
+describe('queriesEqual', () => {
+  it('should correctly test equality of two queries', () => {
+    expect(query.queriesEqual({ a: 'test', b: 'test' }, { a: 'test', b: 'test' })).toBe(true);
+    expect(query.queriesEqual({ a: [1, 2], b: 'test' }, { a: [1, 2], b: 'test' })).toBe(true);
+    expect(query.queriesEqual({ a: 'a' }, { a: 'test', b: 'test' })).toBe(false);
+    expect(query.queriesEqual({ a: [1, 2], b: 'test' }, { a: [1], b: 'test' })).toBe(false);
+  });
+});
+
+describe('cleanQuery', () => {
+  it('should remove undefined and null query items', () => {
+    expect(query.cleanQuery({ a: 'b', b: undefined, c: null, d: '', e: 0 })).toMatchSnapshot();
+  });
+});
+
+describe('parseAsBoolean', () => {
+  it('should parse booleans correctly', () => {
+    expect(query.parseAsBoolean('false')).toBe(false);
+    expect(query.parseAsBoolean('true')).toBe(true);
+  });
+
+  it('should return a default value', () => {
+    expect(query.parseAsBoolean('1')).toBe(true);
+    expect(query.parseAsBoolean('foo')).toBe(true);
+  });
+});
+
+describe('parseAsString', () => {
+  it('should parse strings correctly', () => {
+    expect(query.parseAsString('random')).toBe('random');
+    expect(query.parseAsString('')).toBe('');
+    expect(query.parseAsString(undefined)).toBe('');
+  });
+});
+
+describe('parseAsArray', () => {
+  it('should parse string arrays correctly', () => {
+    expect(query.parseAsArray('1,2,3', query.parseAsString)).toEqual(['1', '2', '3']);
+    expect(query.parseAsArray(undefined, query.parseAsString)).toEqual([]);
+  });
+});
+
+describe('parseAsOptionalArray', () => {
+  it('should parse optional arrays correctly', () => {
+    expect(query.parseAsOptionalArray('true,false,false', query.parseAsBoolean)).toEqual([
+      true,
+      false,
+      false,
+    ]);
+    expect(query.parseAsOptionalArray(undefined, query.parseAsString)).toBeUndefined();
+  });
+});
+
+describe('parseAsDate', () => {
+  it('should parse string date correctly', () => {
+    expect(query.parseAsDate('2016-06-20T13:09:48.256Z')).toMatchSnapshot();
+    expect(query.parseAsDate('')).toBeUndefined();
+    expect(query.parseAsDate()).toBeUndefined();
+  });
+});
+
+describe('serializeDate', () => {
+  const date = parseDate('2016-06-20T13:09:48.256Z');
+  it('should serialize string correctly', () => {
+    expect(query.serializeDate(date)).toBe('2016-06-20T13:09:48+0000');
+    expect(query.serializeDate()).toBeUndefined();
+  });
+});
+
+describe('serializeString', () => {
+  it('should serialize string correctly', () => {
+    expect(query.serializeString('foo')).toBe('foo');
+    expect(query.serializeString('')).toBeUndefined();
+  });
+});
+
+describe('serializeStringArray', () => {
+  it('should serialize array of string correctly', () => {
+    expect(query.serializeStringArray(['1', '2', '3'])).toBe('1,2,3');
+    expect(query.serializeStringArray([])).toBeUndefined();
+  });
+});
diff --git a/server/sonar-ui-common/helpers/__tests__/request-test.ts b/server/sonar-ui-common/helpers/__tests__/request-test.ts
new file mode 100644 (file)
index 0000000..aee5770
--- /dev/null
@@ -0,0 +1,347 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import handleRequiredAuthentication from '../handleRequiredAuthentication';
+import {
+  checkStatus,
+  getJSON,
+  getText,
+  HttpStatus,
+  isSuccessStatus,
+  parseError,
+  parseJSON,
+  parseText,
+  post,
+  postJSON,
+  postJSONBody,
+  requestTryAndRepeatUntil,
+} from '../request';
+
+jest.mock('../handleRequiredAuthentication', () => ({ default: jest.fn() }));
+
+jest.mock('../init', () => {
+  const module = jest.requireActual('../init');
+  return {
+    ...module,
+    getRequestOptions: jest.fn().mockReturnValue({}),
+  };
+});
+const url = '/my-url';
+
+beforeEach(() => {
+  jest.clearAllMocks();
+  window.fetch = jest.fn().mockResolvedValue(mockResponse({}, HttpStatus.Ok, {}));
+});
+
+describe('getJSON', () => {
+  it('should get json without parameters', async () => {
+    const response = mockResponse({}, HttpStatus.Ok, {});
+    window.fetch = jest.fn().mockResolvedValue(response);
+    getJSON(url);
+    await new Promise(setImmediate);
+
+    expect(window.fetch).toBeCalledWith(url, expect.objectContaining({ method: 'GET' }));
+    expect(response.json).toBeCalled();
+  });
+
+  it('should get json with parameters', () => {
+    getJSON(url, { data: 'test' });
+    expect(window.fetch).toBeCalledWith(
+      url + '?data=test',
+      expect.objectContaining({ method: 'GET' })
+    );
+  });
+});
+
+describe('getText', () => {
+  it('should get text without parameters', async () => {
+    const response = mockResponse({}, HttpStatus.Ok, '');
+    window.fetch = jest.fn().mockResolvedValue(response);
+    getText(url);
+    await new Promise(setImmediate);
+
+    expect(window.fetch).toBeCalledWith(url, expect.objectContaining({ method: 'GET' }));
+    expect(response.text).toBeCalled();
+  });
+
+  it('should get text with parameters', () => {
+    getText(url, { data: 'test' });
+    expect(window.fetch).toBeCalledWith(
+      url + '?data=test',
+      expect.objectContaining({ method: 'GET' })
+    );
+  });
+});
+
+describe('parseError', () => {
+  it('should parse error and return the message', async () => {
+    const response = new Response(JSON.stringify({ errors: [{ msg: 'Error1' }] }), {
+      status: HttpStatus.BadRequest,
+    });
+    await expect(parseError(response)).resolves.toBe('Error1');
+  });
+
+  it('should parse error and return concatenated messages', async () => {
+    const response = new Response(
+      JSON.stringify({ errors: [{ msg: 'Error1' }, { msg: 'Error2' }] }),
+      { status: HttpStatus.BadRequest }
+    );
+    await expect(parseError(response)).resolves.toBe('Error1. Error2');
+  });
+
+  it('should parse error and return default message', async () => {
+    const response = new Response('{}', { status: HttpStatus.BadRequest });
+    await expect(parseError(response)).resolves.toBe('default_error_message');
+    const responseUndefined = new Response('', { status: HttpStatus.BadRequest });
+    await expect(parseError(responseUndefined)).resolves.toBe('default_error_message');
+  });
+});
+
+describe('parseJSON', () => {
+  it('should return a json response', () => {
+    const body = { test: 2 };
+    const response = mockResponse({}, HttpStatus.Ok, body);
+    const jsonResponse = parseJSON(response);
+    expect(response.json).toBeCalled();
+    return expect(jsonResponse).resolves.toEqual(body);
+  });
+});
+
+describe('parseText', () => {
+  it('should return a text response', () => {
+    const body = 'test';
+    const response = mockResponse({}, HttpStatus.Ok, body);
+    const textResponse = parseText(response);
+    expect(response.text).toBeCalled();
+    return expect(textResponse).resolves.toBe(body);
+  });
+});
+
+describe('postJSON', () => {
+  it('should post without parameters and get json', async () => {
+    const response = mockResponse();
+    window.fetch = jest.fn().mockResolvedValue(response);
+    postJSON(url);
+    await new Promise(setImmediate);
+
+    expect(window.fetch).toBeCalledWith(url, expect.objectContaining({ method: 'POST' }));
+    expect(response.json).toBeCalled();
+  });
+
+  it('should post with a body and get json', () => {
+    postJSON(url, { data: 'test' });
+    expect(window.fetch).toBeCalledWith(
+      url,
+      expect.objectContaining({ body: 'data=test', method: 'POST' })
+    );
+  });
+});
+
+describe('postJSONBody', () => {
+  it('should post without parameters and get json', async () => {
+    const response = mockResponse();
+    window.fetch = jest.fn().mockResolvedValue(response);
+    postJSONBody(url);
+    await new Promise(setImmediate);
+
+    expect(window.fetch).toBeCalledWith(url, expect.objectContaining({ method: 'POST' }));
+    expect(response.json).toBeCalled();
+  });
+
+  it('should post with a body and get json', () => {
+    postJSONBody(url, { nested: { data: 'test', withArray: [1, 2] } });
+    expect(window.fetch).toBeCalledWith(
+      url,
+      expect.objectContaining({
+        headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
+        body: '{"nested":{"data":"test","withArray":[1,2]}}',
+        method: 'POST',
+      })
+    );
+  });
+});
+
+describe('post', () => {
+  it('should post without parameters and return nothing', async () => {
+    const response = mockResponse();
+    window.fetch = jest.fn().mockResolvedValue(response);
+    post(url, { data: 'test' });
+    await new Promise(setImmediate);
+
+    expect(window.fetch).toBeCalledWith(
+      url,
+      expect.objectContaining({ body: 'data=test', method: 'POST' })
+    );
+    expect(response.json).not.toBeCalled();
+    expect(response.text).not.toBeCalled();
+  });
+});
+
+describe('requestTryAndRepeatUntil', () => {
+  jest.useFakeTimers();
+
+  beforeEach(() => {
+    jest.clearAllTimers();
+  });
+
+  it('should repeat call until stop condition is met', async () => {
+    const apiCall = jest.fn().mockResolvedValue({ repeat: true });
+    const stopRepeat = jest.fn().mockImplementation(({ repeat }) => !repeat);
+
+    const promiseResult = requestTryAndRepeatUntil(
+      apiCall,
+      { max: -1, slowThreshold: -20 },
+      stopRepeat
+    );
+
+    for (let i = 1; i < 5; i++) {
+      jest.runAllTimers();
+      expect(apiCall).toBeCalledTimes(i);
+      await new Promise(setImmediate);
+      expect(stopRepeat).toBeCalledTimes(i);
+    }
+    apiCall.mockResolvedValue({ repeat: false });
+    jest.runAllTimers();
+    expect(apiCall).toBeCalledTimes(5);
+    await new Promise(setImmediate);
+    expect(stopRepeat).toBeCalledTimes(5);
+
+    await expect(promiseResult).resolves.toEqual({ repeat: false });
+  });
+
+  it('should repeat call as long as there is an error', async () => {
+    const apiCall = jest.fn().mockRejectedValue({ status: HttpStatus.GatewayTimeout });
+    const stopRepeat = jest.fn().mockReturnValue(true);
+    const promiseResult = requestTryAndRepeatUntil(
+      apiCall,
+      { max: -1, slowThreshold: -20 },
+      stopRepeat,
+      [HttpStatus.GatewayTimeout]
+    );
+
+    for (let i = 1; i < 5; i++) {
+      jest.runAllTimers();
+      expect(apiCall).toBeCalledTimes(i);
+      await new Promise(setImmediate);
+    }
+    apiCall.mockResolvedValue('Success');
+    jest.runAllTimers();
+    expect(apiCall).toBeCalledTimes(5);
+    await new Promise(setImmediate);
+    expect(stopRepeat).toBeCalledTimes(1);
+
+    await expect(promiseResult).resolves.toBe('Success');
+  });
+
+  it('should stop after 3 calls', async () => {
+    const apiCall = jest.fn().mockResolvedValue({});
+    const stopRepeat = jest.fn().mockReturnValue(false);
+    const promiseResult = requestTryAndRepeatUntil(
+      apiCall,
+      { max: 3, slowThreshold: 0 },
+      stopRepeat
+    );
+
+    for (let i = 1; i < 3; i++) {
+      expect(apiCall).toBeCalledTimes(i);
+      await new Promise(setImmediate);
+      jest.runAllTimers();
+    }
+    expect(apiCall).toBeCalledTimes(3);
+    await expect(promiseResult).rejects.toBeUndefined();
+
+    // It should not call anymore after 3 times
+    jest.runAllTimers();
+    expect(apiCall).toBeCalledTimes(3);
+  });
+
+  it('should slow down after 2 calls', async () => {
+    const apiCall = jest.fn().mockResolvedValue({});
+    const stopRepeat = jest.fn().mockReturnValue(false);
+    requestTryAndRepeatUntil(apiCall, { max: 5, slowThreshold: 3 }, stopRepeat);
+
+    for (let i = 1; i < 3; i++) {
+      jest.advanceTimersByTime(500);
+      expect(apiCall).toBeCalledTimes(i);
+      await new Promise(setImmediate);
+    }
+
+    jest.advanceTimersByTime(500);
+    expect(apiCall).toBeCalledTimes(2);
+    jest.advanceTimersByTime(2000);
+    expect(apiCall).toBeCalledTimes(2);
+    jest.advanceTimersByTime(500);
+    expect(apiCall).toBeCalledTimes(3);
+    await new Promise(setImmediate);
+
+    jest.advanceTimersByTime(3000);
+    expect(apiCall).toBeCalledTimes(4);
+  });
+});
+
+describe('checkStatus', () => {
+  it('should resolve with the response', async () => {
+    const response = mockResponse();
+    await expect(checkStatus(response)).resolves.toBe(response);
+  });
+
+  it('should reject with the response', async () => {
+    const response = mockResponse({}, HttpStatus.InternalServerError);
+    await expect(checkStatus(response)).rejects.toEqual(response);
+  });
+
+  it('should handle required authentication', async () => {
+    await checkStatus(mockResponse({}, HttpStatus.Unauthorized)).catch(() => {
+      expect(handleRequiredAuthentication).toBeCalled();
+    });
+  });
+
+  it('should bybass the redirect with a 401 error', async () => {
+    const mockedResponse = mockResponse({}, HttpStatus.Unauthorized);
+    await checkStatus(mockedResponse, true).catch((response) => {
+      expect(handleRequiredAuthentication).not.toBeCalled();
+      expect(response).toEqual(mockedResponse);
+    });
+  });
+});
+it('should export status codes', () => {
+  expect(HttpStatus.NotFound).toEqual(404);
+});
+
+describe('isSuccessStatus', () => {
+  it('should work for a successful response status', () => {
+    expect(isSuccessStatus(HttpStatus.Ok)).toBe(true);
+    expect(isSuccessStatus(HttpStatus.Created)).toBe(true);
+  });
+
+  it('should work for an unsuccessful response status', () => {
+    expect(isSuccessStatus(HttpStatus.MultipleChoices)).toBe(false);
+    expect(isSuccessStatus(HttpStatus.NotFound)).toBe(false);
+    expect(isSuccessStatus(HttpStatus.InternalServerError)).toBe(false);
+  });
+});
+
+function mockResponse(headers: T.Dict<string> = {}, status = HttpStatus.Ok, value?: any): Response {
+  const body = value && value instanceof Object ? JSON.stringify(value) : value;
+  const response = new Response(body, { headers, status });
+  response.json = jest.fn().mockResolvedValue(value);
+  response.text = jest.fn().mockResolvedValue(value);
+  return response;
+}
diff --git a/server/sonar-ui-common/helpers/__tests__/scrolling-test.ts b/server/sonar-ui-common/helpers/__tests__/scrolling-test.ts
new file mode 100644 (file)
index 0000000..ef9f380
--- /dev/null
@@ -0,0 +1,219 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { scrollHorizontally, scrollToElement } from '../scrolling';
+
+jest.useFakeTimers();
+
+describe('scrollToElement', () => {
+  it('should scroll parent up to element', () => {
+    const element = document.createElement('a');
+    element.getBoundingClientRect = mockGetBoundingClientRect({ top: 5, bottom: 20 });
+
+    const parent = document.createElement('div');
+    parent.getBoundingClientRect = mockGetBoundingClientRect({ height: 30, top: 15 });
+    parent.scrollTop = 10;
+    parent.scrollLeft = 12;
+    parent.appendChild(element);
+
+    document.body.appendChild(parent);
+    scrollToElement(element, { parent, smooth: false });
+
+    expect(parent.scrollTop).toEqual(0);
+    expect(parent.scrollLeft).toEqual(12);
+  });
+
+  it('should scroll parent down to element', () => {
+    const element = document.createElement('a');
+    element.getBoundingClientRect = mockGetBoundingClientRect({ top: 25, bottom: 50 });
+
+    const parent = document.createElement('div');
+    parent.getBoundingClientRect = mockGetBoundingClientRect({ height: 30, top: 15 });
+    parent.scrollTop = 10;
+    parent.scrollLeft = 12;
+    parent.appendChild(element);
+
+    document.body.appendChild(parent);
+    scrollToElement(element, { parent, smooth: false });
+
+    expect(parent.scrollTop).toEqual(15);
+    expect(parent.scrollLeft).toEqual(12);
+  });
+
+  it('should scroll window down to element', () => {
+    const element = document.createElement('a');
+    element.getBoundingClientRect = mockGetBoundingClientRect({ top: 840, bottom: 845 });
+
+    Object.defineProperty(window, 'innerHeight', { value: 400 });
+    window.scrollTo = jest.fn();
+
+    document.body.appendChild(element);
+
+    scrollToElement(element, { smooth: false });
+
+    expect(window.scrollTo).toBeCalledWith(0, 445);
+  });
+
+  it('should scroll window up to element', () => {
+    const element = document.createElement('a');
+    element.getBoundingClientRect = mockGetBoundingClientRect({ top: -10, bottom: 10 });
+
+    Object.defineProperty(window, 'innerHeight', { value: 50 });
+    window.scrollTo = jest.fn();
+
+    document.body.appendChild(element);
+
+    scrollToElement(element, { smooth: false });
+
+    expect(window.scrollTo).toBeCalledWith(0, -10);
+  });
+
+  it('should scroll window down to element smoothly', () => {
+    const element = document.createElement('a');
+    element.getBoundingClientRect = mockGetBoundingClientRect({ top: 840, bottom: 845 });
+
+    Object.defineProperty(window, 'innerHeight', { value: 400 });
+    window.scrollTo = jest.fn();
+
+    document.body.appendChild(element);
+
+    scrollToElement(element, {});
+
+    jest.runAllTimers();
+
+    expect(window.scrollTo).toBeCalledTimes(10);
+  });
+});
+
+describe('scrollHorizontally', () => {
+  it('should scroll parent left to element', () => {
+    const element = document.createElement('a');
+    element.getBoundingClientRect = mockGetBoundingClientRect({ left: 25, right: 42 });
+
+    const parent = document.createElement('div');
+    parent.getBoundingClientRect = mockGetBoundingClientRect({ width: 67, left: 46 });
+    parent.scrollTop = 12;
+    parent.scrollLeft = 38;
+    parent.appendChild(element);
+
+    document.body.appendChild(parent);
+
+    scrollHorizontally(element, { parent, smooth: false });
+
+    expect(parent.scrollTop).toEqual(12);
+    expect(parent.scrollLeft).toEqual(17);
+  });
+
+  it('should scroll parent right to element', () => {
+    const element = document.createElement('a');
+    element.getBoundingClientRect = mockGetBoundingClientRect({ left: 25, right: 99 });
+
+    const parent = document.createElement('div');
+    parent.getBoundingClientRect = mockGetBoundingClientRect({ width: 67, left: 20 });
+    parent.scrollTop = 12;
+    parent.scrollLeft = 20;
+    parent.appendChild(element);
+
+    document.body.appendChild(parent);
+
+    scrollHorizontally(element, { parent, smooth: false });
+
+    expect(parent.scrollTop).toEqual(12);
+    expect(parent.scrollLeft).toEqual(32);
+  });
+
+  it('should scroll window right to element', () => {
+    const element = document.createElement('a');
+    element.getBoundingClientRect = mockGetBoundingClientRect({ left: 840, right: 845 });
+
+    Object.defineProperty(window, 'innerWidth', { value: 400 });
+    window.scrollTo = jest.fn();
+
+    document.body.appendChild(element);
+
+    scrollHorizontally(element, { smooth: false });
+
+    expect(window.scrollTo).toBeCalledWith(445, 0);
+  });
+
+  it('should scroll window left to element', () => {
+    const element = document.createElement('a');
+    element.getBoundingClientRect = mockGetBoundingClientRect({ left: -10, right: 10 });
+
+    Object.defineProperty(window, 'innerWidth', { value: 50 });
+    window.scrollTo = jest.fn();
+
+    document.body.appendChild(element);
+
+    scrollHorizontally(element, { smooth: false });
+
+    expect(window.scrollTo).toBeCalledWith(-10, 0);
+  });
+
+  it('should scroll window right to element smoothly', () => {
+    const element = document.createElement('a');
+    element.getBoundingClientRect = mockGetBoundingClientRect({ left: 840, right: 845 });
+
+    Object.defineProperty(window, 'innerWidth', { value: 400 });
+    window.scrollTo = jest.fn();
+
+    document.body.appendChild(element);
+
+    scrollHorizontally(element, {});
+
+    jest.runAllTimers();
+
+    expect(window.scrollTo).toBeCalledTimes(10);
+  });
+});
+
+it('correctly queues and processes multiple scroll calls', async () => {
+  const element1 = document.createElement('a');
+  const element2 = document.createElement('a');
+  document.body.appendChild(element1);
+  document.body.appendChild(element2);
+  element1.getBoundingClientRect = mockGetBoundingClientRect({ left: 840, right: 845 });
+  element2.getBoundingClientRect = mockGetBoundingClientRect({ top: -10, bottom: 10 });
+
+  window.scrollTo = jest.fn();
+
+  scrollHorizontally(element1, {});
+  scrollToElement(element2, { smooth: false });
+
+  jest.runAllTimers();
+  await Promise.resolve(setImmediate);
+  await Promise.resolve(setImmediate);
+
+  expect(window.scrollTo).toBeCalledTimes(11);
+
+  scrollHorizontally(element1, {});
+  jest.runAllTimers();
+  expect(window.scrollTo).toBeCalledTimes(21);
+});
+
+const mockGetBoundingClientRect = (overrides: Partial<ClientRect>) => () =>
+  ({
+    bottom: 0,
+    height: 0,
+    left: 0,
+    right: 0,
+    top: 0,
+    width: 0,
+    ...overrides,
+  } as DOMRect);
diff --git a/server/sonar-ui-common/helpers/__tests__/strings-test.ts b/server/sonar-ui-common/helpers/__tests__/strings-test.ts
new file mode 100644 (file)
index 0000000..2a9da6c
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { decodeJwt, latinize, slugify } from '../strings';
+
+describe('#decodeJwt', () => {
+  it('should correctly decode a jwt token', () => {
+    const claims = {
+      aud: 'ari:cloud:bitbucket::app/{327713ed-f1b2-4659-9c91-c8ecf8be7f3e}/sonarcloud-greg',
+      exp: 1541062205,
+      iat: 1541058605,
+      iss: 'ari:cloud:bitbucket::app/{327713ed-f1b2-4659-9c91-c8ecf8be7f3e}/sonarcloud-greg',
+      qsh: 'a6c93addd971c05d08da1e1669c2640fba529e98fbb5b2b9effadf00bf484277',
+    };
+    expect(
+      decodeJwt(
+        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhcmk6Y2xvdWQ6Yml0YnVja2V0OjphcHAvezMyNzcxM2VkLWYxYjItNDY1OS05YzkxLWM4ZWNmOGJlN2YzZX0vc29uYXJjbG91ZC1ncmVnIiwiaWF0IjoxNTQxMDU4NjA1LCJxc2giOiJhNmM5M2FkZGQ5NzFjMDVkMDhkYTFlMTY2OWMyNjQwZmJhNTI5ZTk4ZmJiNWIyYjllZmZhZGYwMGJmNDg0Mjc3IiwiYXVkIjoiYXJpOmNsb3VkOmJpdGJ1Y2tldDo6YXBwL3szMjc3MTNlZC1mMWIyLTQ2NTktOWM5MS1jOGVjZjhiZTdmM2V9L3NvbmFyY2xvdWQtZ3JlZyIsImV4cCI6MTU0MTA2MjIwNX0.5_0dFh_TPT_UorDewu2JEErgQE2ZnzBjvCDrOThseRo'
+      )
+    ).toEqual(claims);
+    expect(
+      decodeJwt(
+        'eyJpc3MiOiJhcmk6Y2xvdWQ6Yml0YnVja2V0OjphcHAvezMyNzcxM2VkLWYxYjItNDY1OS05YzkxLWM4ZWNmOGJlN2YzZX0vc29uYXJjbG91ZC1ncmVnIiwiaWF0IjoxNTQxMDU4NjA1LCJxc2giOiJhNmM5M2FkZGQ5NzFjMDVkMDhkYTFlMTY2OWMyNjQwZmJhNTI5ZTk4ZmJiNWIyYjllZmZhZGYwMGJmNDg0Mjc3IiwiYXVkIjoiYXJpOmNsb3VkOmJpdGJ1Y2tldDo6YXBwL3szMjc3MTNlZC1mMWIyLTQ2NTktOWM5MS1jOGVjZjhiZTdmM2V9L3NvbmFyY2xvdWQtZ3JlZyIsImV4cCI6MTU0MTA2MjIwNX0'
+      )
+    ).toEqual(claims);
+  });
+});
+
+describe('#latinize', () => {
+  it('should remove diacritics and replace them with normal letters', () => {
+    expect(latinize('âêîôûŵŷäëïöüẅÿàèìòùẁỳáéíóúẃýøāēīūčģķļņšž')).toBe(
+      'aeiouwyaeiouwyaeiouwyaeiouwyoaeiucgklnsz'
+    );
+    expect(latinize('ASDFGhjklQWERTz')).toBe('ASDFGhjklQWERTz');
+  });
+});
+
+describe('#slugify', () => {
+  it('should transform text into a slug', () => {
+    expect(slugify('Luke Sky&Walker')).toBe('luke-sky-and-walker');
+    expect(slugify('tèst_:-ng><@')).toBe('test-ng');
+    expect(slugify('my-valid-slug-1')).toBe('my-valid-slug-1');
+  });
+});
diff --git a/server/sonar-ui-common/helpers/__tests__/urls-test.ts b/server/sonar-ui-common/helpers/__tests__/urls-test.ts
new file mode 100644 (file)
index 0000000..171b066
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import Initializer from '../init';
+import { getPathUrlAsString, getReturnUrl, isRelativeUrl } from '../urls';
+
+const SIMPLE_COMPONENT_KEY = 'sonarqube';
+const COMPLEX_COMPONENT_KEY = 'org.sonarsource.sonarqube:sonarqube';
+const COMPLEX_COMPONENT_KEY_ENCODED = encodeURIComponent(COMPLEX_COMPONENT_KEY);
+
+afterEach(() => {
+  Initializer.setUrlContext('');
+});
+
+describe('#getPathUrlAsString', () => {
+  it('should return component url', () => {
+    expect(
+      getPathUrlAsString({ pathname: '/dashboard', query: { id: SIMPLE_COMPONENT_KEY } })
+    ).toBe('/dashboard?id=' + SIMPLE_COMPONENT_KEY);
+  });
+
+  it('should encode component key', () => {
+    expect(
+      getPathUrlAsString({ pathname: '/dashboard', query: { id: COMPLEX_COMPONENT_KEY } })
+    ).toBe('/dashboard?id=' + COMPLEX_COMPONENT_KEY_ENCODED);
+  });
+
+  it('should take baseUrl into account', () => {
+    Initializer.setUrlContext('/context');
+    expect(
+      getPathUrlAsString({ pathname: '/dashboard', query: { id: COMPLEX_COMPONENT_KEY } })
+    ).toBe('/context/dashboard?id=' + COMPLEX_COMPONENT_KEY_ENCODED);
+  });
+});
+
+describe('#getReturnUrl', () => {
+  it('should get the return url', () => {
+    expect(getReturnUrl({ query: { return_to: '/test' } })).toBe('/test');
+    expect(getReturnUrl({ query: { return_to: 'http://www.google.com' } })).toBe('/');
+    expect(getReturnUrl({})).toBe('/');
+  });
+});
+
+describe('#isRelativeUrl', () => {
+  it('should check a relative url', () => {
+    expect(isRelativeUrl('/test')).toBe(true);
+    expect(isRelativeUrl('http://www.google.com')).toBe(false);
+    expect(isRelativeUrl('javascript:alert("test")')).toBe(false);
+    expect(isRelativeUrl('\\test')).toBe(false);
+    expect(isRelativeUrl('//test')).toBe(false);
+  });
+});
+
+describe('#getHostUrl', () => {
+  beforeEach(() => {
+    jest.resetModules();
+  });
+  it('should return host url on client side', () => {
+    jest.mock('../init', () => ({
+      getUrlContext: () => '',
+      IS_SSR: false,
+    }));
+    const mockedUrls = require('../urls');
+    expect(mockedUrls.getHostUrl()).toBe('http://localhost');
+  });
+  it('should throw on server-side', () => {
+    jest.mock('../init', () => ({
+      getUrlContext: () => '',
+      IS_SSR: true,
+    }));
+    const mockedUrls = require('../urls');
+    expect(mockedUrls.getHostUrl).toThrowErrorMatchingInlineSnapshot(
+      `"No host url available on server side."`
+    );
+  });
+});
diff --git a/server/sonar-ui-common/helpers/colors.ts b/server/sonar-ui-common/helpers/colors.ts
new file mode 100644 (file)
index 0000000..0d48f01
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+/* eslint-disable no-bitwise, no-mixed-operators */
+export function stringToColor(str: string) {
+  let hash = 0;
+  for (let i = 0; i < str.length; i++) {
+    hash = str.charCodeAt(i) + ((hash << 5) - hash);
+  }
+  let color = '#';
+  for (let i = 0; i < 3; i++) {
+    const value = (hash >> (i * 8)) & 0xff;
+    color += ('00' + value.toString(16)).substr(-2);
+  }
+  return color;
+}
+
+export function isDarkColor(color: string) {
+  color = color.substr(1);
+  if (color.length === 3) {
+    // shortcut notation: #f90
+    color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
+  }
+  const rgb = parseInt(color.substr(1), 16);
+  const r = (rgb >> 16) & 0xff;
+  const g = (rgb >> 8) & 0xff;
+  const b = (rgb >> 0) & 0xff;
+  const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
+  return luma < 140;
+}
+
+export function getTextColor(background: string, dark = '#222', light = '#fff') {
+  return isDarkColor(background) ? light : dark;
+}
diff --git a/server/sonar-ui-common/helpers/cookies.ts b/server/sonar-ui-common/helpers/cookies.ts
new file mode 100644 (file)
index 0000000..42da1d8
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { memoize } from 'lodash';
+
+const parseCookies = memoize(
+  (documentCookie: string): T.Dict<string> => {
+    const rawCookies = documentCookie.split('; ');
+    const cookies: T.Dict<string> = {};
+    rawCookies.forEach((candidate) => {
+      const [key, value] = candidate.split('=');
+      cookies[key] = value;
+    });
+    return cookies;
+  }
+);
+
+export function getCookie(name: string): string | undefined {
+  return parseCookies(document.cookie)[name];
+}
diff --git a/server/sonar-ui-common/helpers/csv.ts b/server/sonar-ui-common/helpers/csv.ts
new file mode 100644 (file)
index 0000000..cd841d4
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+export function csvEscape(value: string): string {
+  const escaped = value.replace(/"/g, '\\"');
+  return `"${escaped}"`;
+}
diff --git a/server/sonar-ui-common/helpers/dates.ts b/server/sonar-ui-common/helpers/dates.ts
new file mode 100644 (file)
index 0000000..0661d62
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as parse from 'date-fns/parse';
+
+function pad(number: number) {
+  if (number < 10) {
+    return '0' + number.toString();
+  }
+  return number;
+}
+
+type ParsableDate = string | number | Date;
+
+export function parseDate(rawDate: ParsableDate): Date {
+  return parse(rawDate);
+}
+
+export function toShortNotSoISOString(rawDate: ParsableDate): string {
+  const date = parseDate(rawDate);
+  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
+}
+
+export function toNotSoISOString(rawDate: ParsableDate): string {
+  const date = parseDate(rawDate);
+  return date.toISOString().replace(/\..+Z$/, '+0000');
+}
+
+export function isValidDate(date: Date): boolean {
+  return !isNaN(date.getTime());
+}
diff --git a/server/sonar-ui-common/helpers/getHistory.ts b/server/sonar-ui-common/helpers/getHistory.ts
new file mode 100644 (file)
index 0000000..5daecc3
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { createHistory, History } from 'history';
+import { useRouterHistory } from 'react-router';
+import { getUrlContext } from './init';
+
+let history: History;
+
+function ensureHistory() {
+  // eslint-disable-next-line react-hooks/rules-of-hooks
+  history = useRouterHistory(createHistory)({
+    basename: getUrlContext(),
+  });
+  return history;
+}
+
+export default function getHistory() {
+  return history ? history : ensureHistory();
+}
diff --git a/server/sonar-ui-common/helpers/handleRequiredAuthentication.ts b/server/sonar-ui-common/helpers/handleRequiredAuthentication.ts
new file mode 100644 (file)
index 0000000..116102b
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import getHistory from './getHistory';
+
+export default function handleRequiredAuthentication() {
+  const history = getHistory();
+  const returnTo = window.location.pathname + window.location.search + window.location.hash;
+  // eslint-disable-next-line camelcase
+  history.replace({ pathname: '/sessions/new', query: { return_to: returnTo } });
+}
diff --git a/server/sonar-ui-common/helpers/init.ts b/server/sonar-ui-common/helpers/init.ts
new file mode 100644 (file)
index 0000000..42a8947
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import type { Messages } from './l10n';
+
+let urlContext: string; // Is the base path (web context) in SQ
+let messages: Messages | undefined;
+let locale: string | undefined;
+let reactDomContainerSelector: string | undefined; // CSS selector of the DOM node where the React App is attached
+
+export const IS_SSR = typeof window === 'undefined';
+export const DEFAULT_LOCALE = 'en';
+export const DEFAULT_MESSAGES = {
+  // eslint-disable-next-line camelcase
+  default_error_message: 'The request cannot be processed. Try again later.',
+};
+const LOGGER_PREFIX = 'sonar-ui-common init:';
+
+export default {
+  setUrlContext(newUrlContext: string) {
+    urlContext = newUrlContext;
+    return this;
+  },
+  setLocale(newLocale: string) {
+    locale = newLocale;
+    return this;
+  },
+  setMessages(newMessages: Messages) {
+    messages = newMessages;
+    return this;
+  },
+  setReactDomContainer(nodeSelector: string) {
+    reactDomContainerSelector = nodeSelector;
+    return this;
+  },
+};
+
+export function getMessages() {
+  if (typeof messages === 'undefined') {
+    logWarning('L10n messages are not initialized. Use default messages.');
+    return DEFAULT_MESSAGES;
+  }
+  return messages;
+}
+
+export function getLocale() {
+  if (typeof locale === 'undefined') {
+    logWarning('L10n locale is not initialized. Use default locale.');
+    return DEFAULT_LOCALE;
+  }
+  return locale;
+}
+
+export function getReactDomContainerSelector() {
+  return reactDomContainerSelector || '#content';
+}
+
+export function getUrlContext() {
+  if (typeof urlContext === 'undefined') {
+    throw new Error(
+      `${LOGGER_PREFIX} web context needs to be initialized by Initializer.setUrlContext before being used`
+    );
+  }
+  return urlContext;
+}
+
+function logWarning(message: string) {
+  if (process.env.NODE_ENV !== 'production') {
+    // eslint-disable-next-line no-console
+    console.warn(`${LOGGER_PREFIX} ${message}`);
+  }
+}
diff --git a/server/sonar-ui-common/helpers/keycodes.ts b/server/sonar-ui-common/helpers/keycodes.ts
new file mode 100644 (file)
index 0000000..5e60693
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+export enum KeyCodes {
+  LeftArrow = 37,
+  UpArrow = 38,
+  RightArrow = 39,
+  DownArrow = 40,
+
+  Alt = 18,
+  Backspace = 8,
+  CapsLock = 20,
+  Command = 93,
+  Ctrl = 17,
+  Delete = 46,
+  End = 35,
+  Enter = 13,
+  Escape = 27,
+  Home = 36,
+  PageDown = 34,
+  PageUp = 33,
+  Shift = 16,
+  Space = 32,
+  Tab = 9,
+
+  A = 65,
+  B = 66,
+  C = 67,
+  D = 68,
+  E = 69,
+  F = 70,
+  G = 71,
+  H = 72,
+  I = 73,
+  J = 74,
+  K = 75,
+  L = 76,
+  M = 77,
+  N = 78,
+  O = 79,
+  P = 80,
+  Q = 81,
+  R = 82,
+  S = 83,
+  T = 84,
+  U = 85,
+  V = 86,
+  W = 87,
+  X = 88,
+  Y = 89,
+  Z = 90,
+}
diff --git a/server/sonar-ui-common/helpers/l10n.ts b/server/sonar-ui-common/helpers/l10n.ts
new file mode 100644 (file)
index 0000000..aa8c5a8
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { getLocale, getMessages } from './init';
+
+export type Messages = T.Dict<string>;
+
+export function translate(...keys: string[]): string {
+  const messageKey = keys.join('.');
+  const l10nMessages = getMessages();
+  if (process.env.NODE_ENV === 'development' && !l10nMessages[messageKey]) {
+    // eslint-disable-next-line no-console
+    console.error(`No message for: ${messageKey}`);
+  }
+  return l10nMessages[messageKey] || messageKey;
+}
+
+export function translateWithParameters(
+  messageKey: string,
+  ...parameters: Array<string | number>
+): string {
+  const message = getMessages()[messageKey];
+  if (message) {
+    return parameters
+      .map((parameter) => String(parameter))
+      .reduce((acc, parameter, index) => acc.replace(`{${index}}`, () => parameter), message);
+  } else {
+    if (process.env.NODE_ENV === 'development') {
+      // eslint-disable-next-line no-console
+      console.error(`No message for: ${messageKey}`);
+    }
+    return `${messageKey}.${parameters.join('.')}`;
+  }
+}
+
+export function hasMessage(...keys: string[]): boolean {
+  const messageKey = keys.join('.');
+  return getMessages()[messageKey] != null;
+}
+
+export function getLocalizedMetricName(
+  metric: { key: string; name?: string },
+  short = false
+): string {
+  const bundleKey = `metric.${metric.key}.${short ? 'short_name' : 'name'}`;
+  if (hasMessage(bundleKey)) {
+    return translate(bundleKey);
+  } else if (short) {
+    return getLocalizedMetricName(metric);
+  } else {
+    return metric.name || metric.key;
+  }
+}
+
+export function getLocalizedCategoryMetricName(metric: { key: string; name?: string }) {
+  const bundleKey = `metric.${metric.key}.extra_short_name`;
+  return hasMessage(bundleKey) ? translate(bundleKey) : getLocalizedMetricName(metric, true);
+}
+
+export function getLocalizedMetricDomain(domainName: string) {
+  const bundleKey = `metric_domain.${domainName}`;
+  return hasMessage(bundleKey) ? translate(bundleKey) : domainName;
+}
+
+export function getCurrentLocale() {
+  return getLocale();
+}
+
+export function getShortMonthName(index: number) {
+  const months = [
+    'Jan',
+    'Feb',
+    'Mar',
+    'Apr',
+    'May',
+    'Jun',
+    'Jul',
+    'Aug',
+    'Sep',
+    'Oct',
+    'Nov',
+    'Dec',
+  ];
+  return translate(months[index]);
+}
+
+export function getWeekDayName(index: number) {
+  const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+  return weekdays[index] ? translate(weekdays[index]) : '';
+}
+
+export function getShortWeekDayName(index: number) {
+  const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
+  return weekdays[index] ? translate(weekdays[index]) : '';
+}
diff --git a/server/sonar-ui-common/helpers/measures.ts b/server/sonar-ui-common/helpers/measures.ts
new file mode 100644 (file)
index 0000000..de4b3b2
--- /dev/null
@@ -0,0 +1,341 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { getCurrentLocale, translate, translateWithParameters } from './l10n';
+
+const HOURS_IN_DAY = 8;
+
+interface Formatter {
+  (value: string | number, options?: any): string;
+}
+
+/** Format a measure value for a given type */
+export function formatMeasure(
+  value: string | number | undefined,
+  type: string,
+  options?: any
+): string {
+  const formatter = getFormatter(type);
+  // eslint-disable-next-line react-hooks/rules-of-hooks
+  return useFormatter(value, formatter, options);
+}
+
+/** Return a localized metric name */
+export function localizeMetric(metricKey: string): string {
+  return translate('metric', metricKey, 'name');
+}
+
+/** Return corresponding "short" for better display in UI */
+export function getShortType(type: string): string {
+  if (type === 'INT') {
+    return 'SHORT_INT';
+  } else if (type === 'WORK_DUR') {
+    return 'SHORT_WORK_DUR';
+  }
+  return type;
+}
+
+/** Check if metric is differential */
+export function isDiffMetric(metricKey: string): boolean {
+  return metricKey.indexOf('new_') === 0;
+}
+
+/*
+ * Conditional decimal count for QualityGate-impacting measures
+ * (e.g. Coverage %)
+ * Increase the number of decimals if the value is close to the threshold
+ * We count the precision (number of 0's, i.e. log10 and round down) needed to show the difference
+ * E.g. threshold 85, value 84.9993 -> delta = 0.0007, we need 4 decimals to see the difference
+ * otherwise rounding will make it look like they are equal.
+ */
+const DEFAULT_DECIMALS = 1;
+export function getMinDecimalsCountToBeDistinctFromThreshold(
+  value: number,
+  threshold: number | undefined
+): number {
+  if (!threshold) {
+    return DEFAULT_DECIMALS;
+  }
+  const delta = Math.abs(threshold - value);
+  if (delta < 0.1 && delta > 0) {
+    return -Math.floor(Math.log10(delta));
+  } else {
+    return DEFAULT_DECIMALS;
+  }
+}
+
+function useFormatter(
+  value: string | number | undefined,
+  formatter: Formatter,
+  options?: any
+): string {
+  return value !== undefined && value !== '' ? formatter(value, options) : '';
+}
+
+function getFormatter(type: string): Formatter {
+  const FORMATTERS: T.Dict<Formatter> = {
+    INT: intFormatter,
+    SHORT_INT: shortIntFormatter,
+    FLOAT: floatFormatter,
+    PERCENT: percentFormatter,
+    WORK_DUR: durationFormatter,
+    SHORT_WORK_DUR: shortDurationFormatter,
+    RATING: ratingFormatter,
+    LEVEL: levelFormatter,
+    MILLISEC: millisecondsFormatter,
+  };
+  return FORMATTERS[type] || noFormatter;
+}
+
+function numberFormatter(
+  value: string | number,
+  minimumFractionDigits = 0,
+  maximumFractionDigits = minimumFractionDigits
+) {
+  const { format } = new Intl.NumberFormat(getCurrentLocale(), {
+    minimumFractionDigits,
+    maximumFractionDigits,
+  });
+  if (typeof value === 'string') {
+    return format(parseFloat(value));
+  }
+  return format(value);
+}
+
+function noFormatter(value: string | number): string | number {
+  return value;
+}
+
+function intFormatter(value: string | number): string {
+  return numberFormatter(value);
+}
+
+const shortIntFormats = [
+  { unit: 1e10, formatUnit: 1e9, fraction: 0, suffix: 'short_number_suffix.g' },
+  { unit: 1e9, formatUnit: 1e9, fraction: 1, suffix: 'short_number_suffix.g' },
+  { unit: 1e7, formatUnit: 1e6, fraction: 0, suffix: 'short_number_suffix.m' },
+  { unit: 1e6, formatUnit: 1e6, fraction: 1, suffix: 'short_number_suffix.m' },
+  { unit: 1e4, formatUnit: 1e3, fraction: 0, suffix: 'short_number_suffix.k' },
+  { unit: 1e3, formatUnit: 1e3, fraction: 1, suffix: 'short_number_suffix.k' },
+];
+
+function shortIntFormatter(
+  value: string | number,
+  option?: { roundingFunc?: (x: number) => number }
+): string {
+  const roundingFunc = (option && option.roundingFunc) || undefined;
+  if (typeof value === 'string') {
+    value = parseFloat(value);
+  }
+  for (let i = 0; i < shortIntFormats.length; i++) {
+    const { unit, formatUnit, fraction, suffix } = shortIntFormats[i];
+    const nextFraction = unit / (shortIntFormats[i + 1] ? shortIntFormats[i + 1].unit / 10 : 1);
+    const roundedValue = numberRound(value / unit, nextFraction, roundingFunc);
+    if (roundedValue >= 1) {
+      return (
+        numberFormatter(
+          numberRound(value / formatUnit, Math.pow(10, fraction), roundingFunc),
+          0,
+          fraction
+        ) + translate(suffix)
+      );
+    }
+  }
+
+  return numberFormatter(value);
+}
+
+function numberRound(
+  value: number,
+  fraction: number = 1000,
+  roundingFunc: (x: number) => number = Math.round
+) {
+  return roundingFunc(value * fraction) / fraction;
+}
+
+function floatFormatter(value: string | number): string {
+  return numberFormatter(value, 1, 5);
+}
+
+function percentFormatter(
+  value: string | number,
+  { decimals, omitExtraDecimalZeros }: { decimals?: number; omitExtraDecimalZeros?: boolean } = {}
+): string {
+  if (typeof value === 'string') {
+    value = parseFloat(value);
+  }
+  if (value === 100) {
+    return '100%';
+  } else if (omitExtraDecimalZeros && decimals) {
+    // If omitExtraDecimalZeros is true, all trailing decimal 0s will be removed,
+    // except for the first decimal.
+    // E.g. for decimals=3:
+    // - omitExtraDecimalZeros: false, value: 45.450 => 45.450
+    // - omitExtraDecimalZeros: true, value: 45.450 => 45.45
+    // - omitExtraDecimalZeros: false, value: 85 => 85.000
+    // - omitExtraDecimalZeros: true, value: 85 => 85.0
+    return `${numberFormatter(value, 1, decimals)}%`;
+  } else {
+    return `${numberFormatter(value, decimals || 1)}%`;
+  }
+}
+
+function ratingFormatter(value: string | number): string {
+  if (typeof value === 'string') {
+    value = parseInt(value, 10);
+  }
+  return String.fromCharCode(97 + value - 1).toUpperCase();
+}
+
+function levelFormatter(value: string | number): string {
+  if (typeof value === 'number') {
+    value = value.toString();
+  }
+  const l10nKey = `metric.level.${value}`;
+  const result = translate(l10nKey);
+
+  // if couldn't translate, return the initial value
+  return l10nKey !== result ? result : value;
+}
+
+function millisecondsFormatter(value: string | number): string {
+  if (typeof value === 'string') {
+    value = parseInt(value, 10);
+  }
+  const ONE_SECOND = 1000;
+  const ONE_MINUTE = 60 * ONE_SECOND;
+  if (value >= ONE_MINUTE) {
+    const minutes = Math.round(value / ONE_MINUTE);
+    return `${minutes}min`;
+  } else if (value >= ONE_SECOND) {
+    const seconds = Math.round(value / ONE_SECOND);
+    return `${seconds}s`;
+  } else {
+    return `${value}ms`;
+  }
+}
+
+/*
+ * Debt Formatters
+ */
+
+function shouldDisplayDays(days: number): boolean {
+  return days > 0;
+}
+
+function shouldDisplayDaysInShortFormat(days: number): boolean {
+  return days > 0.9;
+}
+
+function shouldDisplayHours(days: number, hours: number): boolean {
+  return hours > 0 && days < 10;
+}
+
+function shouldDisplayHoursInShortFormat(hours: number): boolean {
+  return hours > 0.9;
+}
+
+function shouldDisplayMinutes(days: number, hours: number, minutes: number): boolean {
+  return minutes > 0 && hours < 10 && days === 0;
+}
+
+function addSpaceIfNeeded(value: string): string {
+  return value.length > 0 ? `${value} ` : value;
+}
+
+function formatDuration(isNegative: boolean, days: number, hours: number, minutes: number): string {
+  let formatted = '';
+  if (shouldDisplayDays(days)) {
+    formatted += translateWithParameters('work_duration.x_days', isNegative ? -1 * days : days);
+  }
+  if (shouldDisplayHours(days, hours)) {
+    formatted = addSpaceIfNeeded(formatted);
+    formatted += translateWithParameters(
+      'work_duration.x_hours',
+      isNegative && formatted.length === 0 ? -1 * hours : hours
+    );
+  }
+  if (shouldDisplayMinutes(days, hours, minutes)) {
+    formatted = addSpaceIfNeeded(formatted);
+    formatted += translateWithParameters(
+      'work_duration.x_minutes',
+      isNegative && formatted.length === 0 ? -1 * minutes : minutes
+    );
+  }
+  return formatted;
+}
+
+function formatDurationShort(
+  isNegative: boolean,
+  days: number,
+  hours: number,
+  minutes: number
+): string {
+  if (shouldDisplayDaysInShortFormat(days)) {
+    const roundedDays = Math.round(days);
+    const formattedDays = formatMeasure(isNegative ? -1 * roundedDays : roundedDays, 'SHORT_INT');
+    return translateWithParameters('work_duration.x_days', formattedDays);
+  }
+
+  if (shouldDisplayHoursInShortFormat(hours)) {
+    const roundedHours = Math.round(hours);
+    const formattedHours = formatMeasure(
+      isNegative ? -1 * roundedHours : roundedHours,
+      'SHORT_INT'
+    );
+    return translateWithParameters('work_duration.x_hours', formattedHours);
+  }
+
+  const formattedMinutes = formatMeasure(isNegative ? -1 * minutes : minutes, 'SHORT_INT');
+  return translateWithParameters('work_duration.x_minutes', formattedMinutes);
+}
+
+function durationFormatter(value: string | number): string {
+  if (typeof value === 'string') {
+    value = parseInt(value, 10);
+  }
+  if (value === 0) {
+    return '0';
+  }
+  const hoursInDay = HOURS_IN_DAY;
+  const isNegative = value < 0;
+  const absValue = Math.abs(value);
+  const days = Math.floor(absValue / hoursInDay / 60);
+  let remainingValue = absValue - days * hoursInDay * 60;
+  const hours = Math.floor(remainingValue / 60);
+  remainingValue -= hours * 60;
+  return formatDuration(isNegative, days, hours, remainingValue);
+}
+
+function shortDurationFormatter(value: string | number): string {
+  if (typeof value === 'string') {
+    value = parseInt(value, 10);
+  }
+  if (value === 0) {
+    return '0';
+  }
+  const hoursInDay = HOURS_IN_DAY;
+  const isNegative = value < 0;
+  const absValue = Math.abs(value);
+  const days = absValue / hoursInDay / 60;
+  let remainingValue = absValue - Math.floor(days) * hoursInDay * 60;
+  const hours = remainingValue / 60;
+  remainingValue -= Math.floor(hours) * 60;
+  return formatDurationShort(isNegative, days, hours, remainingValue);
+}
diff --git a/server/sonar-ui-common/helpers/pages.ts b/server/sonar-ui-common/helpers/pages.ts
new file mode 100644 (file)
index 0000000..36ba560
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+const CLASS_SIDEBAR_PAGE = 'sidebar-page';
+const CLASS_WHITE_PAGE = 'white-page';
+const CLASS_NO_FOOTER_PAGE = 'no-footer-page';
+
+export function addSideBarClass() {
+  toggleBodyClass(CLASS_SIDEBAR_PAGE, true);
+}
+
+export function addWhitePageClass() {
+  toggleBodyClass(CLASS_WHITE_PAGE, true);
+}
+
+export function addNoFooterPageClass() {
+  /* eslint-disable-next-line no-console */
+  console.warn('DEPRECATED: addNoFooterPageClass() was deprecated.');
+  toggleBodyClass(CLASS_NO_FOOTER_PAGE, true);
+}
+
+export function removeSideBarClass() {
+  toggleBodyClass(CLASS_SIDEBAR_PAGE, false);
+}
+
+export function removeWhitePageClass() {
+  toggleBodyClass(CLASS_WHITE_PAGE, false);
+}
+
+export function removeNoFooterPageClass() {
+  /* eslint-disable-next-line no-console */
+  console.warn('DEPRECATED: removeNoFooterPageClass() was deprecated.');
+  toggleBodyClass(CLASS_NO_FOOTER_PAGE, false);
+}
+
+function toggleBodyClass(className: string, force?: boolean) {
+  document.body.classList.toggle(className, force);
+  if (document.documentElement) {
+    document.documentElement.classList.toggle(className, force);
+  }
+}
diff --git a/server/sonar-ui-common/helpers/path.ts b/server/sonar-ui-common/helpers/path.ts
new file mode 100644 (file)
index 0000000..dbd7b9a
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+export function collapsePath(path: string, limit = 30): string {
+  if (typeof path !== 'string') {
+    return '';
+  }
+
+  const tokens = path.split('/');
+
+  if (tokens.length <= 2) {
+    return path;
+  }
+
+  const head = tokens[0];
+  const tail = tokens[tokens.length - 1];
+  const middle = tokens.slice(1, -1);
+  let cut = false;
+
+  while (middle.join().length > limit && middle.length > 0) {
+    middle.shift();
+    cut = true;
+  }
+
+  const body = [head, ...(cut ? ['...'] : []), ...middle, tail];
+  return body.join('/');
+}
+
+/**
+ * Return a collapsed path without a file name
+ * @example
+ * // returns 'src/.../js/components/navigator/app/models/'
+ * collapsedDirFromPath('src/main/js/components/navigator/app/models/state.js')
+ */
+export function collapsedDirFromPath(path: string | null): string | null {
+  const limit = 30;
+  if (typeof path === 'string') {
+    const tokens = path.split('/').slice(0, -1);
+    if (tokens.length > 2) {
+      const head = tokens[0];
+      const tail = tokens[tokens.length - 1];
+      const middle = tokens.slice(1, -1);
+
+      let cut = false;
+      while (middle.join().length > limit && middle.length > 0) {
+        middle.shift();
+        cut = true;
+      }
+      const body = [head, ...(cut ? ['...'] : []), ...middle, tail];
+      return body.join('/') + '/';
+    } else {
+      return tokens.join('/') + '/';
+    }
+  } else {
+    return null;
+  }
+}
+
+/**
+ * Return a file name for a given file path
+ * * @example
+ * // returns 'state.js'
+ * collapsedDirFromPath('src/main/js/components/navigator/app/models/state.js')
+ */
+export function fileFromPath(path: string | null): string | null {
+  if (typeof path === 'string') {
+    const tokens = path.split('/');
+    return tokens[tokens.length - 1];
+  } else {
+    return null;
+  }
+}
+
+export function splitPath(path: string) {
+  const tokens = path.split('/');
+  return {
+    head: tokens.slice(0, -1).join('/'),
+    tail: tokens[tokens.length - 1],
+  };
+}
+
+export function cutLongWords(str: string, limit = 30) {
+  return str
+    .split(' ')
+    .map((word) => (word.length > limit ? word.substr(0, limit) + '...' : word))
+    .join(' ');
+}
+
+export function limitComponentName(str: string, limit = 30): string {
+  if (typeof str === 'string') {
+    return str.length > limit ? str.substr(0, limit) + '...' : str;
+  } else {
+    return '';
+  }
+}
diff --git a/server/sonar-ui-common/helpers/query.ts b/server/sonar-ui-common/helpers/query.ts
new file mode 100644 (file)
index 0000000..e1f4c6d
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { isEqual, isNil, omitBy } from 'lodash';
+import { isValidDate, parseDate, toNotSoISOString, toShortNotSoISOString } from './dates';
+
+export function queriesEqual(a: T.RawQuery, b: T.RawQuery): boolean {
+  const keysA = Object.keys(a);
+  const keysB = Object.keys(b);
+
+  if (keysA.length !== keysB.length) {
+    return false;
+  }
+
+  return keysA.every((key) => isEqual(a[key], b[key]));
+}
+
+export function cleanQuery(query: T.RawQuery): T.RawQuery {
+  return omitBy(query, isNil);
+}
+
+export function parseAsBoolean(value: string | undefined, defaultValue = true): boolean {
+  if (value === 'false') {
+    return false;
+  }
+  if (value === 'true') {
+    return true;
+  }
+  return defaultValue;
+}
+
+export function parseAsOptionalBoolean(value: string | undefined): boolean | undefined {
+  if (value === 'true') {
+    return true;
+  } else if (value === 'false') {
+    return false;
+  } else {
+    return undefined;
+  }
+}
+
+export function parseAsDate(value?: string): Date | undefined {
+  if (value) {
+    const date = parseDate(value);
+    if (isValidDate(date)) {
+      return date;
+    }
+  }
+  return undefined;
+}
+
+export function parseAsString(value: string | undefined): string {
+  return value || '';
+}
+
+export function parseAsOptionalString(value: string | undefined): string | undefined {
+  return value || undefined;
+}
+
+export function parseAsArray<T>(value: string | undefined, itemParser: (x: string) => T): T[] {
+  return value ? value.split(',').map(itemParser) : [];
+}
+
+export function parseAsOptionalArray<T>(
+  value: string | undefined,
+  itemParser: (x: string) => T
+): T[] | undefined {
+  return value ? parseAsArray(value, itemParser) : undefined;
+}
+
+export function serializeDate(value?: Date, serializer = toNotSoISOString): string | undefined {
+  if (value != null && value.toISOString) {
+    return serializer(value);
+  }
+  return undefined;
+}
+
+export function serializeDateShort(value: Date | undefined): string | undefined {
+  return serializeDate(value, toShortNotSoISOString);
+}
+
+export function serializeString(value: string | undefined): string | undefined {
+  return value || undefined;
+}
+
+export function serializeStringArray(value: string[] | undefined[]): string | undefined {
+  return value && value.length ? value.join() : undefined;
+}
+
+export function serializeOptionalBoolean(value: boolean | undefined): string | undefined {
+  if (value === true) {
+    return 'true';
+  } else if (value === false) {
+    return 'false';
+  } else {
+    return undefined;
+  }
+}
diff --git a/server/sonar-ui-common/helpers/ratings.ts b/server/sonar-ui-common/helpers/ratings.ts
new file mode 100644 (file)
index 0000000..29b942d
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+function checkNumberRating(coverageRating: number): void {
+  if (!(typeof coverageRating === 'number' && coverageRating > 0 && coverageRating < 6)) {
+    throw new Error(`Unknown number rating: "${coverageRating}"`);
+  }
+}
+
+export function getCoverageRatingLabel(rating: number): string {
+  checkNumberRating(rating);
+  const mapping = ['≥ 80%', '70% - 80%', '50% - 70%', '30% - 50%', '< 30%'];
+  return mapping[rating - 1];
+}
+
+export function getCoverageRatingAverageValue(rating: number): number {
+  checkNumberRating(rating);
+  const mapping = [90, 75, 60, 40, 15];
+  return mapping[rating - 1];
+}
+
+export function getDuplicationsRatingLabel(rating: number): string {
+  checkNumberRating(rating);
+  const mapping = ['< 3%', '3% - 5%', '5% - 10%', '10% - 20%', '> 20%'];
+  return mapping[rating - 1];
+}
+
+export function getDuplicationsRatingAverageValue(rating: number): number {
+  checkNumberRating(rating);
+  const mapping = [1.5, 4, 7.5, 15, 30];
+  return mapping[rating - 1];
+}
+
+export function getSizeRatingLabel(rating: number): string {
+  checkNumberRating(rating);
+  const mapping = ['< 1k', '1k - 10k', '10k - 100k', '100k - 500k', '> 500k'];
+  return mapping[rating - 1];
+}
+
+export function getSizeRatingAverageValue(rating: number): number {
+  checkNumberRating(rating);
+  const mapping = [500, 5000, 50000, 250000, 750000];
+  return mapping[rating - 1];
+}
diff --git a/server/sonar-ui-common/helpers/request.ts b/server/sonar-ui-common/helpers/request.ts
new file mode 100644 (file)
index 0000000..0ed88c6
--- /dev/null
@@ -0,0 +1,337 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { isNil, omitBy } from 'lodash';
+import { stringify } from 'querystring';
+import { getCookie } from './cookies';
+import { getUrlContext } from './init';
+import { translate } from './l10n';
+
+export function getCSRFTokenName(): string {
+  return 'X-XSRF-TOKEN';
+}
+
+export function getCSRFTokenValue(): string {
+  const cookieName = 'XSRF-TOKEN';
+  const cookieValue = getCookie(cookieName);
+  if (!cookieValue) {
+    return '';
+  }
+  return cookieValue;
+}
+
+/**
+ * Return an object containing a special http request header used to prevent CSRF attacks.
+ */
+export function getCSRFToken(): T.Dict<string> {
+  // Fetch API in Edge doesn't work with empty header,
+  // so we ensure non-empty value
+  const value = getCSRFTokenValue();
+  return value ? { [getCSRFTokenName()]: value } : {};
+}
+
+export type RequestData = T.Dict<any>;
+
+export function omitNil(obj: RequestData): RequestData {
+  return omitBy(obj, isNil);
+}
+
+/**
+ * Default options for any request
+ */
+const DEFAULT_OPTIONS: {
+  credentials: RequestCredentials;
+  method: string;
+} = {
+  credentials: 'same-origin',
+  method: 'GET',
+};
+
+/**
+ * Default request headers
+ */
+const DEFAULT_HEADERS = {
+  Accept: 'application/json',
+};
+
+/**
+ * Request
+ */
+class Request {
+  private data?: RequestData;
+  private isJSON = false;
+
+  constructor(private readonly url: string, private readonly options: { method?: string } = {}) {}
+
+  getSubmitData(customHeaders: any = {}): { url: string; options: RequestInit } {
+    let { url } = this;
+    const options: RequestInit = { ...DEFAULT_OPTIONS, ...this.options };
+
+    if (this.data) {
+      if (this.data instanceof FormData) {
+        options.body = this.data;
+      } else if (this.isJSON) {
+        customHeaders['Content-Type'] = 'application/json';
+        options.body = JSON.stringify(this.data);
+      } else {
+        const strData = stringify(omitNil(this.data));
+        if (options.method === 'GET') {
+          url += '?' + strData;
+        } else {
+          customHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
+          options.body = strData;
+        }
+      }
+    }
+
+    options.headers = {
+      ...DEFAULT_HEADERS,
+      ...customHeaders,
+    };
+    return { url, options };
+  }
+
+  submit(): Promise<Response> {
+    const { url, options } = this.getSubmitData({ ...getCSRFToken() });
+    return window.fetch(getUrlContext() + url, options);
+  }
+
+  setMethod(method: string): Request {
+    this.options.method = method;
+    return this;
+  }
+
+  setData(data?: RequestData, isJSON = false): Request {
+    if (data) {
+      this.data = data;
+      this.isJSON = isJSON;
+    }
+    return this;
+  }
+}
+
+/**
+ * Make a request
+ */
+export function request(url: string): Request {
+  return new Request(url);
+}
+
+/**
+ * Make a cors request
+ */
+export function corsRequest(url: string, mode: RequestMode = 'cors'): Request {
+  const options: RequestInit = { mode };
+  const request = new Request(url, options);
+  request.submit = function () {
+    const { url, options } = this.getSubmitData();
+    return window.fetch(url, options);
+  };
+  return request;
+}
+
+/**
+ * Check that response status is ok
+ */
+export function checkStatus(response: Response, bypassRedirect?: boolean): Promise<Response> {
+  return new Promise((resolve, reject) => {
+    if (response.status === HttpStatus.Unauthorized && !bypassRedirect) {
+      import('./handleRequiredAuthentication')
+        .then((i) => i.default())
+        .then(reject, reject);
+    } else if (isSuccessStatus(response.status)) {
+      resolve(response);
+    } else {
+      reject(response);
+    }
+  });
+}
+
+/**
+ * Parse response as JSON
+ */
+export function parseJSON(response: Response): Promise<any> {
+  return response.json();
+}
+
+/**
+ * Parse response as Text
+ */
+export function parseText(response: Response): Promise<string> {
+  return response.text();
+}
+
+/**
+ * Parse response of failed request
+ */
+export function parseError(response: Response): Promise<string> {
+  const DEFAULT_MESSAGE = translate('default_error_message');
+  return parseJSON(response)
+    .then(({ errors }) => errors.map((error: any) => error.msg).join('. '))
+    .catch(() => DEFAULT_MESSAGE);
+}
+
+/**
+ * Shortcut to do a GET request and return a Response
+ */
+export function get(url: string, data?: RequestData, bypassRedirect?: boolean): Promise<Response> {
+  return request(url)
+    .setData(data)
+    .submit()
+    .then((response) => checkStatus(response, bypassRedirect));
+}
+
+/**
+ * Shortcut to do a GET request and return response json
+ */
+export function getJSON(url: string, data?: RequestData, bypassRedirect?: boolean): Promise<any> {
+  return get(url, data, bypassRedirect).then(parseJSON);
+}
+
+/**
+ * Shortcut to do a GET request and return response text
+ */
+export function getText(
+  url: string,
+  data?: RequestData,
+  bypassRedirect?: boolean
+): Promise<string> {
+  return get(url, data, bypassRedirect).then(parseText);
+}
+
+/**
+ * Shortcut to do a CORS GET request and return response json
+ */
+export function getCorsJSON(url: string, data?: RequestData): Promise<any> {
+  return corsRequest(url)
+    .setData(data)
+    .submit()
+    .then((response) => {
+      if (isSuccessStatus(response.status)) {
+        return parseJSON(response);
+      } else {
+        return Promise.reject(response);
+      }
+    });
+}
+
+/**
+ * Shortcut to do a POST request and return response json
+ */
+export function postJSON(url: string, data?: RequestData, bypassRedirect?: boolean): Promise<any> {
+  return request(url)
+    .setMethod('POST')
+    .setData(data)
+    .submit()
+    .then((response) => checkStatus(response, bypassRedirect))
+    .then(parseJSON);
+}
+
+/**
+ * Shortcut to do a POST request with a json body and return response json
+ */
+export function postJSONBody(
+  url: string,
+  data?: RequestData,
+  bypassRedirect?: boolean
+): Promise<any> {
+  return request(url)
+    .setMethod('POST')
+    .setData(data, true)
+    .submit()
+    .then((response) => checkStatus(response, bypassRedirect))
+    .then(parseJSON);
+}
+
+/**
+ * Shortcut to do a POST request
+ */
+export function post(url: string, data?: RequestData, bypassRedirect?: boolean): Promise<void> {
+  return new Promise((resolve, reject) => {
+    request(url)
+      .setMethod('POST')
+      .setData(data)
+      .submit()
+      .then((response) => checkStatus(response, bypassRedirect))
+      .then(() => resolve(), reject);
+  });
+}
+
+function tryRequestAgain<T>(
+  repeatAPICall: () => Promise<T>,
+  tries: { max: number; slowThreshold: number },
+  stopRepeat: (response: T) => boolean,
+  repeatErrors: number[] = [],
+  lastError?: Response
+) {
+  tries.max--;
+  if (tries.max !== 0) {
+    return new Promise<T>((resolve) => {
+      setTimeout(
+        () => resolve(requestTryAndRepeatUntil(repeatAPICall, tries, stopRepeat, repeatErrors)),
+        tries.max > tries.slowThreshold ? 500 : 3000
+      );
+    });
+  }
+  return Promise.reject(lastError);
+}
+
+export function requestTryAndRepeatUntil<T>(
+  repeatAPICall: () => Promise<T>,
+  tries: { max: number; slowThreshold: number },
+  stopRepeat: (response: T) => boolean,
+  repeatErrors: number[] = []
+) {
+  return repeatAPICall().then(
+    (r) => {
+      if (stopRepeat(r)) {
+        return r;
+      }
+      return tryRequestAgain(repeatAPICall, tries, stopRepeat, repeatErrors);
+    },
+    (error: Response) => {
+      if (repeatErrors.length === 0 || repeatErrors.includes(error.status)) {
+        return tryRequestAgain(repeatAPICall, tries, stopRepeat, repeatErrors, error);
+      }
+      return Promise.reject(error);
+    }
+  );
+}
+
+export function isSuccessStatus(status: number) {
+  return status >= 200 && status < 300;
+}
+
+// Adapted from https://nodejs.org/api/http.html#http_http_HTTP_STATUS
+export enum HttpStatus {
+  Ok = 200,
+  Created = 201,
+  MultipleChoices = 300,
+  MovedPermanently = 301,
+  Found = 302,
+  BadRequest = 400,
+  Unauthorized = 401,
+  Forbidden = 403,
+  NotFound = 404,
+  InternalServerError = 500,
+  NotImplemented = 501,
+  BadGateway = 502,
+  ServiceUnavailable = 503,
+  GatewayTimeout = 504,
+}
diff --git a/server/sonar-ui-common/helpers/scrolling.ts b/server/sonar-ui-common/helpers/scrolling.ts
new file mode 100644 (file)
index 0000000..6f08703
--- /dev/null
@@ -0,0 +1,196 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+const SCROLLING_DURATION = 100;
+const SCROLLING_INTERVAL = 10;
+const SCROLLING_STEPS = SCROLLING_DURATION / SCROLLING_INTERVAL;
+
+function isWindow(element: Element | Window): element is Window {
+  return element === window;
+}
+
+function getScroll(element: Element | Window) {
+  return isWindow(element)
+    ? { x: window.pageXOffset, y: window.pageYOffset }
+    : { x: element.scrollLeft, y: element.scrollTop };
+}
+
+function scrollElement(element: Element | Window, x: number, y: number): Promise<void> {
+  if (isWindow(element)) {
+    window.scrollTo(x, y);
+  } else {
+    element.scrollLeft = x;
+    element.scrollTop = y;
+  }
+  return Promise.resolve();
+}
+
+function smoothScroll(
+  target: number,
+  current: number,
+  scroll: (position: number) => void
+): Promise<void> {
+  const positiveDirection = target > current;
+  const step = Math.ceil(Math.abs(target - current) / SCROLLING_STEPS);
+  let stepsDone = 0;
+
+  return new Promise((resolve) => {
+    const interval = setInterval(() => {
+      if (current === target || SCROLLING_STEPS === stepsDone) {
+        clearInterval(interval);
+        resolve();
+      } else {
+        let goal;
+        if (positiveDirection) {
+          goal = Math.min(target, current + step);
+        } else {
+          goal = Math.max(target, current - step);
+        }
+        stepsDone++;
+        current = goal;
+        scroll(goal);
+      }
+    }, SCROLLING_INTERVAL);
+  });
+}
+
+function smoothScrollTop(parent: Element | Window, position: number) {
+  const scroll = getScroll(parent);
+  return smoothScroll(position, scroll.y, (position) => scrollElement(parent, scroll.x, position));
+}
+
+function smoothScrollLeft(parent: Element | Window, position: number) {
+  const scroll = getScroll(parent);
+  return smoothScroll(position, scroll.x, (position) => scrollElement(parent, position, scroll.y));
+}
+
+export function scrollToElement(
+  element: Element,
+  options: {
+    topOffset?: number;
+    bottomOffset?: number;
+    parent?: Element;
+    smooth?: boolean;
+  }
+): void {
+  const opts = { topOffset: 0, bottomOffset: 0, parent: window, smooth: true, ...options };
+  const { parent } = opts;
+
+  const { top, bottom } = element.getBoundingClientRect();
+
+  const scroll = getScroll(parent);
+
+  const height: number = isWindow(parent)
+    ? window.innerHeight
+    : parent.getBoundingClientRect().height;
+
+  const parentTop = isWindow(parent) ? 0 : parent.getBoundingClientRect().top;
+
+  if (top - parentTop < opts.topOffset) {
+    const goal = scroll.y - opts.topOffset + top - parentTop;
+    if (opts.smooth) {
+      addToScrollQueue(smoothScrollTop, parent, goal);
+    } else {
+      addToScrollQueue(scrollElement, parent, scroll.x, goal);
+    }
+  }
+
+  if (bottom - parentTop > height - opts.bottomOffset) {
+    const goal = scroll.y + bottom - parentTop - height + opts.bottomOffset;
+    if (opts.smooth) {
+      addToScrollQueue(smoothScrollTop, parent, goal);
+    } else {
+      addToScrollQueue(scrollElement, parent, scroll.x, goal);
+    }
+  }
+}
+
+export function scrollHorizontally(
+  element: Element,
+  options: {
+    leftOffset?: number;
+    rightOffset?: number;
+    parent?: Element;
+    smooth?: boolean;
+  }
+): void {
+  const opts = { leftOffset: 0, rightOffset: 0, parent: window, smooth: true, ...options };
+  const { parent } = opts;
+
+  const { left, right } = element.getBoundingClientRect();
+
+  const scroll = getScroll(parent);
+
+  const { left: parentLeft, width } = isWindow(parent)
+    ? { left: 0, width: window.innerWidth }
+    : parent.getBoundingClientRect();
+
+  if (left - parentLeft < opts.leftOffset) {
+    const goal = scroll.x - opts.leftOffset + left - parentLeft;
+    if (opts.smooth) {
+      addToScrollQueue(smoothScrollLeft, parent, goal);
+    } else {
+      addToScrollQueue(scrollElement, parent, goal, scroll.y);
+    }
+  }
+
+  if (right - parentLeft > width - opts.rightOffset) {
+    const goal = scroll.x + right - parentLeft - width + opts.rightOffset;
+    if (opts.smooth) {
+      addToScrollQueue(smoothScrollLeft, parent, goal);
+    } else {
+      addToScrollQueue(scrollElement, parent, goal, scroll.y);
+    }
+  }
+}
+
+type ScrollFunction = (element: Element | Window, x: number, y?: number) => Promise<void>;
+
+interface ScrollQueueItem {
+  element: Element | Window;
+  fn: ScrollFunction;
+  x: number;
+  y?: number;
+}
+
+const queue: ScrollQueueItem[] = [];
+let queueRunning: boolean;
+
+function addToScrollQueue(
+  fn: ScrollFunction,
+  element: Element | Window,
+  x: number,
+  y?: number
+): void {
+  queue.push({ fn, element, x, y });
+  if (!queueRunning) {
+    processQueue();
+  }
+}
+
+function processQueue() {
+  if (queue.length > 0) {
+    queueRunning = true;
+    const { fn, element, x, y } = queue.shift()!;
+    fn(element, x, y).then(processQueue).catch(processQueue);
+  } else {
+    queueRunning = false;
+  }
+}
diff --git a/server/sonar-ui-common/helpers/search.tsx b/server/sonar-ui-common/helpers/search.tsx
new file mode 100644 (file)
index 0000000..dd4382c
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+
+export function highlightTerm(str: string, term: string) {
+  const pos = str.toLowerCase().indexOf(term.toLowerCase());
+  return pos !== -1 ? (
+    <>
+      {pos > 0 && str.substring(0, pos)}
+      <mark>{str.substr(pos, term.length)}</mark>
+      {pos + term.length < str.length && str.substring(pos + term.length)}
+    </>
+  ) : (
+    str
+  );
+}
diff --git a/server/sonar-ui-common/helpers/storage.ts b/server/sonar-ui-common/helpers/storage.ts
new file mode 100644 (file)
index 0000000..5a50eb4
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+export function save(key: string, value?: string, suffix?: string): void {
+  try {
+    const finalKey = suffix ? `${key}.${suffix}` : key;
+    if (value) {
+      window.localStorage.setItem(finalKey, value);
+    } else {
+      window.localStorage.removeItem(finalKey);
+    }
+  } catch (e) {
+    // usually that means the storage is full
+    // just do nothing in this case
+  }
+}
+
+export function remove(key: string, suffix?: string): void {
+  try {
+    window.localStorage.removeItem(suffix ? `${key}.${suffix}` : key);
+  } catch {
+    // Fail silently
+  }
+}
+
+export function get(key: string, suffix?: string): string | null {
+  try {
+    return window.localStorage.getItem(suffix ? `${key}.${suffix}` : key);
+  } catch {
+    return null;
+  }
+}
diff --git a/server/sonar-ui-common/helpers/strings.ts b/server/sonar-ui-common/helpers/strings.ts
new file mode 100644 (file)
index 0000000..4109a7f
--- /dev/null
@@ -0,0 +1,426 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+/*
+ * Latinize string by removing all diacritics
+ * From http://stackoverflow.com/questions/990904/javascript-remove-accents-in-strings
+ */
+const defaultDiacriticsRemovalap = [
+  {
+    base: 'A',
+    letters:
+      '\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F',
+  },
+  {
+    base: 'AA',
+    letters: '\uA732',
+  },
+  {
+    base: 'AE',
+    letters: '\u00C6\u01FC\u01E2',
+  },
+  {
+    base: 'AO',
+    letters: '\uA734',
+  },
+  {
+    base: 'AU',
+    letters: '\uA736',
+  },
+  {
+    base: 'AV',
+    letters: '\uA738\uA73A',
+  },
+  {
+    base: 'AY',
+    letters: '\uA73C',
+  },
+  {
+    base: 'B',
+    letters: '\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181',
+  },
+  {
+    base: 'C',
+    letters: '\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E',
+  },
+  {
+    base: 'D',
+    letters: '\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779',
+  },
+  {
+    base: 'DZ',
+    letters: '\u01F1\u01C4',
+  },
+  {
+    base: 'Dz',
+    letters: '\u01F2\u01C5',
+  },
+  {
+    base: 'E',
+    letters:
+      '\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E',
+  },
+  {
+    base: 'F',
+    letters: '\u0046\u24BB\uFF26\u1E1E\u0191\uA77B',
+  },
+  {
+    base: 'G',
+    letters:
+      '\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E',
+  },
+  {
+    base: 'H',
+    letters: '\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D',
+  },
+  {
+    base: 'I',
+    letters:
+      '\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197',
+  },
+  {
+    base: 'J',
+    letters: '\u004A\u24BF\uFF2A\u0134\u0248',
+  },
+  {
+    base: 'K',
+    letters: '\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2',
+  },
+  {
+    base: 'L',
+    letters:
+      '\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780',
+  },
+  {
+    base: 'LJ',
+    letters: '\u01C7',
+  },
+  {
+    base: 'Lj',
+    letters: '\u01C8',
+  },
+  {
+    base: 'M',
+    letters: '\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C',
+  },
+  {
+    base: 'N',
+    letters:
+      '\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4',
+  },
+  {
+    base: 'NJ',
+    letters: '\u01CA',
+  },
+  {
+    base: 'Nj',
+    letters: '\u01CB',
+  },
+  {
+    base: 'O',
+    letters:
+      '\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C',
+  },
+  {
+    base: 'OI',
+    letters: '\u01A2',
+  },
+  {
+    base: 'OO',
+    letters: '\uA74E',
+  },
+  {
+    base: 'OU',
+    letters: '\u0222',
+  },
+  {
+    base: 'OE',
+    letters: '\u008C\u0152',
+  },
+  {
+    base: 'oe',
+    letters: '\u009C\u0153',
+  },
+  {
+    base: 'P',
+    letters: '\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754',
+  },
+  {
+    base: 'Q',
+    letters: '\u0051\u24C6\uFF31\uA756\uA758\u024A',
+  },
+  {
+    base: 'R',
+    letters:
+      '\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782',
+  },
+  {
+    base: 'S',
+    letters:
+      '\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784',
+  },
+  {
+    base: 'T',
+    letters:
+      '\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786',
+  },
+  {
+    base: 'TZ',
+    letters: '\uA728',
+  },
+  {
+    base: 'U',
+    letters:
+      '\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244',
+  },
+  {
+    base: 'V',
+    letters: '\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245',
+  },
+  {
+    base: 'VY',
+    letters: '\uA760',
+  },
+  {
+    base: 'W',
+    letters: '\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72',
+  },
+  {
+    base: 'X',
+    letters: '\u0058\u24CD\uFF38\u1E8A\u1E8C',
+  },
+  {
+    base: 'Y',
+    letters:
+      '\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE',
+  },
+  {
+    base: 'Z',
+    letters: '\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762',
+  },
+  {
+    base: 'a',
+    letters:
+      '\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250',
+  },
+  {
+    base: 'aa',
+    letters: '\uA733',
+  },
+  {
+    base: 'ae',
+    letters: '\u00E6\u01FD\u01E3',
+  },
+  {
+    base: 'ao',
+    letters: '\uA735',
+  },
+  {
+    base: 'au',
+    letters: '\uA737',
+  },
+  {
+    base: 'av',
+    letters: '\uA739\uA73B',
+  },
+  {
+    base: 'ay',
+    letters: '\uA73D',
+  },
+  {
+    base: 'b',
+    letters: '\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253',
+  },
+  {
+    base: 'c',
+    letters: '\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184',
+  },
+  {
+    base: 'd',
+    letters: '\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A',
+  },
+  {
+    base: 'dz',
+    letters: '\u01F3\u01C6',
+  },
+  {
+    base: 'e',
+    letters:
+      '\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD',
+  },
+  {
+    base: 'f',
+    letters: '\u0066\u24D5\uFF46\u1E1F\u0192\uA77C',
+  },
+  {
+    base: 'g',
+    letters:
+      '\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F',
+  },
+  {
+    base: 'h',
+    letters:
+      '\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265',
+  },
+  {
+    base: 'hv',
+    letters: '\u0195',
+  },
+  {
+    base: 'i',
+    letters:
+      '\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131',
+  },
+  {
+    base: 'j',
+    letters: '\u006A\u24D9\uFF4A\u0135\u01F0\u0249',
+  },
+  {
+    base: 'k',
+    letters: '\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3',
+  },
+  {
+    base: 'l',
+    letters:
+      '\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747',
+  },
+  {
+    base: 'lj',
+    letters: '\u01C9',
+  },
+  {
+    base: 'm',
+    letters: '\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F',
+  },
+  {
+    base: 'n',
+    letters:
+      '\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5',
+  },
+  {
+    base: 'nj',
+    letters: '\u01CC',
+  },
+  {
+    base: 'o',
+    letters:
+      '\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275',
+  },
+  {
+    base: 'oi',
+    letters: '\u01A3',
+  },
+  {
+    base: 'ou',
+    letters: '\u0223',
+  },
+  {
+    base: 'oo',
+    letters: '\uA74F',
+  },
+  {
+    base: 'p',
+    letters: '\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755',
+  },
+  {
+    base: 'q',
+    letters: '\u0071\u24E0\uFF51\u024B\uA757\uA759',
+  },
+  {
+    base: 'r',
+    letters:
+      '\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783',
+  },
+  {
+    base: 's',
+    letters:
+      '\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B',
+  },
+  {
+    base: 't',
+    letters:
+      '\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787',
+  },
+  {
+    base: 'tz',
+    letters: '\uA729',
+  },
+  {
+    base: 'u',
+    letters:
+      '\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289',
+  },
+  {
+    base: 'v',
+    letters: '\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C',
+  },
+  {
+    base: 'vy',
+    letters: '\uA761',
+  },
+  {
+    base: 'w',
+    letters: '\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73',
+  },
+  {
+    base: 'x',
+    letters: '\u0078\u24E7\uFF58\u1E8B\u1E8D',
+  },
+  {
+    base: 'y',
+    letters:
+      '\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF',
+  },
+  {
+    base: 'z',
+    letters: '\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763',
+  },
+];
+
+const diacriticsMap: T.Dict<string> = {};
+defaultDiacriticsRemovalap.forEach((defaultDiacritic) =>
+  defaultDiacritic.letters.split('').forEach((letter) => {
+    diacriticsMap[letter] = defaultDiacritic.base;
+  })
+);
+
+// "what?" version ... http://jsperf.com/diacritics/12
+export function latinize(str: string): string {
+  // eslint-disable-next-line no-control-regex
+  return str.replace(/[^\u0000-\u007E]/g, (a) => diacriticsMap[a] || a);
+}
+
+// Inspired from https://github.com/SonarSource/sonar-enterprise/blob/master/sonar-core/src/main/java/org/sonar/core/util/Slug.java
+export function slugify(text: string) {
+  return latinize(text.trim().toLowerCase())
+    .replace(/&/g, '-and-') // Replace & with 'and'
+    .replace(/[^\w-]+/g, '-') // Replace all non-word chars with dash
+    .replace(/\s+/g, '-') // Replace whitespaces with dash
+    .replace(/[·/_,:;]/g, '-') // Replace special chars with dash
+    .replace(/--+/g, '-') // Replace multiple dash with single dash
+    .replace(/^-+/, '') // Remove heading dash
+    .replace(/-+$/, ''); // Remove trailing dash
+}
+
+export function decodeJwt(token: string) {
+  const segments = token.split('.');
+  const base64Url = segments.length > 1 ? segments[1] : segments[0];
+  const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
+  return JSON.parse(window.atob(base64));
+}
diff --git a/server/sonar-ui-common/helpers/testUtils.ts b/server/sonar-ui-common/helpers/testUtils.ts
new file mode 100644 (file)
index 0000000..cf7767e
--- /dev/null
@@ -0,0 +1,164 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { ReactWrapper, ShallowWrapper } from 'enzyme';
+
+export function mockEvent(overrides = {}) {
+  return {
+    target: { blur() {} },
+    currentTarget: { blur() {} },
+    preventDefault() {},
+    stopPropagation() {},
+    ...overrides,
+  } as any;
+}
+
+export function click(element: ShallowWrapper | ReactWrapper, event = {}): void {
+  // `type()` returns a component constructor for a composite element and string for DOM nodes
+  if (typeof element.type() === 'function') {
+    element.prop<Function>('onClick')();
+    // TODO find out if `root` is a public api
+    // https://github.com/airbnb/enzyme/blob/master/packages/enzyme/src/ReactWrapper.js#L109
+    (element as any).root().update();
+  } else {
+    element.simulate('click', mockEvent(event));
+  }
+}
+
+export function clickOutside(event = {}): void {
+  const dispatchedEvent = new MouseEvent('click', event);
+  window.dispatchEvent(dispatchedEvent);
+}
+
+export function submit(element: ShallowWrapper | ReactWrapper): void {
+  element.simulate('submit', {
+    preventDefault() {},
+  });
+}
+
+export function change(element: ShallowWrapper | ReactWrapper, value: string, event = {}): void {
+  // `type()` returns a component constructor for a composite element and string for DOM nodes
+  if (typeof element.type() === 'function') {
+    element.prop<Function>('onChange')(value);
+    // TODO find out if `root` is a public api
+    // https://github.com/airbnb/enzyme/blob/master/packages/enzyme/src/ReactWrapper.js#L109
+    (element as any).root().update();
+  } else {
+    element.simulate('change', {
+      target: { value },
+      currentTarget: { value },
+      ...event,
+    });
+  }
+}
+
+export const KEYCODE_MAP: { [keycode: number]: string } = {
+  13: 'enter',
+  37: 'left',
+  38: 'up',
+  39: 'right',
+  40: 'down',
+};
+
+export function keydown(key: number | string): void {
+  let keyCode;
+  if (typeof key === 'number') {
+    keyCode = key;
+  } else {
+    // eslint-disable-next-line no-console
+    console.warn('Using strings in keydown() is deprecated. Consider using the KeyCodes enum.');
+    const mapped = Object.entries(KEYCODE_MAP).find(([_, value]) => value === key);
+    if (!mapped) {
+      throw new Error(`Cannot map key "${key}" to a keyCode!`);
+    }
+    keyCode = mapped[0];
+  }
+
+  const event = new KeyboardEvent('keydown', { keyCode, which: keyCode } as KeyboardEventInit);
+  document.dispatchEvent(event);
+}
+
+export function elementKeydown(element: ShallowWrapper, keyCode: number): void {
+  const event = {
+    currentTarget: { element },
+    keyCode,
+    preventDefault() {},
+  };
+
+  if (typeof element.type() === 'string') {
+    // `type()` is string for native dom elements
+    element.simulate('keydown', event);
+  } else {
+    element.prop<Function>('onKeyDown')(event);
+  }
+}
+
+export function resizeWindowTo(width?: number, height?: number) {
+  // `document.documentElement.clientHeight/clientWidth` are getters by default,
+  // so we need to redefine them. Pass `configurable: true` to allow to redefine
+  // the properties multiple times.
+  if (width) {
+    Object.defineProperty(document.documentElement, 'clientWidth', {
+      configurable: true,
+      value: width,
+    });
+  }
+  if (height) {
+    Object.defineProperty(document.documentElement, 'clientHeight', {
+      configurable: true,
+      value: height,
+    });
+  }
+
+  const resizeEvent = new Event('resize');
+  window.dispatchEvent(resizeEvent);
+}
+
+export function scrollTo({ left = 0, top = 0 }) {
+  Object.defineProperty(window, 'pageYOffset', { value: top });
+  Object.defineProperty(window, 'pageXOffset', { value: left });
+  const resizeEvent = new Event('scroll');
+  window.dispatchEvent(resizeEvent);
+}
+
+export function setNodeRect({ width = 50, height = 50, left = 0, top = 0 }) {
+  const { findDOMNode } = require('react-dom');
+  const element = document.createElement('div');
+  Object.defineProperty(element, 'getBoundingClientRect', {
+    value: () => ({ width, height, left, top }),
+  });
+  findDOMNode.mockReturnValue(element);
+}
+
+export function doAsync(fn?: Function): Promise<void> {
+  return new Promise((resolve) => {
+    setImmediate(() => {
+      if (fn) {
+        fn();
+      }
+      resolve();
+    });
+  });
+}
+
+export async function waitAndUpdate(wrapper: ShallowWrapper<any, any> | ReactWrapper<any, any>) {
+  await new Promise(setImmediate);
+  wrapper.update();
+}
diff --git a/server/sonar-ui-common/helpers/types.ts b/server/sonar-ui-common/helpers/types.ts
new file mode 100644 (file)
index 0000000..09b786e
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+export function isDefined<T>(x: T | undefined | null): x is T {
+  return x !== undefined && x !== null;
+}
diff --git a/server/sonar-ui-common/helpers/urls.ts b/server/sonar-ui-common/helpers/urls.ts
new file mode 100644 (file)
index 0000000..599d8fe
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { isNil, omitBy } from 'lodash';
+import { stringify } from 'querystring';
+import { getUrlContext, IS_SSR } from './init';
+
+interface Query {
+  [x: string]: string | undefined;
+}
+
+export interface Location {
+  pathname: string;
+  query?: Query;
+}
+
+export function getBaseUrl(): string {
+  return getUrlContext();
+}
+
+export function getHostUrl(): string {
+  if (IS_SSR) {
+    throw new Error('No host url available on server side.');
+  }
+  return window.location.origin + getBaseUrl();
+}
+
+export function getPathUrlAsString(path: Location, internal = true): string {
+  return `${internal ? getBaseUrl() : getHostUrl()}${path.pathname}?${stringify(
+    omitBy(path.query, isNil)
+  )}`;
+}
+
+export function getReturnUrl(location: { hash?: string; query?: { return_to?: string } }) {
+  const returnTo = location.query && location.query['return_to'];
+  if (isRelativeUrl(returnTo)) {
+    return returnTo + (location.hash ? location.hash : '');
+  }
+  return getBaseUrl() + '/';
+}
+
+export function isRelativeUrl(url?: string): boolean {
+  const regex = new RegExp(/^\/[^/\\]/);
+  return Boolean(url && regex.test(url));
+}
diff --git a/server/sonar-ui-common/package.json b/server/sonar-ui-common/package.json
new file mode 100644 (file)
index 0000000..d2d37c6
--- /dev/null
@@ -0,0 +1,155 @@
+{
+  "name": "sonar-ui-common",
+  "version": "1.0.33",
+  "description": "Common UI lib for SonarQube and SonarCloud",
+  "repository": "SonarSource/sonar-ui-common",
+  "license": "LGPL-3.0",
+  "types": "types.d.ts",
+  "dependencies": {
+    "@types/react-select": "1.2.6",
+    "classnames": "2.2.6",
+    "clipboard": "2.0.6",
+    "d3-array": "2.4.0",
+    "d3-hierarchy": "1.1.9",
+    "d3-scale": "3.2.1",
+    "d3-selection": "1.4.1",
+    "d3-shape": "1.3.7",
+    "d3-zoom": "1.8.3",
+    "date-fns": "1.30.1",
+    "formik": "1.2.0",
+    "history": "3.3.0",
+    "prop-types": "15.7.2",
+    "react-draggable": "4.2.0",
+    "react-intl": "2.8.0",
+    "react-modal": "3.8.2",
+    "react-router": "3.2.1",
+    "react-select": "1.2.1",
+    "react-virtualized": "9.21.0"
+  },
+  "peerDependencies": {
+    "@emotion/core": "^10.0.17",
+    "@emotion/styled": "^10.0.17",
+    "emotion-theming": "^10.0.19",
+    "lodash": "^4.17.21",
+    "react": "^16.8.6",
+    "react-dom": "^16.8.6"
+  },
+  "devDependencies": {
+    "@emotion/core": "10.0.17",
+    "@emotion/styled": "10.0.17",
+    "@types/classnames": "2.2.8",
+    "@types/clipboard": "2.0.1",
+    "@types/d3-array": "1.2.4",
+    "@types/d3-hierarchy": "1.1.4",
+    "@types/d3-scale": "2.0.2",
+    "@types/d3-selection": "1.3.2",
+    "@types/d3-shape": "1.2.4",
+    "@types/d3-zoom": "1.7.3",
+    "@types/enzyme": "3.10.5",
+    "@types/jest": "25.2.1",
+    "@types/lodash": "4.14.159",
+    "@types/react": "16.8.23",
+    "@types/react-dom": "16.8.4",
+    "@types/react-intl": "2.3.17",
+    "@types/react-modal": "3.8.2",
+    "@types/react-router": "3.0.20",
+    "@types/react-virtualized": "9.21.0",
+    "@typescript-eslint/parser": "2.29.0",
+    "cpy-cli": "2.0.0",
+    "date-fns": "1.30.1",
+    "diff": "4.0.1",
+    "emotion-theming": "10.0.19",
+    "enzyme": "3.11.0",
+    "enzyme-adapter-react-16": "1.15.2",
+    "enzyme-to-json": "3.4.4",
+    "eslint": "6.8.0",
+    "eslint-config-sonarqube": "0.6.1",
+    "eslint-plugin-import": "2.20.1",
+    "eslint-plugin-jest": "23.8.2",
+    "eslint-plugin-jsx-a11y": "6.2.3",
+    "eslint-plugin-promise": "4.2.1",
+    "eslint-plugin-react": "7.19.0",
+    "eslint-plugin-react-hooks": "2.5.1",
+    "eslint-plugin-sonarjs": "0.5.0",
+    "globby": "10.0.1",
+    "jest": "25.4.0",
+    "jest-emotion": "10.0.32",
+    "lodash": "4.17.21",
+    "prettier": "2.0.4",
+    "react": "16.8.6",
+    "react-dom": "16.8.6",
+    "react-router": "3.2.1",
+    "react-test-renderer": "16.8.6",
+    "ts-jest": "25.4.0",
+    "typescript": "3.8.3",
+    "whatwg-fetch": "3.0.0"
+  },
+  "scripts": {
+    "clean": "rm -rf build/dist",
+    "build": "sh scripts/build.sh",
+    "package": "yarn build && cd build/dist && yarn pack",
+    "release": "sh scripts/release.sh",
+    "test": "jest",
+    "format": "prettier --write --list-different \"{,!(build|node_modules)/**/}*.{ts,tsx,css}\"",
+    "format-check": "prettier --list-different \"{,!(build|node_modules)/**/}*.{ts,tsx,css}\"",
+    "license-check": "node scripts/license-check",
+    "lint": "eslint --ext ts,tsx --quiet \"{,!(build|node_modules)/**/}*.{ts,tsx}\"",
+    "lint-report": "eslint --ext ts,tsx -f json -o build/eslint-report.json \"{,!(build|node_modules)/**/}*.{ts,tsx}\"",
+    "ts-check": "tsc --noEmit",
+    "validate": "yarn ts-check && yarn license-check && yarn lint && yarn format-check && yarn test",
+    "validate-ci": "yarn lint-report && yarn license-check && yarn format-check && yarn test --coverage"
+  },
+  "engines": {
+    "node": ">=10.15.3",
+    "yarn": ">=1.15.2"
+  },
+  "jest": {
+    "coverageDirectory": "<rootDir>/build/coverage",
+    "collectCoverageFrom": [
+      "{components,helpers}/**/*.{ts,tsx,js}",
+      "!helpers/{keycodes,testUtils}.{ts,tsx}"
+    ],
+    "coverageReporters": [
+      "lcovonly",
+      "text"
+    ],
+    "globals": {
+      "ts-jest": {
+        "diagnostics": false
+      }
+    },
+    "moduleFileExtensions": [
+      "ts",
+      "tsx",
+      "js"
+    ],
+    "moduleNameMapper": {
+      "^.+\\.(md|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/config/jest/FileStub.js",
+      "^.+\\.css$": "<rootDir>/config/jest/CSSStub.js"
+    },
+    "setupFiles": [
+      "<rootDir>/config/jest/SetupTestEnvironment.js",
+      "<rootDir>/config/jest/SetupEnzyme.js",
+      "<rootDir>/config/jest/SetupSUC.ts"
+    ],
+    "snapshotSerializers": [
+      "enzyme-to-json/serializer",
+      "jest-emotion"
+    ],
+    "testPathIgnorePatterns": [
+      "<rootDir>/build",
+      "<rootDir>/config",
+      "<rootDir>/node_modules",
+      "<rootDir>/scripts"
+    ],
+    "testRegex": "(/__tests__/.*|\\-test)\\.(ts|tsx)$",
+    "transform": {
+      "\\.(ts|tsx)$": "ts-jest"
+    }
+  },
+  "prettier": {
+    "jsxBracketSameLine": true,
+    "printWidth": 100,
+    "singleQuote": true
+  }
+}
diff --git a/server/sonar-ui-common/scripts/build.sh b/server/sonar-ui-common/scripts/build.sh
new file mode 100755 (executable)
index 0000000..acc1c39
--- /dev/null
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+yarn clean
+yarn tsc
+cp package.json README.md types.d.ts build/dist
+yarn cpy 'components/**/*.css' 'build/dist' --parents
diff --git a/server/sonar-ui-common/scripts/license-check.js b/server/sonar-ui-common/scripts/license-check.js
new file mode 100644 (file)
index 0000000..cd4b022
--- /dev/null
@@ -0,0 +1,56 @@
+const fs = require('fs');
+const diff = require('diff');
+const globby = require('globby');
+
+const GLOBS = ['**/*.ts', '**/*.tsx', '!node_modules/**/*', '!build/**/*'];
+
+let header;
+
+return readFile('./HEADER')
+  .then(h => {
+    header = h;
+  })
+  .then(() => globby(GLOBS))
+  .then(readFilesFromPaths)
+  .then(checkFiles)
+  .then(errors => {
+    if (errors) {
+      console.error(errors, 'files have an invalid license header');
+      process.exit(1);
+    } else {
+      console.log('✓ All files have valid license headers');
+    }
+  })
+  .catch(e => {
+    console.error(e);
+    process.exit(1);
+  });
+
+function checkFiles(files) {
+  return files.reduce((errors, { path, text }) => {
+    if (text.slice(0, header.length) !== header) {
+      console.error('❌ ', path);
+      console.error(
+        diff.createPatch(path, header + '\n', text.slice(0, header.length) + '\n', '', '')
+      );
+      return errors + 1;
+    }
+    return errors;
+  }, 0);
+}
+
+function readFilesFromPaths(paths) {
+  return Promise.all(paths.map(path => readFile(path).then(text => ({ path, text }))));
+}
+
+function readFile(path) {
+  return new Promise((resolve, reject) => {
+    fs.readFile(path, { encoding: 'utf-8' }, (err, text) => {
+      if (err) {
+        reject(err);
+      } else {
+        resolve(text);
+      }
+    });
+  });
+}
diff --git a/server/sonar-ui-common/scripts/release.sh b/server/sonar-ui-common/scripts/release.sh
new file mode 100755 (executable)
index 0000000..a34ed5b
--- /dev/null
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+
+if [ $# -eq 0 ]
+  then
+    echo "Missing argument for the new version to release"
+    exit 1
+fi
+
+if [ ! $(which npmrc) ]
+  then
+    echo "You need to install npmrc and configure it correctly, check the readme. Call:"
+    echo "  npm install -g npmrc"
+    exit 1
+fi
+
+git stash
+sed "s/## Unreleased/## Unreleased__NEW_LINE__## $1/g" CHANGELOG.md | awk '{ sub(/__NEW_LINE__/,"\n\n"); print }' > CHANGELOG.md.back
+mv CHANGELOG.md.back CHANGELOG.md
+sed "s/\"version\": \".*\",/\"version\": \"$1\",/g" package.json > package.json.back
+mv package.json.back package.json
+yarn
+yarn package
+npmrc npm
+git add CHANGELOG.md package.json
+git commit -m "Prepare version $1"
+git tag $1
+yarn publish ./build/dist/sonar-ui-common-v$1.tgz
+
+if [ $? -gt 0 ]
+  then
+    echo "Publish failed, aborting"
+    git tag -d $1
+    git reset HEAD~
+    git checkout .
+    npmrc default
+    git stash pop
+    exit 1
+fi
+
+git push
+git push origin $1
+npmrc default
+git stash pop
+
diff --git a/server/sonar-ui-common/tsconfig.json b/server/sonar-ui-common/tsconfig.json
new file mode 100644 (file)
index 0000000..9183405
--- /dev/null
@@ -0,0 +1,23 @@
+{
+  "compilerOptions": {
+    "allowJs": false,
+    "checkJs": false,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "strict": true,
+    "strictFunctionTypes": false,
+    "module": "commonjs",
+    "target": "es5",
+    "lib": ["es2018", "dom"],
+    "outDir": "build/dist",
+    "jsx": "react",
+    "moduleResolution": "node",
+    "removeComments": true,
+    "sourceMap": true,
+    "declaration": true,
+    "rootDir": "./",
+    "baseUrl": "."
+  },
+  "include": ["components", "helpers", "types.d.ts"],
+  "exclude": ["**/*-test.*"]
+}
diff --git a/server/sonar-ui-common/types.d.ts b/server/sonar-ui-common/types.d.ts
new file mode 100644 (file)
index 0000000..8ed1448
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * Sonar UI Common
+ * Copyright (C) 2019-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+declare namespace T {
+  export type Dict<T> = { [key: string]: T };
+  export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
+
+  // Type ordered alphabetically to prevent merge conflicts
+
+  export type IssueType = 'BUG' | 'VULNERABILITY' | 'CODE_SMELL' | 'SECURITY_HOTSPOT';
+
+  export type RawQuery = T.Dict<any>;
+
+  export namespace Chart {
+    export interface Point {
+      x: Date;
+      y: number | string | undefined;
+    }
+
+    export interface Serie {
+      data: Point[];
+      name: string;
+      translatedName: string;
+      type: string;
+    }
+  }
+}
diff --git a/server/sonar-ui-common/yarn.lock b/server/sonar-ui-common/yarn.lock
new file mode 100644 (file)
index 0000000..f209156
--- /dev/null
@@ -0,0 +1,6545 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@^7.0.0":
+  version "7.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
+  integrity sha1-BuKrGb21NThVWaq7W6WXKUgoAPg=
+  dependencies:
+    "@babel/highlight" "^7.0.0"
+
+"@babel/code-frame@^7.8.3":
+  version "7.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e"
+  integrity sha1-M+JZA9dIEYFTThLsCiXxa2/PQZ4=
+  dependencies:
+    "@babel/highlight" "^7.8.3"
+
+"@babel/core@^7.1.0":
+  version "7.5.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/core/-/core-7.5.4.tgz#4c32df7ad5a58e9ea27ad025c11276324e0b4ddd"
+  integrity sha1-TDLfetWljp6ietAlwRJ2Mk4LTd0=
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    "@babel/generator" "^7.5.0"
+    "@babel/helpers" "^7.5.4"
+    "@babel/parser" "^7.5.0"
+    "@babel/template" "^7.4.4"
+    "@babel/traverse" "^7.5.0"
+    "@babel/types" "^7.5.0"
+    convert-source-map "^1.1.0"
+    debug "^4.1.0"
+    json5 "^2.1.0"
+    lodash "^4.17.11"
+    resolve "^1.3.2"
+    semver "^5.4.1"
+    source-map "^0.5.0"
+
+"@babel/core@^7.7.5":
+  version "7.9.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e"
+  integrity sha1-rJd7U4t34TL/cG87ik260JwDxW4=
+  dependencies:
+    "@babel/code-frame" "^7.8.3"
+    "@babel/generator" "^7.9.0"
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helpers" "^7.9.0"
+    "@babel/parser" "^7.9.0"
+    "@babel/template" "^7.8.6"
+    "@babel/traverse" "^7.9.0"
+    "@babel/types" "^7.9.0"
+    convert-source-map "^1.7.0"
+    debug "^4.1.0"
+    gensync "^1.0.0-beta.1"
+    json5 "^2.1.2"
+    lodash "^4.17.13"
+    resolve "^1.3.2"
+    semver "^5.4.1"
+    source-map "^0.5.0"
+
+"@babel/generator@^7.5.0":
+  version "7.5.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/generator/-/generator-7.5.0.tgz#f20e4b7a91750ee8b63656073d843d2a736dca4a"
+  integrity sha1-8g5LepF1Dui2NlYHPYQ9KnNtyko=
+  dependencies:
+    "@babel/types" "^7.5.0"
+    jsesc "^2.5.1"
+    lodash "^4.17.11"
+    source-map "^0.5.0"
+    trim-right "^1.0.1"
+
+"@babel/generator@^7.9.0", "@babel/generator@^7.9.5":
+  version "7.9.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/generator/-/generator-7.9.5.tgz#27f0917741acc41e6eaaced6d68f96c3fa9afaf9"
+  integrity sha1-J/CRd0GsxB5uqs7W1o+Ww/qa+vk=
+  dependencies:
+    "@babel/types" "^7.9.5"
+    jsesc "^2.5.1"
+    lodash "^4.17.13"
+    source-map "^0.5.0"
+
+"@babel/helper-function-name@^7.1.0":
+  version "7.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53"
+  integrity sha1-oM6wFoX3M1XUNgwSR/WCv6/I/1M=
+  dependencies:
+    "@babel/helper-get-function-arity" "^7.0.0"
+    "@babel/template" "^7.1.0"
+    "@babel/types" "^7.0.0"
+
+"@babel/helper-function-name@^7.9.5":
+  version "7.9.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz#2b53820d35275120e1874a82e5aabe1376920a5c"
+  integrity sha1-K1OCDTUnUSDhh0qC5aq+E3aSClw=
+  dependencies:
+    "@babel/helper-get-function-arity" "^7.8.3"
+    "@babel/template" "^7.8.3"
+    "@babel/types" "^7.9.5"
+
+"@babel/helper-get-function-arity@^7.0.0":
+  version "7.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3"
+  integrity sha1-g1ctQyDipGVyY3NBE8QoaLZOScM=
+  dependencies:
+    "@babel/types" "^7.0.0"
+
+"@babel/helper-get-function-arity@^7.8.3":
+  version "7.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5"
+  integrity sha1-uJS5R70AQ4HOY+odufCFR+kgq9U=
+  dependencies:
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-member-expression-to-functions@^7.8.3":
+  version "7.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz#659b710498ea6c1d9907e0c73f206eee7dadc24c"
+  integrity sha1-ZZtxBJjqbB2ZB+DHPyBu7n2twkw=
+  dependencies:
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-module-imports@^7.0.0":
+  version "7.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz#96081b7111e486da4d2cd971ad1a4fe216cc2e3d"
+  integrity sha1-lggbcRHkhtpNLNlxrRpP4hbMLj0=
+  dependencies:
+    "@babel/types" "^7.0.0"
+
+"@babel/helper-module-imports@^7.8.3":
+  version "7.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz#7fe39589b39c016331b6b8c3f441e8f0b1419498"
+  integrity sha1-f+OVibOcAWMxtrjD9EHo8LFBlJg=
+  dependencies:
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-module-transforms@^7.9.0":
+  version "7.9.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz#43b34dfe15961918707d247327431388e9fe96e5"
+  integrity sha1-Q7NN/hWWGRhwfSRzJ0MTiOn+luU=
+  dependencies:
+    "@babel/helper-module-imports" "^7.8.3"
+    "@babel/helper-replace-supers" "^7.8.6"
+    "@babel/helper-simple-access" "^7.8.3"
+    "@babel/helper-split-export-declaration" "^7.8.3"
+    "@babel/template" "^7.8.6"
+    "@babel/types" "^7.9.0"
+    lodash "^4.17.13"
+
+"@babel/helper-optimise-call-expression@^7.8.3":
+  version "7.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz#7ed071813d09c75298ef4f208956006b6111ecb9"
+  integrity sha1-ftBxgT0Jx1KY708giVYAa2ER7Lk=
+  dependencies:
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-plugin-utils@^7.0.0":
+  version "7.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250"
+  integrity sha1-u7P77phmHFaQNCN8wDlnupm08lA=
+
+"@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
+  version "7.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670"
+  integrity sha1-nqKTvhm6vA9S/4yoizTDYRsghnA=
+
+"@babel/helper-replace-supers@^7.8.6":
+  version "7.8.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8"
+  integrity sha1-Wtp0T9WtcyA78dZ0WaJ9y6Z+/8g=
+  dependencies:
+    "@babel/helper-member-expression-to-functions" "^7.8.3"
+    "@babel/helper-optimise-call-expression" "^7.8.3"
+    "@babel/traverse" "^7.8.6"
+    "@babel/types" "^7.8.6"
+
+"@babel/helper-simple-access@^7.8.3":
+  version "7.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz#7f8109928b4dab4654076986af575231deb639ae"
+  integrity sha1-f4EJkotNq0ZUB2mGr1dSMd62Oa4=
+  dependencies:
+    "@babel/template" "^7.8.3"
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-split-export-declaration@^7.4.4":
+  version "7.4.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677"
+  integrity sha1-/5SJSjQL549T8GrwOLIFxJ2ZNnc=
+  dependencies:
+    "@babel/types" "^7.4.4"
+
+"@babel/helper-split-export-declaration@^7.8.3":
+  version "7.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9"
+  integrity sha1-ManzAHD5E2inGCzwX4MXgQZfx6k=
+  dependencies:
+    "@babel/types" "^7.8.3"
+
+"@babel/helper-validator-identifier@^7.9.0", "@babel/helper-validator-identifier@^7.9.5":
+  version "7.9.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80"
+  integrity sha1-kJd6jm+/a0MafcMXUu7iM78FLYA=
+
+"@babel/helpers@^7.5.4":
+  version "7.5.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/helpers/-/helpers-7.5.4.tgz#2f00608aa10d460bde0ccf665d6dcf8477357cf0"
+  integrity sha1-LwBgiqENRgveDM9mXW3PhHc1fPA=
+  dependencies:
+    "@babel/template" "^7.4.4"
+    "@babel/traverse" "^7.5.0"
+    "@babel/types" "^7.5.0"
+
+"@babel/helpers@^7.9.0":
+  version "7.9.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/helpers/-/helpers-7.9.2.tgz#b42a81a811f1e7313b88cba8adc66b3d9ae6c09f"
+  integrity sha1-tCqBqBHx5zE7iMuorcZrPZrmwJ8=
+  dependencies:
+    "@babel/template" "^7.8.3"
+    "@babel/traverse" "^7.9.0"
+    "@babel/types" "^7.9.0"
+
+"@babel/highlight@^7.0.0":
+  version "7.5.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540"
+  integrity sha1-VtETEr2SSPphlZHQJHK+boyzJUA=
+  dependencies:
+    chalk "^2.0.0"
+    esutils "^2.0.2"
+    js-tokens "^4.0.0"
+
+"@babel/highlight@^7.8.3":
+  version "7.9.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079"
+  integrity sha1-TptFzLgreWBycbKXmtgse2gWMHk=
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.9.0"
+    chalk "^2.0.0"
+    js-tokens "^4.0.0"
+
+"@babel/parser@^7.1.0", "@babel/parser@^7.4.4", "@babel/parser@^7.5.0":
+  version "7.5.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/parser/-/parser-7.5.0.tgz#3e0713dff89ad6ae37faec3b29dcfc5c979770b7"
+  integrity sha1-PgcT3/ia1q43+uw7Kdz8XJeXcLc=
+
+"@babel/parser@^7.7.5", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0":
+  version "7.9.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8"
+  integrity sha1-aKNeawMZu8AURlvkOCgwARPy8ug=
+
+"@babel/plugin-syntax-async-generators@^7.8.4":
+  version "7.8.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
+  integrity sha1-qYP7Gusuw/btBCohD2QOkOeG/g0=
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-bigint@^7.8.3":
+  version "7.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea"
+  integrity sha1-TJpvZp9dDN8bkKFnHpoUa+UwDOo=
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-class-properties@^7.8.3":
+  version "7.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.8.3.tgz#6cb933a8872c8d359bfde69bbeaae5162fd1e8f7"
+  integrity sha1-bLkzqIcsjTWb/eabvqrlFi/R6Pc=
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-syntax-json-strings@^7.8.3":
+  version "7.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a"
+  integrity sha1-AcohtmjNghjJ5kDLbdiMVBKyyWo=
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-logical-assignment-operators@^7.8.3":
+  version "7.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.8.3.tgz#3995d7d7ffff432f6ddc742b47e730c054599897"
+  integrity sha1-OZXX1///Qy9t3HQrR+cwwFRZmJc=
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3":
+  version "7.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
+  integrity sha1-Fn7XA2iIYIH3S1w2xlqIwDtm0ak=
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-numeric-separator@^7.8.3":
+  version "7.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz#0e3fb63e09bea1b11e96467271c8308007e7c41f"
+  integrity sha1-Dj+2Pgm+obEelkZyccgwgAfnxB8=
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-syntax-object-rest-spread@^7.8.3":
+  version "7.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
+  integrity sha1-YOIl7cvZimQDMqLnLdPmbxr1WHE=
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-optional-catch-binding@^7.8.3":
+  version "7.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1"
+  integrity sha1-YRGiZbz7Ag6579D9/X0mQCue1sE=
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-optional-chaining@^7.8.3":
+  version "7.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
+  integrity sha1-T2nCq5UWfgGAzVM2YT+MV4j31Io=
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/runtime-corejs3@^7.8.3":
+  version "7.9.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/runtime-corejs3/-/runtime-corejs3-7.9.2.tgz#26fe4aa77e9f1ecef9b776559bbb8e84d34284b7"
+  integrity sha1-Jv5Kp36fHs75t3ZVm7uOhNNChLc=
+  dependencies:
+    core-js-pure "^3.0.0"
+    regenerator-runtime "^0.13.4"
+
+"@babel/runtime@^7.1.2":
+  version "7.5.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/runtime/-/runtime-7.5.4.tgz#cb7d1ad7c6d65676e66b47186577930465b5271b"
+  integrity sha1-y30a18bWVnbma0cYZXeTBGW1Jxs=
+  dependencies:
+    regenerator-runtime "^0.13.2"
+
+"@babel/runtime@^7.4.2", "@babel/runtime@^7.5.5":
+  version "7.6.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/runtime/-/runtime-7.6.0.tgz#4fc1d642a9fd0299754e8b5de62c631cf5568205"
+  integrity sha1-T8HWQqn9Apl1Totd5ixjHPVWggU=
+  dependencies:
+    regenerator-runtime "^0.13.2"
+
+"@babel/runtime@^7.4.5":
+  version "7.8.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308"
+  integrity sha1-159aIED3yqJNU+VjqtScvAVYEwg=
+  dependencies:
+    regenerator-runtime "^0.13.2"
+
+"@babel/template@^7.1.0", "@babel/template@^7.4.4":
+  version "7.4.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237"
+  integrity sha1-9LiNEiVomgj1vDoXSDVFvp5O0jc=
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    "@babel/parser" "^7.4.4"
+    "@babel/types" "^7.4.4"
+
+"@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
+  version "7.8.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
+  integrity sha1-hrIq8V+CjfsIZHT5ZNzD45xDzis=
+  dependencies:
+    "@babel/code-frame" "^7.8.3"
+    "@babel/parser" "^7.8.6"
+    "@babel/types" "^7.8.6"
+
+"@babel/traverse@^7.1.0", "@babel/traverse@^7.5.0":
+  version "7.5.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/traverse/-/traverse-7.5.0.tgz#4216d6586854ef5c3c4592dab56ec7eb78485485"
+  integrity sha1-QhbWWGhU71w8RZLatW7H63hIVIU=
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    "@babel/generator" "^7.5.0"
+    "@babel/helper-function-name" "^7.1.0"
+    "@babel/helper-split-export-declaration" "^7.4.4"
+    "@babel/parser" "^7.5.0"
+    "@babel/types" "^7.5.0"
+    debug "^4.1.0"
+    globals "^11.1.0"
+    lodash "^4.17.11"
+
+"@babel/traverse@^7.7.4", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0":
+  version "7.9.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/traverse/-/traverse-7.9.5.tgz#6e7c56b44e2ac7011a948c21e283ddd9d9db97a2"
+  integrity sha1-bnxWtE4qxwEalIwh4oPd2dnbl6I=
+  dependencies:
+    "@babel/code-frame" "^7.8.3"
+    "@babel/generator" "^7.9.5"
+    "@babel/helper-function-name" "^7.9.5"
+    "@babel/helper-split-export-declaration" "^7.8.3"
+    "@babel/parser" "^7.9.0"
+    "@babel/types" "^7.9.5"
+    debug "^4.1.0"
+    globals "^11.1.0"
+    lodash "^4.17.13"
+
+"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.4", "@babel/types@^7.5.0":
+  version "7.5.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/types/-/types-7.5.0.tgz#e47d43840c2e7f9105bc4d3a2c371b4d0c7832ab"
+  integrity sha1-5H1DhAwuf5EFvE06LDcbTQx4Mqs=
+  dependencies:
+    esutils "^2.0.2"
+    lodash "^4.17.11"
+    to-fast-properties "^2.0.0"
+
+"@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0", "@babel/types@^7.9.5":
+  version "7.9.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444"
+  integrity sha1-iSMfgpFailZqcDs7IBM/c9prlEQ=
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.9.5"
+    lodash "^4.17.13"
+    to-fast-properties "^2.0.0"
+
+"@bcoe/v8-coverage@^0.2.3":
+  version "0.2.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
+  integrity sha1-daLotRy3WKdVPWgEpZMteqznXDk=
+
+"@cnakazawa/watch@^1.0.3":
+  version "1.0.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef"
+  integrity sha1-CZE56ux+vweifBeGo/9k85Rk0u8=
+  dependencies:
+    exec-sh "^0.3.2"
+    minimist "^1.2.0"
+
+"@emotion/cache@^10.0.17":
+  version "10.0.17"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@emotion/cache/-/cache-10.0.17.tgz#3491a035f62f276620d586677bfc3d4fad0b8472"
+  integrity sha1-NJGgNfYvJ2Yg1YZne/w9T60LhHI=
+  dependencies:
+    "@emotion/sheet" "0.9.3"
+    "@emotion/stylis" "0.8.4"
+    "@emotion/utils" "0.11.2"
+    "@emotion/weak-memoize" "0.2.3"
+
+"@emotion/core@10.0.17":
+  version "10.0.17"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@emotion/core/-/core-10.0.17.tgz#3367376709721f4ee2068cff54ba581d362789d8"
+  integrity sha1-M2c3ZwlyH07iBoz/VLpYHTYnidg=
+  dependencies:
+    "@babel/runtime" "^7.5.5"
+    "@emotion/cache" "^10.0.17"
+    "@emotion/css" "^10.0.14"
+    "@emotion/serialize" "^0.11.10"
+    "@emotion/sheet" "0.9.3"
+    "@emotion/utils" "0.11.2"
+
+"@emotion/css@^10.0.14":
+  version "10.0.14"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@emotion/css/-/css-10.0.14.tgz#95dacabdd0e22845d1a1b0b5968d9afa34011139"
+  integrity sha1-ldrKvdDiKEXRobC1lo2a+jQBETk=
+  dependencies:
+    "@emotion/serialize" "^0.11.8"
+    "@emotion/utils" "0.11.2"
+    babel-plugin-emotion "^10.0.14"
+
+"@emotion/hash@0.7.2":
+  version "0.7.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@emotion/hash/-/hash-0.7.2.tgz#53211e564604beb9befa7a4400ebf8147473eeef"
+  integrity sha1-UyEeVkYEvrm++npEAOv4FHRz7u8=
+
+"@emotion/is-prop-valid@0.8.2":
+  version "0.8.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@emotion/is-prop-valid/-/is-prop-valid-0.8.2.tgz#b9692080da79041683021fcc32f96b40c54c59dc"
+  integrity sha1-uWkggNp5BBaDAh/MMvlrQMVMWdw=
+  dependencies:
+    "@emotion/memoize" "0.7.2"
+
+"@emotion/memoize@0.7.2":
+  version "0.7.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@emotion/memoize/-/memoize-0.7.2.tgz#7f4c71b7654068dfcccad29553520f984cc66b30"
+  integrity sha1-f0xxt2VAaN/MytKVU1IPmEzGazA=
+
+"@emotion/serialize@^0.11.10", "@emotion/serialize@^0.11.8":
+  version "0.11.10"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@emotion/serialize/-/serialize-0.11.10.tgz#53207dba7e28bd96928fc2a37e20b31b712bf9a2"
+  integrity sha1-UyB9un4ovZaSj8KjfiCzG3Er+aI=
+  dependencies:
+    "@emotion/hash" "0.7.2"
+    "@emotion/memoize" "0.7.2"
+    "@emotion/unitless" "0.7.4"
+    "@emotion/utils" "0.11.2"
+    csstype "^2.5.7"
+
+"@emotion/sheet@0.9.3":
+  version "0.9.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@emotion/sheet/-/sheet-0.9.3.tgz#689f135ecf87d3c650ed0c4f5ddcbe579883564a"
+  integrity sha1-aJ8TXs+H08ZQ7QxPXdy+V5iDVko=
+
+"@emotion/styled-base@^10.0.17":
+  version "10.0.17"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@emotion/styled-base/-/styled-base-10.0.17.tgz#701af0cd256be2977db8d67c33630f542e460b85"
+  integrity sha1-cBrwzSVr4pd9uNZ8M2MPVC5GC4U=
+  dependencies:
+    "@babel/runtime" "^7.5.5"
+    "@emotion/is-prop-valid" "0.8.2"
+    "@emotion/serialize" "^0.11.10"
+    "@emotion/utils" "0.11.2"
+
+"@emotion/styled@10.0.17":
+  version "10.0.17"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@emotion/styled/-/styled-10.0.17.tgz#0cd38b8b36259541f2c6717fc22607a120623654"
+  integrity sha1-DNOLizYllUHyxnF/wiYHoSBiNlQ=
+  dependencies:
+    "@emotion/styled-base" "^10.0.17"
+    babel-plugin-emotion "^10.0.17"
+
+"@emotion/stylis@0.8.4":
+  version "0.8.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@emotion/stylis/-/stylis-0.8.4.tgz#6c51afdf1dd0d73666ba09d2eb6c25c220d6fe4c"
+  integrity sha1-bFGv3x3Q1zZmugnS62wlwiDW/kw=
+
+"@emotion/unitless@0.7.4":
+  version "0.7.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@emotion/unitless/-/unitless-0.7.4.tgz#a87b4b04e5ae14a88d48ebef15015f6b7d1f5677"
+  integrity sha1-qHtLBOWuFKiNSOvvFQFfa30fVnc=
+
+"@emotion/utils@0.11.2":
+  version "0.11.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@emotion/utils/-/utils-0.11.2.tgz#713056bfdffb396b0a14f1c8f18e7b4d0d200183"
+  integrity sha1-cTBWv9/7OWsKFPHI8Y57TQ0gAYM=
+
+"@emotion/weak-memoize@0.2.3":
+  version "0.2.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@emotion/weak-memoize/-/weak-memoize-0.2.3.tgz#dfa0c92efe44a1d1a7974fb49ffeb40ef2da5a27"
+  integrity sha1-36DJLv5EodGnl0+0n/60DvLaWic=
+
+"@emotion/weak-memoize@0.2.4":
+  version "0.2.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@emotion/weak-memoize/-/weak-memoize-0.2.4.tgz#622a72bebd1e3f48d921563b4b60a762295a81fc"
+  integrity sha1-Yipyvr0eP0jZIVY7S2CnYilagfw=
+
+"@istanbuljs/load-nyc-config@^1.0.0":
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b"
+  integrity sha1-EGAt5VcLrqgvivv6JjCyTnqM/ls=
+  dependencies:
+    camelcase "^5.3.1"
+    find-up "^4.1.0"
+    js-yaml "^3.13.1"
+    resolve-from "^5.0.0"
+
+"@istanbuljs/schema@^0.1.2":
+  version "0.1.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd"
+  integrity sha1-JlIL8Jq+SlZEzVQU43ElqJVCQd0=
+
+"@jest/console@^25.4.0":
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@jest/console/-/console-25.4.0.tgz#e2760b532701137801ba824dcff6bc822c961bac"
+  integrity sha1-4nYLUycBE3gBuoJNz/a8giyWG6w=
+  dependencies:
+    "@jest/types" "^25.4.0"
+    chalk "^3.0.0"
+    jest-message-util "^25.4.0"
+    jest-util "^25.4.0"
+    slash "^3.0.0"
+
+"@jest/core@^25.4.0":
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@jest/core/-/core-25.4.0.tgz#cc1fe078df69b8f0fbb023bb0bcee23ef3b89411"
+  integrity sha1-zB/geN9puPD7sCO7C87iPvO4lBE=
+  dependencies:
+    "@jest/console" "^25.4.0"
+    "@jest/reporters" "^25.4.0"
+    "@jest/test-result" "^25.4.0"
+    "@jest/transform" "^25.4.0"
+    "@jest/types" "^25.4.0"
+    ansi-escapes "^4.2.1"
+    chalk "^3.0.0"
+    exit "^0.1.2"
+    graceful-fs "^4.2.3"
+    jest-changed-files "^25.4.0"
+    jest-config "^25.4.0"
+    jest-haste-map "^25.4.0"
+    jest-message-util "^25.4.0"
+    jest-regex-util "^25.2.6"
+    jest-resolve "^25.4.0"
+    jest-resolve-dependencies "^25.4.0"
+    jest-runner "^25.4.0"
+    jest-runtime "^25.4.0"
+    jest-snapshot "^25.4.0"
+    jest-util "^25.4.0"
+    jest-validate "^25.4.0"
+    jest-watcher "^25.4.0"
+    micromatch "^4.0.2"
+    p-each-series "^2.1.0"
+    realpath-native "^2.0.0"
+    rimraf "^3.0.0"
+    slash "^3.0.0"
+    strip-ansi "^6.0.0"
+
+"@jest/environment@^25.4.0":
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@jest/environment/-/environment-25.4.0.tgz#45071f525f0d8c5a51ed2b04fd42b55a8f0c7cb3"
+  integrity sha1-RQcfUl8NjFpR7SsE/UK1Wo8MfLM=
+  dependencies:
+    "@jest/fake-timers" "^25.4.0"
+    "@jest/types" "^25.4.0"
+    jest-mock "^25.4.0"
+
+"@jest/fake-timers@^25.4.0":
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@jest/fake-timers/-/fake-timers-25.4.0.tgz#3a9a4289ba836abd084953dca406389a57e00fbd"
+  integrity sha1-OppCibqDar0ISVPcpAY4mlfgD70=
+  dependencies:
+    "@jest/types" "^25.4.0"
+    jest-message-util "^25.4.0"
+    jest-mock "^25.4.0"
+    jest-util "^25.4.0"
+    lolex "^5.0.0"
+
+"@jest/reporters@^25.4.0":
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@jest/reporters/-/reporters-25.4.0.tgz#836093433b32ce4e866298af2d6fcf6ed351b0b0"
+  integrity sha1-g2CTQzsyzk6GYpivLW/PbtNRsLA=
+  dependencies:
+    "@bcoe/v8-coverage" "^0.2.3"
+    "@jest/console" "^25.4.0"
+    "@jest/test-result" "^25.4.0"
+    "@jest/transform" "^25.4.0"
+    "@jest/types" "^25.4.0"
+    chalk "^3.0.0"
+    collect-v8-coverage "^1.0.0"
+    exit "^0.1.2"
+    glob "^7.1.2"
+    istanbul-lib-coverage "^3.0.0"
+    istanbul-lib-instrument "^4.0.0"
+    istanbul-lib-report "^3.0.0"
+    istanbul-lib-source-maps "^4.0.0"
+    istanbul-reports "^3.0.2"
+    jest-haste-map "^25.4.0"
+    jest-resolve "^25.4.0"
+    jest-util "^25.4.0"
+    jest-worker "^25.4.0"
+    slash "^3.0.0"
+    source-map "^0.6.0"
+    string-length "^3.1.0"
+    terminal-link "^2.0.0"
+    v8-to-istanbul "^4.1.3"
+  optionalDependencies:
+    node-notifier "^6.0.0"
+
+"@jest/source-map@^25.2.6":
+  version "25.2.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@jest/source-map/-/source-map-25.2.6.tgz#0ef2209514c6d445ebccea1438c55647f22abb4c"
+  integrity sha1-DvIglRTG1EXrzOoUOMVWR/Iqu0w=
+  dependencies:
+    callsites "^3.0.0"
+    graceful-fs "^4.2.3"
+    source-map "^0.6.0"
+
+"@jest/test-result@^25.4.0":
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@jest/test-result/-/test-result-25.4.0.tgz#6f2ec2c8da9981ef013ad8651c1c6f0cb20c6324"
+  integrity sha1-by7CyNqZge8BOthlHBxvDLIMYyQ=
+  dependencies:
+    "@jest/console" "^25.4.0"
+    "@jest/types" "^25.4.0"
+    "@types/istanbul-lib-coverage" "^2.0.0"
+    collect-v8-coverage "^1.0.0"
+
+"@jest/test-sequencer@^25.4.0":
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@jest/test-sequencer/-/test-sequencer-25.4.0.tgz#2b96f9d37f18dc3336b28e3c8070f97f9f55f43b"
+  integrity sha1-K5b5038Y3DM2so48gHD5f59V9Ds=
+  dependencies:
+    "@jest/test-result" "^25.4.0"
+    jest-haste-map "^25.4.0"
+    jest-runner "^25.4.0"
+    jest-runtime "^25.4.0"
+
+"@jest/transform@^25.4.0":
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@jest/transform/-/transform-25.4.0.tgz#eef36f0367d639e2fd93dccd758550377fbb9962"
+  integrity sha1-7vNvA2fWOeL9k9zNdYVQN3+7mWI=
+  dependencies:
+    "@babel/core" "^7.1.0"
+    "@jest/types" "^25.4.0"
+    babel-plugin-istanbul "^6.0.0"
+    chalk "^3.0.0"
+    convert-source-map "^1.4.0"
+    fast-json-stable-stringify "^2.0.0"
+    graceful-fs "^4.2.3"
+    jest-haste-map "^25.4.0"
+    jest-regex-util "^25.2.6"
+    jest-util "^25.4.0"
+    micromatch "^4.0.2"
+    pirates "^4.0.1"
+    realpath-native "^2.0.0"
+    slash "^3.0.0"
+    source-map "^0.6.1"
+    write-file-atomic "^3.0.0"
+
+"@jest/types@^25.4.0":
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@jest/types/-/types-25.4.0.tgz#5afeb8f7e1cba153a28e5ac3c9fe3eede7206d59"
+  integrity sha1-Wv649+HLoVOijlrDyf4+7ecgbVk=
+  dependencies:
+    "@types/istanbul-lib-coverage" "^2.0.0"
+    "@types/istanbul-reports" "^1.1.1"
+    "@types/yargs" "^15.0.0"
+    chalk "^3.0.0"
+
+"@mrmlnc/readdir-enhanced@^2.2.1":
+  version "2.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
+  integrity sha1-UkryQNGjYFJ7cwR17PoTRKpUDd4=
+  dependencies:
+    call-me-maybe "^1.0.1"
+    glob-to-regexp "^0.3.0"
+
+"@nodelib/fs.scandir@2.1.1":
+  version "2.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@nodelib/fs.scandir/-/fs.scandir-2.1.1.tgz#7fa8fed654939e1a39753d286b48b4836d00e0eb"
+  integrity sha1-f6j+1lSTnho5dT0oa0i0g20A4Os=
+  dependencies:
+    "@nodelib/fs.stat" "2.0.1"
+    run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.1", "@nodelib/fs.stat@^2.0.1":
+  version "2.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@nodelib/fs.stat/-/fs.stat-2.0.1.tgz#814f71b1167390cfcb6a6b3d9cdeb0951a192c14"
+  integrity sha1-gU9xsRZzkM/Lams9nN6wlRoZLBQ=
+
+"@nodelib/fs.stat@^1.1.2":
+  version "1.1.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
+  integrity sha1-K1o6s/kYzKSKjHVMCBaOPwPrphs=
+
+"@nodelib/fs.walk@^1.2.1":
+  version "1.2.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@nodelib/fs.walk/-/fs.walk-1.2.2.tgz#6a6450c5e17012abd81450eb74949a4d970d2807"
+  integrity sha1-amRQxeFwEqvYFFDrdJSaTZcNKAc=
+  dependencies:
+    "@nodelib/fs.scandir" "2.1.1"
+    fastq "^1.6.0"
+
+"@sinonjs/commons@^1.7.0":
+  version "1.7.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2"
+  integrity sha1-UF9Vx04CcrQ/bFLYGUa+1wWPwOI=
+  dependencies:
+    type-detect "4.0.8"
+
+"@types/babel__core@^7.1.7":
+  version "7.1.7"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/babel__core/-/babel__core-7.1.7.tgz#1dacad8840364a57c98d0dd4855c6dd3752c6b89"
+  integrity sha1-HaytiEA2SlfJjQ3UhVxt03Usa4k=
+  dependencies:
+    "@babel/parser" "^7.1.0"
+    "@babel/types" "^7.0.0"
+    "@types/babel__generator" "*"
+    "@types/babel__template" "*"
+    "@types/babel__traverse" "*"
+
+"@types/babel__generator@*":
+  version "7.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/babel__generator/-/babel__generator-7.0.2.tgz#d2112a6b21fad600d7674274293c85dce0cb47fc"
+  integrity sha1-0hEqayH61gDXZ0J0KTyF3ODLR/w=
+  dependencies:
+    "@babel/types" "^7.0.0"
+
+"@types/babel__template@*":
+  version "7.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/babel__template/-/babel__template-7.0.2.tgz#4ff63d6b52eddac1de7b975a5223ed32ecea9307"
+  integrity sha1-T/Y9a1Lt2sHee5daUiPtMuzqkwc=
+  dependencies:
+    "@babel/parser" "^7.1.0"
+    "@babel/types" "^7.0.0"
+
+"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6":
+  version "7.0.7"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/babel__traverse/-/babel__traverse-7.0.7.tgz#2496e9ff56196cc1429c72034e07eab6121b6f3f"
+  integrity sha1-JJbp/1YZbMFCnHIDTgfqthIbbz8=
+  dependencies:
+    "@babel/types" "^7.3.0"
+
+"@types/cheerio@*":
+  version "0.22.12"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/cheerio/-/cheerio-0.22.12.tgz#93c050401d4935a5376e8b352965f7458bed5340"
+  integrity sha1-k8BQQB1JNaU3bos1KWX3RYvtU0A=
+  dependencies:
+    "@types/node" "*"
+
+"@types/classnames@2.2.8":
+  version "2.2.8"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/classnames/-/classnames-2.2.8.tgz#17139e1e1104203572caa4368f6796f6225b70b4"
+  integrity sha1-FxOeHhEEIDVyyqQ2j2eW9iJbcLQ=
+
+"@types/clipboard@2.0.1":
+  version "2.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/clipboard/-/clipboard-2.0.1.tgz#75a74086c293d75b12bc93ff13bc7797fef05a40"
+  integrity sha1-dadAhsKT11sSvJP/E7x3l/7wWkA=
+
+"@types/color-name@^1.1.1":
+  version "1.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
+  integrity sha1-HBJhu+qhCoBVu8XYq4S3sq/IRqA=
+
+"@types/d3-array@1.2.4":
+  version "1.2.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/d3-array/-/d3-array-1.2.4.tgz#7088445c8717ba1fba416a1df7bbd11cc72a3763"
+  integrity sha1-cIhEXIcXuh+6QWod97vRHMcqN2M=
+
+"@types/d3-color@*":
+  version "1.2.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/d3-color/-/d3-color-1.2.2.tgz#80cf7cfff7401587b8f89307ba36fe4a576bc7cf"
+  integrity sha1-gM98//dAFYe4+JMHujb+Sldrx88=
+
+"@types/d3-hierarchy@1.1.4":
+  version "1.1.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/d3-hierarchy/-/d3-hierarchy-1.1.4.tgz#b04dfcb1f2074da789ada10fe4942d13f0bce421"
+  integrity sha1-sE38sfIHTaeJraEP5JQtE/C85CE=
+
+"@types/d3-interpolate@*":
+  version "1.3.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/d3-interpolate/-/d3-interpolate-1.3.1.tgz#1c280511f622de9b0b47d463fa55f9a4fd6f5fc8"
+  integrity sha1-HCgFEfYi3psLR9Rj+lX5pP1vX8g=
+  dependencies:
+    "@types/d3-color" "*"
+
+"@types/d3-path@*":
+  version "1.0.8"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/d3-path/-/d3-path-1.0.8.tgz#48e6945a8ff43ee0a1ce85c8cfa2337de85c7c79"
+  integrity sha1-SOaUWo/0PuChzoXIz6IzfehcfHk=
+
+"@types/d3-scale@2.0.2":
+  version "2.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/d3-scale/-/d3-scale-2.0.2.tgz#61145948aa1a52ab31384766cd013308699112b3"
+  integrity sha1-YRRZSKoaUqsxOEdmzQEzCGmRErM=
+  dependencies:
+    "@types/d3-time" "*"
+
+"@types/d3-selection@*":
+  version "1.4.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/d3-selection/-/d3-selection-1.4.1.tgz#fa1f8710a6b5d7cfe5c6caa61d161be7cae4a022"
+  integrity sha1-+h+HEKa118/lxsqmHRYb58rkoCI=
+
+"@types/d3-selection@1.3.2":
+  version "1.3.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/d3-selection/-/d3-selection-1.3.2.tgz#dd5661a560ba9ce3aba823c424b8d4a1bc7e833f"
+  integrity sha1-3VZhpWC6nOOrqCPEJLjUobx+gz8=
+
+"@types/d3-shape@1.2.4":
+  version "1.2.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/d3-shape/-/d3-shape-1.2.4.tgz#e65585f2254d83ae42c47af2e730dd9b97952996"
+  integrity sha1-5lWF8iVNg65CxHry5zDdm5eVKZY=
+  dependencies:
+    "@types/d3-path" "*"
+
+"@types/d3-time@*":
+  version "1.0.10"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/d3-time/-/d3-time-1.0.10.tgz#d338c7feac93a98a32aac875d1100f92c7b61f4f"
+  integrity sha1-0zjH/qyTqYoyqsh10RAPkse2H08=
+
+"@types/d3-zoom@1.7.3":
+  version "1.7.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/d3-zoom/-/d3-zoom-1.7.3.tgz#ed9421551328157f70edabc401d8c91c38d360d9"
+  integrity sha1-7ZQhVRMoFX9w7avEAdjJHDjTYNk=
+  dependencies:
+    "@types/d3-interpolate" "*"
+    "@types/d3-selection" "*"
+
+"@types/enzyme@3.10.5":
+  version "3.10.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/enzyme/-/enzyme-3.10.5.tgz#fe7eeba3550369eed20e7fb565bfb74eec44f1f0"
+  integrity sha1-/n7ro1UDae7SDn+1Zb+3TuxE8fA=
+  dependencies:
+    "@types/cheerio" "*"
+    "@types/react" "*"
+
+"@types/eslint-visitor-keys@^1.0.0":
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
+  integrity sha1-HuMNeVRMqE1o1LPNsK9PIFZj3S0=
+
+"@types/events@*":
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
+  integrity sha1-KGLz9Yqaf3w+eNefEw3U1xwlwqc=
+
+"@types/glob@^7.1.1":
+  version "7.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
+  integrity sha1-qlmhxuP7xCHgfM0xqUTDDrpSFXU=
+  dependencies:
+    "@types/events" "*"
+    "@types/minimatch" "*"
+    "@types/node" "*"
+
+"@types/history@^3":
+  version "3.2.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/history/-/history-3.2.3.tgz#2416fee5cac641da2d05a905de5af5cb50162f60"
+  integrity sha1-JBb+5crGQdotBakF3lr1y1AWL2A=
+
+"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
+  version "2.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
+  integrity sha1-QplbRG25pIoRoH7Ag0mahg6ROP8=
+
+"@types/istanbul-lib-report@*":
+  version "1.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#e5471e7fa33c61358dd38426189c037a58433b8c"
+  integrity sha1-5Ucef6M8YTWN04QmGJwDelhDO4w=
+  dependencies:
+    "@types/istanbul-lib-coverage" "*"
+
+"@types/istanbul-reports@^1.1.1":
+  version "1.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a"
+  integrity sha1-eoy/akBvNsit2HFiWyeOrwsNJVo=
+  dependencies:
+    "@types/istanbul-lib-coverage" "*"
+    "@types/istanbul-lib-report" "*"
+
+"@types/jest@25.2.1":
+  version "25.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/jest/-/jest-25.2.1.tgz#9544cd438607955381c1bdbdb97767a249297db5"
+  integrity sha1-lUTNQ4YHlVOBwb29uXdnokkpfbU=
+  dependencies:
+    jest-diff "^25.2.1"
+    pretty-format "^25.2.1"
+
+"@types/jest@^23.0.2":
+  version "23.3.14"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/jest/-/jest-23.3.14.tgz#37daaf78069e7948520474c87b80092ea912520a"
+  integrity sha1-N9qveAaeeUhSBHTIe4AJLqkSUgo=
+
+"@types/json-schema@^7.0.3":
+  version "7.0.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
+  integrity sha1-vf1p1h5GTcyBslFZwnDXWnPBpjY=
+
+"@types/lodash@4.14.159":
+  version "4.14.159"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/lodash/-/lodash-4.14.159.tgz#61089719dc6fdd9c5cb46efc827f2571d1517065"
+  integrity sha1-YQiXGdxv3ZxctG78gn8lcdFRcGU=
+
+"@types/minimatch@*":
+  version "3.0.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
+  integrity sha1-PcoOPzOyAPx9ETnAzZbBJoyt/Z0=
+
+"@types/node@*":
+  version "12.6.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/node/-/node-12.6.3.tgz#44d507c5634f85e7164707ca36bba21b5213d487"
+  integrity sha1-RNUHxWNPhecWRwfKNruiG1IT1Ic=
+
+"@types/normalize-package-data@^2.4.0":
+  version "2.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
+  integrity sha1-5IbQ2XOW15vu3QpuM/RTT/a0lz4=
+
+"@types/prettier@^1.19.0":
+  version "1.19.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/prettier/-/prettier-1.19.1.tgz#33509849f8e679e4add158959fdb086440e9553f"
+  integrity sha1-M1CYSfjmeeSt0ViVn9sIZEDpVT8=
+
+"@types/prop-types@*":
+  version "15.7.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/prop-types/-/prop-types-15.7.1.tgz#f1a11e7babb0c3cad68100be381d1e064c68f1f6"
+  integrity sha1-8aEee6uww8rWgQC+OB0eBkxo8fY=
+
+"@types/react-dom@16.8.4":
+  version "16.8.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/react-dom/-/react-dom-16.8.4.tgz#7fb7ba368857c7aa0f4e4511c4710ca2c5a12a88"
+  integrity sha1-f7e6NohXx6oPTkURxHEMosWhKog=
+  dependencies:
+    "@types/react" "*"
+
+"@types/react-intl@2.3.17":
+  version "2.3.17"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/react-intl/-/react-intl-2.3.17.tgz#e1fc6e46e8af58bdef9531259d509380a8a99e8e"
+  integrity sha1-4fxuRuivWL3vlTElnVCTgKipno4=
+
+"@types/react-modal@3.8.2":
+  version "3.8.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/react-modal/-/react-modal-3.8.2.tgz#401309e624cde522d65c3ef04918dbe325383598"
+  integrity sha1-QBMJ5iTN5SLWXD7wSRjb4yU4NZg=
+  dependencies:
+    "@types/react" "*"
+
+"@types/react-router@3.0.20":
+  version "3.0.20"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/react-router/-/react-router-3.0.20.tgz#a711682475ccef70ad9ad9e459859380221e6ee6"
+  integrity sha1-pxFoJHXM73CtmtnkWYWTgCIebuY=
+  dependencies:
+    "@types/history" "^3"
+    "@types/react" "*"
+
+"@types/react-select@1.2.6":
+  version "1.2.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/react-select/-/react-select-1.2.6.tgz#8a9579705e04b2c15ce529379402980d80e9d243"
+  integrity sha1-ipV5cF4EssFc5Sk3lAKYDYDp0kM=
+  dependencies:
+    "@types/react" "*"
+
+"@types/react-virtualized@9.21.0":
+  version "9.21.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/react-virtualized/-/react-virtualized-9.21.0.tgz#5889c62ddb23628252a4612b0af0d39777d3a889"
+  integrity sha1-WInGLdsjYoJSpGErCvDTl3fTqIk=
+  dependencies:
+    "@types/prop-types" "*"
+    "@types/react" "*"
+
+"@types/react@*", "@types/react@16.8.23":
+  version "16.8.23"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/react/-/react-16.8.23.tgz#ec6be3ceed6353a20948169b6cb4c97b65b97ad2"
+  integrity sha1-7Gvjzu1jU6IJSBabbLTJe2W5etI=
+  dependencies:
+    "@types/prop-types" "*"
+    csstype "^2.2.0"
+
+"@types/stack-utils@^1.0.1":
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
+  integrity sha1-CoUdO9lkmPolwzq3J47TvWXwbD4=
+
+"@types/yargs-parser@*":
+  version "15.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"
+  integrity sha1-yz+fdBhp4gzOMw/765JxWQSDiC0=
+
+"@types/yargs@^15.0.0":
+  version "15.0.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@types/yargs/-/yargs-15.0.4.tgz#7e5d0f8ca25e9d5849f2ea443cf7c402decd8299"
+  integrity sha1-fl0PjKJenVhJ8upEPPfEAt7Ngpk=
+  dependencies:
+    "@types/yargs-parser" "*"
+
+"@typescript-eslint/experimental-utils@2.29.0":
+  version "2.29.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@typescript-eslint/experimental-utils/-/experimental-utils-2.29.0.tgz#3cb8060de9265ba131625a96bbfec31ba6d4a0fe"
+  integrity sha1-PLgGDekmW6ExYlqWu/7DG6bUoP4=
+  dependencies:
+    "@types/json-schema" "^7.0.3"
+    "@typescript-eslint/typescript-estree" "2.29.0"
+    eslint-scope "^5.0.0"
+    eslint-utils "^2.0.0"
+
+"@typescript-eslint/experimental-utils@^2.5.0":
+  version "2.18.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@typescript-eslint/experimental-utils/-/experimental-utils-2.18.0.tgz#e4eab839082030282496c1439bbf9fdf2a4f3da8"
+  integrity sha1-5Oq4OQggMCgklsFDm7+f3ypPPag=
+  dependencies:
+    "@types/json-schema" "^7.0.3"
+    "@typescript-eslint/typescript-estree" "2.18.0"
+    eslint-scope "^5.0.0"
+
+"@typescript-eslint/parser@2.29.0":
+  version "2.29.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@typescript-eslint/parser/-/parser-2.29.0.tgz#6e3c4e21ed6393dc05b9d8b47f0b7e731ef21c9c"
+  integrity sha1-bjxOIe1jk9wFudi0fwt+cx7yHJw=
+  dependencies:
+    "@types/eslint-visitor-keys" "^1.0.0"
+    "@typescript-eslint/experimental-utils" "2.29.0"
+    "@typescript-eslint/typescript-estree" "2.29.0"
+    eslint-visitor-keys "^1.1.0"
+
+"@typescript-eslint/typescript-estree@2.18.0":
+  version "2.18.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@typescript-eslint/typescript-estree/-/typescript-estree-2.18.0.tgz#cfbd16ed1b111166617d718619c19b62764c8460"
+  integrity sha1-z70W7RsREWZhfXGGGcGbYnZMhGA=
+  dependencies:
+    debug "^4.1.1"
+    eslint-visitor-keys "^1.1.0"
+    glob "^7.1.6"
+    is-glob "^4.0.1"
+    lodash "^4.17.15"
+    semver "^6.3.0"
+    tsutils "^3.17.1"
+
+"@typescript-eslint/typescript-estree@2.29.0":
+  version "2.29.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/@typescript-eslint/typescript-estree/-/typescript-estree-2.29.0.tgz#1be6612bb02fc37ac9f466521c1459a4744e8d3a"
+  integrity sha1-G+ZhK7Avw3rJ9GZSHBRZpHROjTo=
+  dependencies:
+    debug "^4.1.1"
+    eslint-visitor-keys "^1.1.0"
+    glob "^7.1.6"
+    is-glob "^4.0.1"
+    lodash "^4.17.15"
+    semver "^6.3.0"
+    tsutils "^3.17.1"
+
+abab@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f"
+  integrity sha1-q6CrTF7uLUx500h9hUUPsjduuw8=
+
+acorn-globals@^4.3.2:
+  version "4.3.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7"
+  integrity sha1-n6GSat3BHJcwjE5m163Q1Awycuc=
+  dependencies:
+    acorn "^6.0.1"
+    acorn-walk "^6.0.1"
+
+acorn-jsx@^5.2.0:
+  version "5.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe"
+  integrity sha1-TGYGkXPW/daO2FI5/CViJhgrLr4=
+
+acorn-walk@^6.0.1:
+  version "6.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c"
+  integrity sha1-Ejy487hMIXHx9/slJhWxx4prGow=
+
+acorn@^6.0.1:
+  version "6.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/acorn/-/acorn-6.2.0.tgz#67f0da2fc339d6cfb5d6fb244fd449f33cd8bbe3"
+  integrity sha1-Z/DaL8M51s+11vskT9RJ8zzYu+M=
+
+acorn@^7.1.0, acorn@^7.1.1:
+  version "7.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf"
+  integrity sha1-41Zo3gtALzWd5RXFSCoaufiaab8=
+
+airbnb-prop-types@^2.15.0:
+  version "2.15.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/airbnb-prop-types/-/airbnb-prop-types-2.15.0.tgz#5287820043af1eb469f5b0af0d6f70da6c52aaef"
+  integrity sha1-UoeCAEOvHrRp9bCvDW9w2mxSqu8=
+  dependencies:
+    array.prototype.find "^2.1.0"
+    function.prototype.name "^1.1.1"
+    has "^1.0.3"
+    is-regex "^1.0.4"
+    object-is "^1.0.1"
+    object.assign "^4.1.0"
+    object.entries "^1.1.0"
+    prop-types "^15.7.2"
+    prop-types-exact "^1.2.0"
+    react-is "^16.9.0"
+
+ajv@^6.10.0:
+  version "6.12.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd"
+  integrity sha1-xinF7O0XuvMUQ3kY0tqIyZ1ZWM0=
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.2"
+
+ajv@^6.10.2, ajv@^6.5.5:
+  version "6.10.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52"
+  integrity sha1-086gTWsBeyiUrWkED+yLYj60vVI=
+  dependencies:
+    fast-deep-equal "^2.0.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.2"
+
+ansi-escapes@^4.2.1:
+  version "4.3.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61"
+  integrity sha1-pcR8xDGB8fOP/XB2g3cA05VSKmE=
+  dependencies:
+    type-fest "^0.11.0"
+
+ansi-regex@^4.1.0:
+  version "4.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
+  integrity sha1-i5+PCM8ay4Q3Vqg5yox+MWjFGZc=
+
+ansi-regex@^5.0.0:
+  version "5.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
+  integrity sha1-OIU59VF5vzkznIGvMKZU1p+Hy3U=
+
+ansi-styles@^3.2.0, ansi-styles@^3.2.1:
+  version "3.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+  integrity sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=
+  dependencies:
+    color-convert "^1.9.0"
+
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
+  version "4.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
+  integrity sha1-kK51xCTQCNJiTFvynq0xd+v881k=
+  dependencies:
+    "@types/color-name" "^1.1.1"
+    color-convert "^2.0.1"
+
+anymatch@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
+  integrity sha1-vLJLTzeTTZqnrBe0ra+J58du8us=
+  dependencies:
+    micromatch "^3.1.4"
+    normalize-path "^2.1.1"
+
+anymatch@^3.0.3:
+  version "3.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142"
+  integrity sha1-xV7PAhheJGklk5kxDBc84xIzsUI=
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
+argparse@^1.0.7:
+  version "1.0.10"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
+  integrity sha1-vNZ5HqWuCXJeF+WtmIE0zUCz2RE=
+  dependencies:
+    sprintf-js "~1.0.2"
+
+aria-query@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc"
+  integrity sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=
+  dependencies:
+    ast-types-flow "0.0.7"
+    commander "^2.11.0"
+
+arr-diff@^4.0.0:
+  version "4.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+  integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=
+
+arr-flatten@^1.1.0:
+  version "1.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+  integrity sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=
+
+arr-union@^3.1.0:
+  version "3.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+  integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
+
+array-equal@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
+  integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
+
+array-filter@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
+  integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
+
+array-find-index@^1.0.1:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
+  integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=
+
+array-includes@^3.0.3:
+  version "3.0.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d"
+  integrity sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=
+  dependencies:
+    define-properties "^1.1.2"
+    es-abstract "^1.7.0"
+
+array-includes@^3.1.1:
+  version "3.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348"
+  integrity sha1-zdZ+aFK9+cEhVGB4ZzIlXtJFk0g=
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0"
+    is-string "^1.0.5"
+
+array-union@^1.0.2:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
+  integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=
+  dependencies:
+    array-uniq "^1.0.1"
+
+array-union@^2.1.0:
+  version "2.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
+  integrity sha1-t5hCCtvrHego2ErNii4j0+/oXo0=
+
+array-uniq@^1.0.1:
+  version "1.0.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
+  integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=
+
+array-unique@^0.3.2:
+  version "0.3.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+  integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
+
+array.prototype.find@^2.1.0:
+  version "2.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/array.prototype.find/-/array.prototype.find-2.1.1.tgz#3baca26108ca7affb08db06bf0be6cb3115a969c"
+  integrity sha1-O6yiYQjKev+wjbBr8L5ssxFalpw=
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.4"
+
+array.prototype.flat@^1.2.1:
+  version "1.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz#812db8f02cad24d3fab65dd67eabe3b8903494a4"
+  integrity sha1-gS248CytJNP6tl3WfqvjuJA0lKQ=
+  dependencies:
+    define-properties "^1.1.2"
+    es-abstract "^1.10.0"
+    function-bind "^1.1.1"
+
+array.prototype.flat@^1.2.3:
+  version "1.2.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b"
+  integrity sha1-DegrQmsDGNv9uUAInjiwQ9N/bHs=
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+
+arrify@^1.0.1:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
+  integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
+
+asap@~2.0.3:
+  version "2.0.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+  integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
+
+asn1@~0.2.3:
+  version "0.2.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
+  integrity sha1-jSR136tVO7M+d7VOWeiAu4ziMTY=
+  dependencies:
+    safer-buffer "~2.1.0"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+  integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
+
+assign-symbols@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+  integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
+
+ast-types-flow@0.0.7, ast-types-flow@^0.0.7:
+  version "0.0.7"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
+  integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0=
+
+astral-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
+  integrity sha1-bIw/uCfdQ+45GPJ7gngqt2WKb9k=
+
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+  integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
+
+atob@^2.1.1:
+  version "2.1.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
+  integrity sha1-bZUX654DDSQ2ZmZR6GvZ9vE1M8k=
+
+aws-sign2@~0.7.0:
+  version "0.7.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+  integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
+
+aws4@^1.8.0:
+  version "1.8.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
+  integrity sha1-8OAD2cqef1nHpQiUXXsu+aBKVC8=
+
+axobject-query@^2.0.2:
+  version "2.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/axobject-query/-/axobject-query-2.0.2.tgz#ea187abe5b9002b377f925d8bf7d1c561adf38f9"
+  integrity sha1-6hh6vluQArN3+SXYv30cVhrfOPk=
+  dependencies:
+    ast-types-flow "0.0.7"
+
+babel-jest@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/babel-jest/-/babel-jest-25.4.0.tgz#409eb3e2ddc2ad9a92afdbb00991f1633f8018d0"
+  integrity sha1-QJ6z4t3CrZqSr9uwCZHxYz+AGNA=
+  dependencies:
+    "@jest/transform" "^25.4.0"
+    "@jest/types" "^25.4.0"
+    "@types/babel__core" "^7.1.7"
+    babel-plugin-istanbul "^6.0.0"
+    babel-preset-jest "^25.4.0"
+    chalk "^3.0.0"
+    slash "^3.0.0"
+
+babel-plugin-emotion@^10.0.14, babel-plugin-emotion@^10.0.17:
+  version "10.0.17"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/babel-plugin-emotion/-/babel-plugin-emotion-10.0.17.tgz#5673fbed7b1ed61b4b98d5530f33c8a4d1b08484"
+  integrity sha1-VnP77Xse1htLmNVTDzPIpNGwhIQ=
+  dependencies:
+    "@babel/helper-module-imports" "^7.0.0"
+    "@emotion/hash" "0.7.2"
+    "@emotion/memoize" "0.7.2"
+    "@emotion/serialize" "^0.11.10"
+    babel-plugin-macros "^2.0.0"
+    babel-plugin-syntax-jsx "^6.18.0"
+    convert-source-map "^1.5.0"
+    escape-string-regexp "^1.0.5"
+    find-root "^1.1.0"
+    source-map "^0.5.7"
+
+babel-plugin-istanbul@^6.0.0:
+  version "6.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz#e159ccdc9af95e0b570c75b4573b7c34d671d765"
+  integrity sha1-4VnM3Jr5XgtXDHW0Vzt8NNZx12U=
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.0.0"
+    "@istanbuljs/load-nyc-config" "^1.0.0"
+    "@istanbuljs/schema" "^0.1.2"
+    istanbul-lib-instrument "^4.0.0"
+    test-exclude "^6.0.0"
+
+babel-plugin-jest-hoist@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.4.0.tgz#0c122c1b93fb76f52d2465be2e8069e798e9d442"
+  integrity sha1-DBIsG5P7dvUtJGW+LoBp55jp1EI=
+  dependencies:
+    "@types/babel__traverse" "^7.0.6"
+
+babel-plugin-macros@^2.0.0:
+  version "2.6.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/babel-plugin-macros/-/babel-plugin-macros-2.6.1.tgz#41f7ead616fc36f6a93180e89697f69f51671181"
+  integrity sha1-Qffq1hb8NvapMYDolpf2n1FnEYE=
+  dependencies:
+    "@babel/runtime" "^7.4.2"
+    cosmiconfig "^5.2.0"
+    resolve "^1.10.0"
+
+babel-plugin-syntax-jsx@^6.18.0:
+  version "6.18.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
+  integrity sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=
+
+babel-preset-current-node-syntax@^0.1.2:
+  version "0.1.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.2.tgz#fb4a4c51fe38ca60fede1dc74ab35eb843cb41d6"
+  integrity sha1-+0pMUf44ymD+3h3HSrNeuEPLQdY=
+  dependencies:
+    "@babel/plugin-syntax-async-generators" "^7.8.4"
+    "@babel/plugin-syntax-bigint" "^7.8.3"
+    "@babel/plugin-syntax-class-properties" "^7.8.3"
+    "@babel/plugin-syntax-json-strings" "^7.8.3"
+    "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
+    "@babel/plugin-syntax-numeric-separator" "^7.8.3"
+    "@babel/plugin-syntax-object-rest-spread" "^7.8.3"
+    "@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.3"
+
+babel-preset-jest@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/babel-preset-jest/-/babel-preset-jest-25.4.0.tgz#10037cc32b751b994b260964629e49dc479abf4c"
+  integrity sha1-EAN8wyt1G5lLJglkYp5J3Eeav0w=
+  dependencies:
+    babel-plugin-jest-hoist "^25.4.0"
+    babel-preset-current-node-syntax "^0.1.2"
+
+babel-runtime@^6.26.0:
+  version "6.26.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
+  integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
+  dependencies:
+    core-js "^2.4.0"
+    regenerator-runtime "^0.11.0"
+
+balanced-match@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+  integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+
+base@^0.11.1:
+  version "0.11.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+  integrity sha1-e95c7RRbbVUakNuH+DxVi060io8=
+  dependencies:
+    cache-base "^1.0.1"
+    class-utils "^0.3.5"
+    component-emitter "^1.2.1"
+    define-property "^1.0.0"
+    isobject "^3.0.1"
+    mixin-deep "^1.2.0"
+    pascalcase "^0.1.1"
+
+bcrypt-pbkdf@^1.0.0:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
+  integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
+  dependencies:
+    tweetnacl "^0.14.3"
+
+boolbase@~1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+  integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
+
+brace-expansion@^1.1.7:
+  version "1.1.11"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  integrity sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=
+  dependencies:
+    balanced-match "^1.0.0"
+    concat-map "0.0.1"
+
+braces@^2.3.1:
+  version "2.3.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
+  integrity sha1-WXn9PxTNUxVl5fot8av/8d+u5yk=
+  dependencies:
+    arr-flatten "^1.1.0"
+    array-unique "^0.3.2"
+    extend-shallow "^2.0.1"
+    fill-range "^4.0.0"
+    isobject "^3.0.1"
+    repeat-element "^1.1.2"
+    snapdragon "^0.8.1"
+    snapdragon-node "^2.0.1"
+    split-string "^3.0.2"
+    to-regex "^3.0.1"
+
+braces@^3.0.1:
+  version "3.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha1-NFThpGLujVmeI23zNs2epPiv4Qc=
+  dependencies:
+    fill-range "^7.0.1"
+
+browser-process-hrtime@^0.1.2:
+  version "0.1.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz#616f00faef1df7ec1b5bf9cfe2bdc3170f26c7b4"
+  integrity sha1-YW8A+u8d9+wbW/nP4r3DFw8mx7Q=
+
+browser-resolve@^1.11.3:
+  version "1.11.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6"
+  integrity sha1-m3y7PQ9RDky4a9vXlhJNKLWJCvY=
+  dependencies:
+    resolve "1.1.7"
+
+bs-logger@0.x:
+  version "0.2.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8"
+  integrity sha1-6302UwenLPl0zGzadraDVK0za9g=
+  dependencies:
+    fast-json-stable-stringify "2.x"
+
+bser@^2.0.0:
+  version "2.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/bser/-/bser-2.1.0.tgz#65fc784bf7f87c009b973c12db6546902fa9c7b5"
+  integrity sha1-Zfx4S/f4fACblzwS22VGkC+px7U=
+  dependencies:
+    node-int64 "^0.4.0"
+
+buffer-from@1.x, buffer-from@^1.0.0:
+  version "1.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
+  integrity sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8=
+
+cache-base@^1.0.1:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+  integrity sha1-Cn9GQWgxyLZi7jb+TnxZ129marI=
+  dependencies:
+    collection-visit "^1.0.0"
+    component-emitter "^1.2.1"
+    get-value "^2.0.6"
+    has-value "^1.0.0"
+    isobject "^3.0.1"
+    set-value "^2.0.0"
+    to-object-path "^0.3.0"
+    union-value "^1.0.0"
+    unset-value "^1.0.0"
+
+call-me-maybe@^1.0.1:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
+  integrity sha1-JtII6onje1y95gJQoV8DHBak1ms=
+
+caller-callsite@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134"
+  integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=
+  dependencies:
+    callsites "^2.0.0"
+
+caller-path@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4"
+  integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=
+  dependencies:
+    caller-callsite "^2.0.0"
+
+callsites@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
+  integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=
+
+callsites@^3.0.0:
+  version "3.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+  integrity sha1-s2MKvYlDQy9Us/BRkjjjPNffL3M=
+
+camelcase-keys@^4.0.0:
+  version "4.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/camelcase-keys/-/camelcase-keys-4.2.0.tgz#a2aa5fb1af688758259c32c141426d78923b9b77"
+  integrity sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=
+  dependencies:
+    camelcase "^4.1.0"
+    map-obj "^2.0.0"
+    quick-lru "^1.0.0"
+
+camelcase@^4.1.0:
+  version "4.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
+  integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
+
+camelcase@^5.0.0, camelcase@^5.3.1:
+  version "5.3.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+  integrity sha1-48mzFWnhBoEd8kL3FXJaH0xJQyA=
+
+capture-exit@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4"
+  integrity sha1-+5U7+uvreB9iiYI52rtCbQilCaQ=
+  dependencies:
+    rsvp "^4.8.4"
+
+caseless@~0.12.0:
+  version "0.12.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+  integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
+
+chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.1:
+  version "2.4.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+  integrity sha1-zUJUFnelQzPPVBpJEIwUMrRMlCQ=
+  dependencies:
+    ansi-styles "^3.2.1"
+    escape-string-regexp "^1.0.5"
+    supports-color "^5.3.0"
+
+chalk@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
+  integrity sha1-P3PCv1JlkfV0zEksUeJFY0n4ROQ=
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chardet@^0.7.0:
+  version "0.7.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
+  integrity sha1-kAlISfCTfy7twkJdDSip5fDLrZ4=
+
+cheerio@^1.0.0-rc.3:
+  version "1.0.0-rc.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6"
+  integrity sha1-CUY21CWy6cD065GkbAVjDJoai/Y=
+  dependencies:
+    css-select "~1.2.0"
+    dom-serializer "~0.1.1"
+    entities "~1.1.1"
+    htmlparser2 "^3.9.1"
+    lodash "^4.15.0"
+    parse5 "^3.0.1"
+
+ci-info@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
+  integrity sha1-Z6npZL4xpR4V5QENWObxKDQAL0Y=
+
+class-utils@^0.3.5:
+  version "0.3.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+  integrity sha1-+TNprouafOAv1B+q0MqDAzGQxGM=
+  dependencies:
+    arr-union "^3.1.0"
+    define-property "^0.2.5"
+    isobject "^3.0.0"
+    static-extend "^0.1.1"
+
+classnames@2.2.6, classnames@^2.2.3, classnames@^2.2.4, classnames@^2.2.5:
+  version "2.2.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
+  integrity sha1-Q5Nb/90pHzJtrQogUwmzjQD2UM4=
+
+cli-cursor@^3.1.0:
+  version "3.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
+  integrity sha1-JkMFp65JDR0Dvwybp8kl0XU68wc=
+  dependencies:
+    restore-cursor "^3.1.0"
+
+cli-width@^2.0.0:
+  version "2.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
+  integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
+
+clipboard@2.0.6:
+  version "2.0.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/clipboard/-/clipboard-2.0.6.tgz#52921296eec0fdf77ead1749421b21c968647376"
+  integrity sha1-UpISlu7A/fd+rRdJQhshyWhkc3Y=
+  dependencies:
+    good-listener "^1.2.2"
+    select "^1.1.2"
+    tiny-emitter "^2.0.0"
+
+cliui@^6.0.0:
+  version "6.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
+  integrity sha1-UR1wLAxOQcoVbX0OlgIfI+EyJbE=
+  dependencies:
+    string-width "^4.2.0"
+    strip-ansi "^6.0.0"
+    wrap-ansi "^6.2.0"
+
+co@^4.6.0:
+  version "4.6.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+  integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
+
+collect-v8-coverage@^1.0.0:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"
+  integrity sha1-zCyOlPwYu9/+ZNZTRXDIpnOyf1k=
+
+collection-visit@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+  integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=
+  dependencies:
+    map-visit "^1.0.0"
+    object-visit "^1.0.0"
+
+color-convert@^1.9.0:
+  version "1.9.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+  integrity sha1-u3GFBpDh8TZWfeYp0tVHHe2kweg=
+  dependencies:
+    color-name "1.1.3"
+
+color-convert@^2.0.1:
+  version "2.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+  integrity sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=
+  dependencies:
+    color-name "~1.1.4"
+
+color-name@1.1.3:
+  version "1.1.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+  integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=
+
+combined-stream@^1.0.6, combined-stream@~1.0.6:
+  version "1.0.8"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  integrity sha1-w9RaizT9cwYxoRCoolIGgrMdWn8=
+  dependencies:
+    delayed-stream "~1.0.0"
+
+commander@^2.11.0, commander@^2.19.0:
+  version "2.20.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
+  integrity sha1-1YuytcHuj4ew00ACfp6U4iLFpCI=
+
+component-emitter@^1.2.1:
+  version "1.3.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
+  integrity sha1-FuQHD7qK4ptnnyIVhT7hgasuq8A=
+
+concat-map@0.0.1:
+  version "0.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+  integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+contains-path@^0.1.0:
+  version "0.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
+  integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=
+
+convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.0:
+  version "1.6.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20"
+  integrity sha1-UbU3qMQ+DwTewZk7/83VBOdYrCA=
+  dependencies:
+    safe-buffer "~5.1.1"
+
+convert-source-map@^1.6.0, convert-source-map@^1.7.0:
+  version "1.7.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
+  integrity sha1-F6LLiC1/d9NJBYXizmxSRCSjpEI=
+  dependencies:
+    safe-buffer "~5.1.1"
+
+copy-descriptor@^0.1.0:
+  version "0.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+  integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
+
+core-js-pure@^3.0.0:
+  version "3.6.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
+  integrity sha1-x5519eONvIWmYtke6lK4JW1TuBM=
+
+core-js@^1.0.0:
+  version "1.2.7"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
+  integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
+
+core-js@^2.4.0:
+  version "2.6.9"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2"
+  integrity sha1-a0shRiDINBUuF5Mjcn/Bl0GwhPI=
+
+core-util-is@1.0.2:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+  integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+
+cosmiconfig@^5.2.0:
+  version "5.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a"
+  integrity sha1-BA9yaAnFked6F8CjYmykW08Wixo=
+  dependencies:
+    import-fresh "^2.0.0"
+    is-directory "^0.3.1"
+    js-yaml "^3.13.1"
+    parse-json "^4.0.0"
+
+cp-file@^6.1.0:
+  version "6.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/cp-file/-/cp-file-6.2.0.tgz#40d5ea4a1def2a9acdd07ba5c0b0246ef73dc10d"
+  integrity sha1-QNXqSh3vKprN0HulwLAkbvc9wQ0=
+  dependencies:
+    graceful-fs "^4.1.2"
+    make-dir "^2.0.0"
+    nested-error-stacks "^2.0.0"
+    pify "^4.0.1"
+    safe-buffer "^5.0.1"
+
+cpy-cli@2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/cpy-cli/-/cpy-cli-2.0.0.tgz#13f1528a231605c52ee7b7f74848e4be82253274"
+  integrity sha1-E/FSiiMWBcUu57f3SEjkvoIlMnQ=
+  dependencies:
+    cpy "^7.0.0"
+    meow "^5.0.0"
+
+cpy@^7.0.0:
+  version "7.3.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/cpy/-/cpy-7.3.0.tgz#62f2847986b4ff9d029710568a49e9a9ab5a210e"
+  integrity sha1-YvKEeYa0/50ClxBWiknpqataIQ4=
+  dependencies:
+    arrify "^1.0.1"
+    cp-file "^6.1.0"
+    globby "^9.2.0"
+    nested-error-stacks "^2.1.0"
+
+create-react-class@^15.5.1:
+  version "15.6.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036"
+  integrity sha1-LXMjf7P5cK5uvgEanmb0bbyoADY=
+  dependencies:
+    fbjs "^0.8.9"
+    loose-envify "^1.3.1"
+    object-assign "^4.1.1"
+
+create-react-context@^0.2.2:
+  version "0.2.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/create-react-context/-/create-react-context-0.2.3.tgz#9ec140a6914a22ef04b8b09b7771de89567cb6f3"
+  integrity sha1-nsFAppFKIu8EuLCbd3HeiVZ8tvM=
+  dependencies:
+    fbjs "^0.8.0"
+    gud "^1.0.0"
+
+cross-spawn@^6.0.0, cross-spawn@^6.0.5:
+  version "6.0.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+  integrity sha1-Sl7Hxk364iw6FBJNus3uhG2Ay8Q=
+  dependencies:
+    nice-try "^1.0.4"
+    path-key "^2.0.1"
+    semver "^5.5.0"
+    shebang-command "^1.2.0"
+    which "^1.2.9"
+
+cross-spawn@^7.0.0:
+  version "7.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/cross-spawn/-/cross-spawn-7.0.2.tgz#d0d7dcfa74e89115c7619f4f721a94e1fdb716d6"
+  integrity sha1-0Nfc+nTokRXHYZ9PchqU4f23FtY=
+  dependencies:
+    path-key "^3.1.0"
+    shebang-command "^2.0.0"
+    which "^2.0.1"
+
+css-select@~1.2.0:
+  version "1.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
+  integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
+  dependencies:
+    boolbase "~1.0.0"
+    css-what "2.1"
+    domutils "1.5.1"
+    nth-check "~1.0.1"
+
+css-what@2.1:
+  version "2.1.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
+  integrity sha1-ptdgRXM2X+dGhsPzEcVlE9iChfI=
+
+css@^2.2.1:
+  version "2.2.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929"
+  integrity sha1-xkZ1XHOXHyu6amAeLPL9cbEpiSk=
+  dependencies:
+    inherits "^2.0.3"
+    source-map "^0.6.1"
+    source-map-resolve "^0.5.2"
+    urix "^0.1.0"
+
+cssom@^0.4.1:
+  version "0.4.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
+  integrity sha1-WmbPk9LQtmHYC/akT7ZfXC5OChA=
+
+cssom@~0.3.6:
+  version "0.3.8"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
+  integrity sha1-nxJ29bK0Y/IRTT8sdSUK+MGjb0o=
+
+cssstyle@^2.0.0:
+  version "2.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/cssstyle/-/cssstyle-2.2.0.tgz#e4c44debccd6b7911ed617a4395e5754bba59992"
+  integrity sha1-5MRN68zWt5Ee1hekOV5XVLulmZI=
+  dependencies:
+    cssom "~0.3.6"
+
+csstype@^2.2.0, csstype@^2.5.7:
+  version "2.6.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/csstype/-/csstype-2.6.6.tgz#c34f8226a94bbb10c32cc0d714afdf942291fc41"
+  integrity sha1-w0+CJqlLuxDDLMDXFK/flCKR/EE=
+
+currently-unhandled@^0.4.1:
+  version "0.4.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
+  integrity sha1-mI3zP+qxke95mmE2nddsF635V+o=
+  dependencies:
+    array-find-index "^1.0.1"
+
+"d3-array@1.2.0 - 2", d3-array@2.4.0:
+  version "2.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/d3-array/-/d3-array-2.4.0.tgz#87f8b9ad11088769c82b5ea846bcb1cc9393f242"
+  integrity sha1-h/i5rREIh2nIK16oRryxzJOT8kI=
+
+d3-color@1:
+  version "1.2.8"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/d3-color/-/d3-color-1.2.8.tgz#4eaf9b60ef188c893fcf8b28f3546aafebfbd9f4"
+  integrity sha1-Tq+bYO8YjIk/z4so81Rqr+v72fQ=
+
+d3-dispatch@1:
+  version "1.0.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/d3-dispatch/-/d3-dispatch-1.0.5.tgz#e25c10a186517cd6c82dd19ea018f07e01e39015"
+  integrity sha1-4lwQoYZRfNbILdGeoBjwfgHjkBU=
+
+d3-drag@1:
+  version "1.2.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/d3-drag/-/d3-drag-1.2.3.tgz#46e206ad863ec465d88c588098a1df444cd33c64"
+  integrity sha1-RuIGrYY+xGXYjFiAmKHfREzTPGQ=
+  dependencies:
+    d3-dispatch "1"
+    d3-selection "1"
+
+d3-ease@1:
+  version "1.0.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/d3-ease/-/d3-ease-1.0.5.tgz#8ce59276d81241b1b72042d6af2d40e76d936ffb"
+  integrity sha1-jOWSdtgSQbG3IELWry1A522Tb/s=
+
+d3-format@1:
+  version "1.3.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/d3-format/-/d3-format-1.3.2.tgz#6a96b5e31bcb98122a30863f7d92365c00603562"
+  integrity sha1-apa14xvLmBIqMIY/fZI2XABgNWI=
+
+d3-hierarchy@1.1.9:
+  version "1.1.9"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83"
+  integrity sha1-L2vuJMqupD+Nw3VF+gFihVlkeoM=
+
+d3-interpolate@1:
+  version "1.3.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/d3-interpolate/-/d3-interpolate-1.3.2.tgz#417d3ebdeb4bc4efcc8fd4361c55e4040211fd68"
+  integrity sha1-QX0+vetLxO/Mj9Q2HFXkBAIR/Wg=
+  dependencies:
+    d3-color "1"
+
+d3-interpolate@^1.2.0:
+  version "1.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987"
+  integrity sha1-Um554tgNqjg/ngwcHH3MDwWD6Yc=
+  dependencies:
+    d3-color "1"
+
+d3-path@1:
+  version "1.0.7"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/d3-path/-/d3-path-1.0.7.tgz#8de7cd693a75ac0b5480d3abaccd94793e58aae8"
+  integrity sha1-jefNaTp1rAtUgNOrrM2UeT5Yqug=
+
+d3-scale@3.2.1:
+  version "3.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/d3-scale/-/d3-scale-3.2.1.tgz#da1684adce7261b4bc7a76fe193d887f0e909e69"
+  integrity sha1-2haErc5yYbS8enb+GT2Ifw6Qnmk=
+  dependencies:
+    d3-array "1.2.0 - 2"
+    d3-format "1"
+    d3-interpolate "^1.2.0"
+    d3-time "1"
+    d3-time-format "2"
+
+d3-selection@1, d3-selection@^1.1.0:
+  version "1.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/d3-selection/-/d3-selection-1.4.0.tgz#ab9ac1e664cf967ebf1b479cc07e28ce9908c474"
+  integrity sha1-q5rB5mTPln6/G0ecwH4ozpkIxHQ=
+
+d3-selection@1.4.1:
+  version "1.4.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/d3-selection/-/d3-selection-1.4.1.tgz#98eedbbe085fbda5bafa2f9e3f3a2f4d7d622a98"
+  integrity sha1-mO7bvghfvaW6+i+ePzovTX1iKpg=
+
+d3-shape@1.3.7:
+  version "1.3.7"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7"
+  integrity sha1-32OAG+B7yYa8VPY3ibT+UCmStdc=
+  dependencies:
+    d3-path "1"
+
+d3-time-format@2:
+  version "2.1.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/d3-time-format/-/d3-time-format-2.1.3.tgz#ae06f8e0126a9d60d6364eac5b1533ae1bac826b"
+  integrity sha1-rgb44BJqnWDWNk6sWxUzrhusgms=
+  dependencies:
+    d3-time "1"
+
+d3-time@1:
+  version "1.0.11"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/d3-time/-/d3-time-1.0.11.tgz#1d831a3e25cd189eb256c17770a666368762bbce"
+  integrity sha1-HYMaPiXNGJ6yVsF3cKZmNodiu84=
+
+d3-timer@1:
+  version "1.0.9"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/d3-timer/-/d3-timer-1.0.9.tgz#f7bb8c0d597d792ff7131e1c24a36dd471a471ba"
+  integrity sha1-97uMDVl9eS/3Ex4cJKNt1HGkcbo=
+
+d3-transition@1:
+  version "1.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/d3-transition/-/d3-transition-1.2.0.tgz#f538c0e21b2aa1f05f3e965f8567e81284b3b2b8"
+  integrity sha1-9TjA4hsqofBfPpZfhWfoEoSzsrg=
+  dependencies:
+    d3-color "1"
+    d3-dispatch "1"
+    d3-ease "1"
+    d3-interpolate "1"
+    d3-selection "^1.1.0"
+    d3-timer "1"
+
+d3-zoom@1.8.3:
+  version "1.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/d3-zoom/-/d3-zoom-1.8.3.tgz#b6a3dbe738c7763121cd05b8a7795ffe17f4fc0a"
+  integrity sha1-tqPb5zjHdjEhzQW4p3lf/hf0/Ao=
+  dependencies:
+    d3-dispatch "1"
+    d3-drag "1"
+    d3-interpolate "1"
+    d3-selection "1"
+    d3-transition "1"
+
+damerau-levenshtein@^1.0.4:
+  version "1.0.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/damerau-levenshtein/-/damerau-levenshtein-1.0.5.tgz#780cf7144eb2e8dbd1c3bb83ae31100ccc31a414"
+  integrity sha1-eAz3FE6y6NvRw7uDrjEQDMwxpBQ=
+
+dashdash@^1.12.0:
+  version "1.14.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+  integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
+  dependencies:
+    assert-plus "^1.0.0"
+
+data-urls@^1.1.0:
+  version "1.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
+  integrity sha1-Fe4Fgrql4iu1nHcUDaj5x2lju/4=
+  dependencies:
+    abab "^2.0.0"
+    whatwg-mimetype "^2.2.0"
+    whatwg-url "^7.0.0"
+
+date-fns@1.30.1:
+  version "1.30.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
+  integrity sha1-LnG/CxGRU9u0zE6I2epaz7UNwFw=
+
+debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
+  version "2.6.9"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+  integrity sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=
+  dependencies:
+    ms "2.0.0"
+
+debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
+  version "4.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
+  integrity sha1-O3ImAlUQnGtYnO4FDx1RYTlmR5E=
+  dependencies:
+    ms "^2.1.1"
+
+decamelize-keys@^1.0.0:
+  version "1.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
+  integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=
+  dependencies:
+    decamelize "^1.1.0"
+    map-obj "^1.0.0"
+
+decamelize@^1.1.0, decamelize@^1.2.0:
+  version "1.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+  integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+
+decode-uri-component@^0.2.0:
+  version "0.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+  integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
+
+deep-is@~0.1.3:
+  version "0.1.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+  integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
+
+deepmerge@^2.1.1:
+  version "2.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
+  integrity sha1-XT/yKgHAD2RUBaL7wX0HeKGAEXA=
+
+deepmerge@^4.2.2:
+  version "4.2.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
+  integrity sha1-RNLqNnm49NT/ujPwPYZfwee/SVU=
+
+define-properties@^1.1.2, define-properties@^1.1.3:
+  version "1.1.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+  integrity sha1-z4jabL7ib+bbcJT2HYcMvYTO6fE=
+  dependencies:
+    object-keys "^1.0.12"
+
+define-property@^0.2.5:
+  version "0.2.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
+  integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=
+  dependencies:
+    is-descriptor "^0.1.0"
+
+define-property@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
+  integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY=
+  dependencies:
+    is-descriptor "^1.0.0"
+
+define-property@^2.0.2:
+  version "2.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
+  integrity sha1-1Flono1lS6d+AqgX+HENcCyxbp0=
+  dependencies:
+    is-descriptor "^1.0.2"
+    isobject "^3.0.1"
+
+delayed-stream@~1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+  integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
+
+delegate@^3.1.2:
+  version "3.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166"
+  integrity sha1-tmtxwxWFIuirV0T3INjKDCr1kWY=
+
+detect-newline@^3.0.0:
+  version "3.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
+  integrity sha1-V29d/GOuGhkv8ZLYrTr2MImRtlE=
+
+diff-sequences@^25.2.6:
+  version "25.2.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd"
+  integrity sha1-X0Z8AO3TU1K3vKRteSfWDmh6dt0=
+
+diff@4.0.1:
+  version "4.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff"
+  integrity sha1-DGZ8tGfru1zqfxTxNcwtuneAqP8=
+
+dir-glob@^2.2.2:
+  version "2.2.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4"
+  integrity sha1-+gnwaUFTyJGLGLoN6vrpR2n8UMQ=
+  dependencies:
+    path-type "^3.0.0"
+
+dir-glob@^3.0.1:
+  version "3.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
+  integrity sha1-Vtv3PZkqSpO6FYT0U0Bj/S5BcX8=
+  dependencies:
+    path-type "^4.0.0"
+
+discontinuous-range@1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
+  integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=
+
+doctrine@1.5.0:
+  version "1.5.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
+  integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=
+  dependencies:
+    esutils "^2.0.2"
+    isarray "^1.0.0"
+
+doctrine@^2.1.0:
+  version "2.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
+  integrity sha1-XNAfwQFiG0LEzX9dGmYkNxbT850=
+  dependencies:
+    esutils "^2.0.2"
+
+doctrine@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
+  integrity sha1-rd6+rXKmV023g2OdyHoSF3OXOWE=
+  dependencies:
+    esutils "^2.0.2"
+
+"dom-helpers@^2.4.0 || ^3.0.0":
+  version "3.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
+  integrity sha1-6bNpcA+Vn2Ls3lprq95LzNkWmvg=
+  dependencies:
+    "@babel/runtime" "^7.1.2"
+
+dom-serializer@0, dom-serializer@~0.1.1:
+  version "0.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
+  integrity sha1-HsQFnihLq+027sKUHUqXChic58A=
+  dependencies:
+    domelementtype "^1.3.0"
+    entities "^1.1.1"
+
+domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
+  version "1.3.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
+  integrity sha1-0EjESzew0Qp/Kj1f7j9DM9eQSB8=
+
+domexception@^1.0.1:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
+  integrity sha1-k3RCZEymoxJh7zbj7Gd/6AVYLJA=
+  dependencies:
+    webidl-conversions "^4.0.2"
+
+domhandler@^2.3.0:
+  version "2.4.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
+  integrity sha1-iAUJfpM9ZehVRvcm1g9euItE+AM=
+  dependencies:
+    domelementtype "1"
+
+domutils@1.5.1:
+  version "1.5.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
+  integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=
+  dependencies:
+    dom-serializer "0"
+    domelementtype "1"
+
+domutils@^1.5.1:
+  version "1.7.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+  integrity sha1-Vuo0HoNOBuZ0ivehyyXaZ+qfjCo=
+  dependencies:
+    dom-serializer "0"
+    domelementtype "1"
+
+ecc-jsbn@~0.1.1:
+  version "0.1.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
+  integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
+  dependencies:
+    jsbn "~0.1.0"
+    safer-buffer "^2.1.0"
+
+emoji-regex@^7.0.2:
+  version "7.0.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
+  integrity sha1-kzoEBShgyF6DwSJHnEdIqOTHIVY=
+
+emoji-regex@^8.0.0:
+  version "8.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+  integrity sha1-6Bj9ac5cz8tARZT4QpY79TFkzDc=
+
+emotion-theming@10.0.19:
+  version "10.0.19"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/emotion-theming/-/emotion-theming-10.0.19.tgz#66d13db74fccaefad71ba57c915b306cf2250295"
+  integrity sha1-ZtE9t0/MrvrXG6V8kVswbPIlApU=
+  dependencies:
+    "@babel/runtime" "^7.5.5"
+    "@emotion/weak-memoize" "0.2.4"
+    hoist-non-react-statics "^3.3.0"
+
+encoding@^0.1.11:
+  version "0.1.12"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
+  integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=
+  dependencies:
+    iconv-lite "~0.4.13"
+
+end-of-stream@^1.1.0:
+  version "1.4.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
+  integrity sha1-7SljTRm6ukY7bOa4CjchPqtx7EM=
+  dependencies:
+    once "^1.4.0"
+
+entities@^1.1.1, entities@~1.1.1:
+  version "1.1.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
+  integrity sha1-vfpzUplmTfr9NFKe1PhSKidf6lY=
+
+enzyme-adapter-react-16@1.15.2:
+  version "1.15.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.2.tgz#b16db2f0ea424d58a808f9df86ab6212895a4501"
+  integrity sha1-sW2y8OpCTVioCPnfhqtiEolaRQE=
+  dependencies:
+    enzyme-adapter-utils "^1.13.0"
+    enzyme-shallow-equal "^1.0.1"
+    has "^1.0.3"
+    object.assign "^4.1.0"
+    object.values "^1.1.1"
+    prop-types "^15.7.2"
+    react-is "^16.12.0"
+    react-test-renderer "^16.0.0-0"
+    semver "^5.7.0"
+
+enzyme-adapter-utils@^1.13.0:
+  version "1.13.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/enzyme-adapter-utils/-/enzyme-adapter-utils-1.13.0.tgz#01c885dde2114b4690bf741f8dc94cee3060eb78"
+  integrity sha1-AciF3eIRS0aQv3QfjclM7jBg63g=
+  dependencies:
+    airbnb-prop-types "^2.15.0"
+    function.prototype.name "^1.1.2"
+    object.assign "^4.1.0"
+    object.fromentries "^2.0.2"
+    prop-types "^15.7.2"
+    semver "^5.7.1"
+
+enzyme-shallow-equal@^1.0.1:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.1.tgz#7afe03db3801c9b76de8440694096412a8d9d49e"
+  integrity sha1-ev4D2zgBybdt6EQGlAlkEqjZ1J4=
+  dependencies:
+    has "^1.0.3"
+    object-is "^1.0.2"
+
+enzyme-to-json@3.4.4:
+  version "3.4.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/enzyme-to-json/-/enzyme-to-json-3.4.4.tgz#b30726c59091d273521b6568c859e8831e94d00e"
+  integrity sha1-swcmxZCR0nNSG2VoyFnogx6U0A4=
+  dependencies:
+    lodash "^4.17.15"
+    react-is "^16.12.0"
+
+enzyme@3.11.0:
+  version "3.11.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/enzyme/-/enzyme-3.11.0.tgz#71d680c580fe9349f6f5ac6c775bc3e6b7a79c28"
+  integrity sha1-cdaAxYD+k0n29axsd1vD5rennCg=
+  dependencies:
+    array.prototype.flat "^1.2.3"
+    cheerio "^1.0.0-rc.3"
+    enzyme-shallow-equal "^1.0.1"
+    function.prototype.name "^1.1.2"
+    has "^1.0.3"
+    html-element-map "^1.2.0"
+    is-boolean-object "^1.0.1"
+    is-callable "^1.1.5"
+    is-number-object "^1.0.4"
+    is-regex "^1.0.5"
+    is-string "^1.0.5"
+    is-subset "^0.1.1"
+    lodash.escape "^4.0.1"
+    lodash.isequal "^4.5.0"
+    object-inspect "^1.7.0"
+    object-is "^1.0.2"
+    object.assign "^4.1.0"
+    object.entries "^1.1.1"
+    object.values "^1.1.1"
+    raf "^3.4.1"
+    rst-selector-parser "^2.2.3"
+    string.prototype.trim "^1.2.1"
+
+error-ex@^1.2.0, error-ex@^1.3.1:
+  version "1.3.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
+  integrity sha1-tKxAZIEH/c3PriQvQovqihTU8b8=
+  dependencies:
+    is-arrayish "^0.2.1"
+
+es-abstract@^1.10.0, es-abstract@^1.12.0, es-abstract@^1.7.0:
+  version "1.13.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
+  integrity sha1-rIYUX91QmdjdSVWMy6Lq+biOJOk=
+  dependencies:
+    es-to-primitive "^1.2.0"
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    is-callable "^1.1.4"
+    is-regex "^1.0.4"
+    object-keys "^1.0.12"
+
+es-abstract@^1.17.0:
+  version "1.17.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9"
+  integrity sha1-2MnR1myJgfuSAOIlHXme7pJ3Suk=
+  dependencies:
+    es-to-primitive "^1.2.1"
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.1"
+    is-callable "^1.1.5"
+    is-regex "^1.0.5"
+    object-inspect "^1.7.0"
+    object-keys "^1.1.1"
+    object.assign "^4.1.0"
+    string.prototype.trimleft "^2.1.1"
+    string.prototype.trimright "^2.1.1"
+
+es-abstract@^1.17.0-next.1, es-abstract@^1.17.4:
+  version "1.17.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184"
+  integrity sha1-467fGXBrIOfCWUw1/A1XYFp54YQ=
+  dependencies:
+    es-to-primitive "^1.2.1"
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.1"
+    is-callable "^1.1.5"
+    is-regex "^1.0.5"
+    object-inspect "^1.7.0"
+    object-keys "^1.1.1"
+    object.assign "^4.1.0"
+    string.prototype.trimleft "^2.1.1"
+    string.prototype.trimright "^2.1.1"
+
+es-to-primitive@^1.2.0:
+  version "1.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377"
+  integrity sha1-7fckeAM0VujdqO8J4ArZZQcH83c=
+  dependencies:
+    is-callable "^1.1.4"
+    is-date-object "^1.0.1"
+    is-symbol "^1.0.2"
+
+es-to-primitive@^1.2.1:
+  version "1.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
+  integrity sha1-5VzUyc3BiLzvsDs2bHNjI/xciYo=
+  dependencies:
+    is-callable "^1.1.4"
+    is-date-object "^1.0.1"
+    is-symbol "^1.0.2"
+
+escape-string-regexp@^1.0.5:
+  version "1.0.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+  integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+escodegen@^1.11.1:
+  version "1.14.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457"
+  integrity sha1-ugHQyCeLXpWppFNQFCAmZZAnpFc=
+  dependencies:
+    esprima "^4.0.1"
+    estraverse "^4.2.0"
+    esutils "^2.0.2"
+    optionator "^0.8.1"
+  optionalDependencies:
+    source-map "~0.6.1"
+
+eslint-config-sonarqube@0.6.1:
+  version "0.6.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/eslint-config-sonarqube/-/eslint-config-sonarqube-0.6.1.tgz#6779dfd643341e9688c4181800403bd26544d584"
+  integrity sha1-Z3nf1kM0HpaIxBgYAEA70mVE1YQ=
+
+eslint-import-resolver-node@^0.3.2:
+  version "0.3.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a"
+  integrity sha1-WPFfuDm40FdsqYBBNHaqskcttmo=
+  dependencies:
+    debug "^2.6.9"
+    resolve "^1.5.0"
+
+eslint-module-utils@^2.4.1:
+  version "2.5.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/eslint-module-utils/-/eslint-module-utils-2.5.2.tgz#7878f7504824e1b857dd2505b59a8e5eda26a708"
+  integrity sha1-eHj3UEgk4bhX3SUFtZqOXtompwg=
+  dependencies:
+    debug "^2.6.9"
+    pkg-dir "^2.0.0"
+
+eslint-plugin-import@2.20.1:
+  version "2.20.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/eslint-plugin-import/-/eslint-plugin-import-2.20.1.tgz#802423196dcb11d9ce8435a5fc02a6d3b46939b3"
+  integrity sha1-gCQjGW3LEdnOhDWl/AKm07RpObM=
+  dependencies:
+    array-includes "^3.0.3"
+    array.prototype.flat "^1.2.1"
+    contains-path "^0.1.0"
+    debug "^2.6.9"
+    doctrine "1.5.0"
+    eslint-import-resolver-node "^0.3.2"
+    eslint-module-utils "^2.4.1"
+    has "^1.0.3"
+    minimatch "^3.0.4"
+    object.values "^1.1.0"
+    read-pkg-up "^2.0.0"
+    resolve "^1.12.0"
+
+eslint-plugin-jest@23.8.2:
+  version "23.8.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/eslint-plugin-jest/-/eslint-plugin-jest-23.8.2.tgz#6f28b41c67ef635f803ebd9e168f6b73858eb8d4"
+  integrity sha1-byi0HGfvY1+APr2eFo9rc4WOuNQ=
+  dependencies:
+    "@typescript-eslint/experimental-utils" "^2.5.0"
+
+eslint-plugin-jsx-a11y@6.2.3:
+  version "6.2.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz#b872a09d5de51af70a97db1eea7dc933043708aa"
+  integrity sha1-uHKgnV3lGvcKl9se6n3JMwQ3CKo=
+  dependencies:
+    "@babel/runtime" "^7.4.5"
+    aria-query "^3.0.0"
+    array-includes "^3.0.3"
+    ast-types-flow "^0.0.7"
+    axobject-query "^2.0.2"
+    damerau-levenshtein "^1.0.4"
+    emoji-regex "^7.0.2"
+    has "^1.0.3"
+    jsx-ast-utils "^2.2.1"
+
+eslint-plugin-promise@4.2.1:
+  version "4.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a"
+  integrity sha1-hF/YsiYK2PglZMEiL85ErXHZQYo=
+
+eslint-plugin-react-hooks@2.5.1:
+  version "2.5.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.5.1.tgz#4ef5930592588ce171abeb26f400c7fbcbc23cd0"
+  integrity sha1-TvWTBZJYjOFxq+sm9ADH+8vCPNA=
+
+eslint-plugin-react@7.19.0:
+  version "7.19.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz#6d08f9673628aa69c5559d33489e855d83551666"
+  integrity sha1-bQj5ZzYoqmnFVZ0zSJ6FXYNVFmY=
+  dependencies:
+    array-includes "^3.1.1"
+    doctrine "^2.1.0"
+    has "^1.0.3"
+    jsx-ast-utils "^2.2.3"
+    object.entries "^1.1.1"
+    object.fromentries "^2.0.2"
+    object.values "^1.1.1"
+    prop-types "^15.7.2"
+    resolve "^1.15.1"
+    semver "^6.3.0"
+    string.prototype.matchall "^4.0.2"
+    xregexp "^4.3.0"
+
+eslint-plugin-sonarjs@0.5.0:
+  version "0.5.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.5.0.tgz#ce17b2daba65a874c2862213a9e38e8986ad7d7d"
+  integrity sha1-zhey2rplqHTChiITqeOOiYatfX0=
+
+eslint-scope@^5.0.0:
+  version "5.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9"
+  integrity sha1-6HyIh8c+jR7ITxylkWRcNYv8j7k=
+  dependencies:
+    esrecurse "^4.1.0"
+    estraverse "^4.1.1"
+
+eslint-utils@^1.4.3:
+  version "1.4.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f"
+  integrity sha1-dP7HxU0Hdrb2fgJRBAtYBlZOmB8=
+  dependencies:
+    eslint-visitor-keys "^1.1.0"
+
+eslint-utils@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/eslint-utils/-/eslint-utils-2.0.0.tgz#7be1cc70f27a72a76cd14aa698bcabed6890e1cd"
+  integrity sha1-e+HMcPJ6cqds0UqmmLyr7WiQ4c0=
+  dependencies:
+    eslint-visitor-keys "^1.1.0"
+
+eslint-visitor-keys@^1.1.0:
+  version "1.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2"
+  integrity sha1-4qgs6oT/JGrW+1f5veW0ZiFFnsI=
+
+eslint@6.8.0:
+  version "6.8.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb"
+  integrity sha1-YiYtZylzn5J1cjgkMC+yJ8jJP/s=
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    ajv "^6.10.0"
+    chalk "^2.1.0"
+    cross-spawn "^6.0.5"
+    debug "^4.0.1"
+    doctrine "^3.0.0"
+    eslint-scope "^5.0.0"
+    eslint-utils "^1.4.3"
+    eslint-visitor-keys "^1.1.0"
+    espree "^6.1.2"
+    esquery "^1.0.1"
+    esutils "^2.0.2"
+    file-entry-cache "^5.0.1"
+    functional-red-black-tree "^1.0.1"
+    glob-parent "^5.0.0"
+    globals "^12.1.0"
+    ignore "^4.0.6"
+    import-fresh "^3.0.0"
+    imurmurhash "^0.1.4"
+    inquirer "^7.0.0"
+    is-glob "^4.0.0"
+    js-yaml "^3.13.1"
+    json-stable-stringify-without-jsonify "^1.0.1"
+    levn "^0.3.0"
+    lodash "^4.17.14"
+    minimatch "^3.0.4"
+    mkdirp "^0.5.1"
+    natural-compare "^1.4.0"
+    optionator "^0.8.3"
+    progress "^2.0.0"
+    regexpp "^2.0.1"
+    semver "^6.1.2"
+    strip-ansi "^5.2.0"
+    strip-json-comments "^3.0.1"
+    table "^5.2.3"
+    text-table "^0.2.0"
+    v8-compile-cache "^2.0.3"
+
+espree@^6.1.2:
+  version "6.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a"
+  integrity sha1-d/xy4f10SiBSwg84pbV1gy6Cc0o=
+  dependencies:
+    acorn "^7.1.1"
+    acorn-jsx "^5.2.0"
+    eslint-visitor-keys "^1.1.0"
+
+esprima@^4.0.0, esprima@^4.0.1:
+  version "4.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+  integrity sha1-E7BM2z5sXRnfkatph6hpVhmwqnE=
+
+esquery@^1.0.1:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
+  integrity sha1-QGxRZYsfWZGl+bYrHcJbAOPlxwg=
+  dependencies:
+    estraverse "^4.0.0"
+
+esrecurse@^4.1.0:
+  version "4.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf"
+  integrity sha1-AHo7n9vCs7uH5IeeoZyS/b05Qs8=
+  dependencies:
+    estraverse "^4.1.0"
+
+estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
+  version "4.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+  integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=
+
+esutils@^2.0.2:
+  version "2.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+  integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=
+
+exec-sh@^0.3.2:
+  version "0.3.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b"
+  integrity sha1-ZzjeLrfI5nHQNmrqCw24xvfXORs=
+
+execa@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
+  integrity sha1-xiNqW7TfbW8V6I5/AXeYIWdJ3dg=
+  dependencies:
+    cross-spawn "^6.0.0"
+    get-stream "^4.0.0"
+    is-stream "^1.1.0"
+    npm-run-path "^2.0.0"
+    p-finally "^1.0.0"
+    signal-exit "^3.0.0"
+    strip-eof "^1.0.0"
+
+execa@^3.2.0:
+  version "3.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/execa/-/execa-3.4.0.tgz#c08ed4550ef65d858fac269ffc8572446f37eb89"
+  integrity sha1-wI7UVQ72XYWPrCaf/IVyRG8364k=
+  dependencies:
+    cross-spawn "^7.0.0"
+    get-stream "^5.0.0"
+    human-signals "^1.1.1"
+    is-stream "^2.0.0"
+    merge-stream "^2.0.0"
+    npm-run-path "^4.0.0"
+    onetime "^5.1.0"
+    p-finally "^2.0.0"
+    signal-exit "^3.0.2"
+    strip-final-newline "^2.0.0"
+
+exenv@^1.2.0:
+  version "1.2.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
+  integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=
+
+exit@^0.1.2:
+  version "0.1.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
+  integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=
+
+expand-brackets@^2.1.4:
+  version "2.1.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+  integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI=
+  dependencies:
+    debug "^2.3.3"
+    define-property "^0.2.5"
+    extend-shallow "^2.0.1"
+    posix-character-classes "^0.1.0"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.1"
+
+expect@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/expect/-/expect-25.4.0.tgz#0b16c17401906d1679d173e59f0d4580b22f8dc8"
+  integrity sha1-CxbBdAGQbRZ50XPlnw1FgLIvjcg=
+  dependencies:
+    "@jest/types" "^25.4.0"
+    ansi-styles "^4.0.0"
+    jest-get-type "^25.2.6"
+    jest-matcher-utils "^25.4.0"
+    jest-message-util "^25.4.0"
+    jest-regex-util "^25.2.6"
+
+extend-shallow@^2.0.1:
+  version "2.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+  integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=
+  dependencies:
+    is-extendable "^0.1.0"
+
+extend-shallow@^3.0.0, extend-shallow@^3.0.2:
+  version "3.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
+  integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=
+  dependencies:
+    assign-symbols "^1.0.0"
+    is-extendable "^1.0.1"
+
+extend@~3.0.2:
+  version "3.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+  integrity sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=
+
+external-editor@^3.0.3:
+  version "3.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
+  integrity sha1-ywP3QL764D6k0oPK7SdBqD8zVJU=
+  dependencies:
+    chardet "^0.7.0"
+    iconv-lite "^0.4.24"
+    tmp "^0.0.33"
+
+extglob@^2.0.4:
+  version "2.0.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+  integrity sha1-rQD+TcYSqSMuhxhxHcXLWrAoVUM=
+  dependencies:
+    array-unique "^0.3.2"
+    define-property "^1.0.0"
+    expand-brackets "^2.1.4"
+    extend-shallow "^2.0.1"
+    fragment-cache "^0.2.1"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.1"
+
+extsprintf@1.3.0:
+  version "1.3.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+  integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
+
+extsprintf@^1.2.0:
+  version "1.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+  integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
+
+fast-deep-equal@^2.0.1:
+  version "2.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
+  integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
+
+fast-deep-equal@^3.1.1:
+  version "3.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
+  integrity sha1-VFFFB3xQFJHjOxXsQIwpQ3bpSuQ=
+
+fast-glob@^2.2.6:
+  version "2.2.7"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
+  integrity sha1-aVOFfDr6R1//ku5gFdUtpwpM050=
+  dependencies:
+    "@mrmlnc/readdir-enhanced" "^2.2.1"
+    "@nodelib/fs.stat" "^1.1.2"
+    glob-parent "^3.1.0"
+    is-glob "^4.0.0"
+    merge2 "^1.2.3"
+    micromatch "^3.1.10"
+
+fast-glob@^3.0.3:
+  version "3.0.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/fast-glob/-/fast-glob-3.0.4.tgz#d484a41005cb6faeb399b951fd1bd70ddaebb602"
+  integrity sha1-1ISkEAXLb66zmblR/RvXDdrrtgI=
+  dependencies:
+    "@nodelib/fs.stat" "^2.0.1"
+    "@nodelib/fs.walk" "^1.2.1"
+    glob-parent "^5.0.0"
+    is-glob "^4.0.1"
+    merge2 "^1.2.3"
+    micromatch "^4.0.2"
+
+fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
+  integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I=
+
+fast-levenshtein@~2.0.4, fast-levenshtein@~2.0.6:
+  version "2.0.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+  integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
+
+fastq@^1.6.0:
+  version "1.6.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/fastq/-/fastq-1.6.0.tgz#4ec8a38f4ac25f21492673adb7eae9cfef47d1c2"
+  integrity sha1-Tsijj0rCXyFJJnOtt+rpz+9H0cI=
+  dependencies:
+    reusify "^1.0.0"
+
+fb-watchman@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
+  integrity sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=
+  dependencies:
+    bser "^2.0.0"
+
+fbjs@^0.8.0, fbjs@^0.8.9:
+  version "0.8.17"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
+  integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=
+  dependencies:
+    core-js "^1.0.0"
+    isomorphic-fetch "^2.1.1"
+    loose-envify "^1.0.0"
+    object-assign "^4.1.0"
+    promise "^7.1.1"
+    setimmediate "^1.0.5"
+    ua-parser-js "^0.7.18"
+
+figures@^3.0.0:
+  version "3.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
+  integrity sha1-YlwYvSk8YE3EqN2y/r8MiDQXRq8=
+  dependencies:
+    escape-string-regexp "^1.0.5"
+
+file-entry-cache@^5.0.1:
+  version "5.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c"
+  integrity sha1-yg9u+m3T1WEzP7FFFQZcL6/fQ5w=
+  dependencies:
+    flat-cache "^2.0.1"
+
+fill-range@^4.0.0:
+  version "4.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
+  integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=
+  dependencies:
+    extend-shallow "^2.0.1"
+    is-number "^3.0.0"
+    repeat-string "^1.6.1"
+    to-regex-range "^2.1.0"
+
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha1-GRmmp8df44ssfHflGYU12prN2kA=
+  dependencies:
+    to-regex-range "^5.0.1"
+
+find-root@^1.1.0:
+  version "1.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
+  integrity sha1-q8/Iunb3CMQql7PWhbfpRQv7nOQ=
+
+find-up@^2.0.0, find-up@^2.1.0:
+  version "2.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+  integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c=
+  dependencies:
+    locate-path "^2.0.0"
+
+find-up@^4.0.0, find-up@^4.1.0:
+  version "4.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+  integrity sha1-l6/n1s3AvFkoWEt8jXsW6KmqXRk=
+  dependencies:
+    locate-path "^5.0.0"
+    path-exists "^4.0.0"
+
+flat-cache@^2.0.1:
+  version "2.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
+  integrity sha1-XSltbwS9pEpGMKMBQTvbwuwIXsA=
+  dependencies:
+    flatted "^2.0.0"
+    rimraf "2.6.3"
+    write "1.0.3"
+
+flatted@^2.0.0:
+  version "2.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08"
+  integrity sha1-aeV8qo8OrLwoHS4stFjUb9tEngg=
+
+for-in@^1.0.2:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+  integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
+
+forever-agent@~0.6.1:
+  version "0.6.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+  integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
+
+form-data@~2.3.2:
+  version "2.3.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+  integrity sha1-3M5SwF9kTymManq5Nr1yTO/786Y=
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.6"
+    mime-types "^2.1.12"
+
+formik@1.2.0:
+  version "1.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/formik/-/formik-1.2.0.tgz#a0daf8512ce2ec18d88ff59a5bb172b0167e85d1"
+  integrity sha1-oNr4USzi7BjYj/WaW7FysBZ+hdE=
+  dependencies:
+    create-react-context "^0.2.2"
+    deepmerge "^2.1.1"
+    hoist-non-react-statics "^2.5.5"
+    lodash.clonedeep "^4.5.0"
+    lodash.topath "4.5.2"
+    prop-types "^15.6.1"
+    react-fast-compare "^1.0.0"
+    tslib "^1.9.3"
+    warning "^3.0.0"
+
+fragment-cache@^0.2.1:
+  version "0.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
+  integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=
+  dependencies:
+    map-cache "^0.2.2"
+
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+  integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+fsevents@^2.1.2:
+  version "2.1.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805"
+  integrity sha1-TAofs0vGjlQ7S4Kp7Dkr+9qECAU=
+
+function-bind@^1.1.1:
+  version "1.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+  integrity sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=
+
+function.prototype.name@^1.1.1, function.prototype.name@^1.1.2:
+  version "1.1.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/function.prototype.name/-/function.prototype.name-1.1.2.tgz#5cdf79d7c05db401591dfde83e3b70c5123e9a45"
+  integrity sha1-XN9518BdtAFZHf3oPjtwxRI+mkU=
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+    functions-have-names "^1.2.0"
+
+functional-red-black-tree@^1.0.1:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
+  integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
+
+functions-have-names@^1.2.0:
+  version "1.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/functions-have-names/-/functions-have-names-1.2.1.tgz#a981ac397fa0c9964551402cdc5533d7a4d52f91"
+  integrity sha1-qYGsOX+gyZZFUUAs3FUz16TVL5E=
+
+gensync@^1.0.0-beta.1:
+  version "1.0.0-beta.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
+  integrity sha1-WPQ2H/mH5f9uHnohCCeqNx6qwmk=
+
+get-caller-file@^2.0.1:
+  version "2.0.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+  integrity sha1-T5RBKoLbMvNuOwuXQfipf+sDH34=
+
+get-stream@^4.0.0:
+  version "4.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
+  integrity sha1-wbJVV189wh1Zv8ec09K0axw6VLU=
+  dependencies:
+    pump "^3.0.0"
+
+get-stream@^5.0.0:
+  version "5.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9"
+  integrity sha1-ASA83JJZf5uQkGfD5lbMH008Tck=
+  dependencies:
+    pump "^3.0.0"
+
+get-value@^2.0.3, get-value@^2.0.6:
+  version "2.0.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
+  integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
+
+getpass@^0.1.1:
+  version "0.1.7"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+  integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
+  dependencies:
+    assert-plus "^1.0.0"
+
+glob-parent@^3.1.0:
+  version "3.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
+  integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=
+  dependencies:
+    is-glob "^3.1.0"
+    path-dirname "^1.0.0"
+
+glob-parent@^5.0.0:
+  version "5.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/glob-parent/-/glob-parent-5.0.0.tgz#1dc99f0f39b006d3e92c2c284068382f0c20e954"
+  integrity sha1-HcmfDzmwBtPpLCwoQGg4Lwwg6VQ=
+  dependencies:
+    is-glob "^4.0.1"
+
+glob-to-regexp@^0.3.0:
+  version "0.3.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
+  integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
+
+glob@^7.1.1, glob@^7.1.2, glob@^7.1.3:
+  version "7.1.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
+  integrity sha1-qmCKL2xXetNX4a5aXCbZqNGWklU=
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+glob@^7.1.4:
+  version "7.1.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/glob/-/glob-7.1.5.tgz#6714c69bee20f3c3e64c4dd905553e532b40cdc0"
+  integrity sha1-ZxTGm+4g88PmTE3ZBVU+UytAzcA=
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+glob@^7.1.6:
+  version "7.1.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
+  integrity sha1-FB8zuBp8JJLhJVlDB0gMRmeSeKY=
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+globals@^11.1.0:
+  version "11.12.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
+  integrity sha1-q4eVM4hooLq9hSV1gBjCp+uVxC4=
+
+globals@^12.1.0:
+  version "12.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8"
+  integrity sha1-oYgTV2pBsAokqX5/gVkYwuGZJfg=
+  dependencies:
+    type-fest "^0.8.1"
+
+globby@10.0.1:
+  version "10.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/globby/-/globby-10.0.1.tgz#4782c34cb75dd683351335c5829cc3420e606b22"
+  integrity sha1-R4LDTLdd1oM1EzXFgpzDQg5gayI=
+  dependencies:
+    "@types/glob" "^7.1.1"
+    array-union "^2.1.0"
+    dir-glob "^3.0.1"
+    fast-glob "^3.0.3"
+    glob "^7.1.3"
+    ignore "^5.1.1"
+    merge2 "^1.2.3"
+    slash "^3.0.0"
+
+globby@^9.2.0:
+  version "9.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d"
+  integrity sha1-/QKacGxwPSm90XD0tts6P3p8tj0=
+  dependencies:
+    "@types/glob" "^7.1.1"
+    array-union "^1.0.2"
+    dir-glob "^2.2.2"
+    fast-glob "^2.2.6"
+    glob "^7.1.3"
+    ignore "^4.0.3"
+    pify "^4.0.1"
+    slash "^2.0.0"
+
+good-listener@^1.2.2:
+  version "1.2.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
+  integrity sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=
+  dependencies:
+    delegate "^3.1.2"
+
+graceful-fs@^4.1.2:
+  version "4.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b"
+  integrity sha1-jY/cc5d8sEEEchy1NmbBymTNMos=
+
+graceful-fs@^4.2.3:
+  version "4.2.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
+  integrity sha1-ShL/G2A3bvCYYsIJPt2Qgyi+hCM=
+
+growly@^1.3.0:
+  version "1.3.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
+  integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=
+
+gud@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
+  integrity sha1-pIlYGxfmpwvsqavjrlfeekmYUsA=
+
+har-schema@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+  integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
+
+har-validator@~5.1.3:
+  version "5.1.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
+  integrity sha1-HvievT5JllV2de7ZiTEQ3DUPoIA=
+  dependencies:
+    ajv "^6.5.5"
+    har-schema "^2.0.0"
+
+has-flag@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+  integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=
+
+has-symbols@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
+  integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=
+
+has-symbols@^1.0.1:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
+  integrity sha1-n1IUdYpEGWxAbZvXbOv4HsLdMeg=
+
+has-value@^0.3.1:
+  version "0.3.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
+  integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=
+  dependencies:
+    get-value "^2.0.3"
+    has-values "^0.1.4"
+    isobject "^2.0.0"
+
+has-value@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
+  integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=
+  dependencies:
+    get-value "^2.0.6"
+    has-values "^1.0.0"
+    isobject "^3.0.0"
+
+has-values@^0.1.4:
+  version "0.1.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
+  integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E=
+
+has-values@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
+  integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=
+  dependencies:
+    is-number "^3.0.0"
+    kind-of "^4.0.0"
+
+has@^1.0.1, has@^1.0.3:
+  version "1.0.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+  integrity sha1-ci18v8H2qoJB8W3YFOAR4fQeh5Y=
+  dependencies:
+    function-bind "^1.1.1"
+
+history@3.3.0, history@^3.0.0:
+  version "3.3.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/history/-/history-3.3.0.tgz#fcedcce8f12975371545d735461033579a6dae9c"
+  integrity sha1-/O3M6PEpdTcVRdc1RhAzV5ptrpw=
+  dependencies:
+    invariant "^2.2.1"
+    loose-envify "^1.2.0"
+    query-string "^4.2.2"
+    warning "^3.0.0"
+
+hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.5:
+  version "2.5.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
+  integrity sha1-xZA89AnA39kI84jmGdhrnBF0y0c=
+
+hoist-non-react-statics@^3.3.0:
+  version "3.3.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b"
+  integrity sha1-sJF48BIhhPuVrPUl2q7LTY9FlYs=
+  dependencies:
+    react-is "^16.7.0"
+
+hosted-git-info@^2.1.4:
+  version "2.7.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
+  integrity sha1-l/I2l3vW4SVAiTD/bePuxigewEc=
+
+html-element-map@^1.2.0:
+  version "1.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/html-element-map/-/html-element-map-1.2.0.tgz#dfbb09efe882806af63d990cf6db37993f099f22"
+  integrity sha1-37sJ7+iCgGr2PZkM9ts3mT8JnyI=
+  dependencies:
+    array-filter "^1.0.0"
+
+html-encoding-sniffer@^1.0.2:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
+  integrity sha1-5w2EuU2lOqN14R/jo1G+ZkLKRvg=
+  dependencies:
+    whatwg-encoding "^1.0.1"
+
+html-escaper@^2.0.0:
+  version "2.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
+  integrity sha1-39YAJ9o2o238viNiYsAKWCJoFFM=
+
+htmlparser2@^3.9.1:
+  version "3.10.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
+  integrity sha1-vWedw/WYl7ajS7EHSchVu1OpOS8=
+  dependencies:
+    domelementtype "^1.3.1"
+    domhandler "^2.3.0"
+    domutils "^1.5.1"
+    entities "^1.1.1"
+    inherits "^2.0.1"
+    readable-stream "^3.1.1"
+
+http-signature@~1.2.0:
+  version "1.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+  integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
+  dependencies:
+    assert-plus "^1.0.0"
+    jsprim "^1.2.2"
+    sshpk "^1.7.0"
+
+human-signals@^1.1.1:
+  version "1.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
+  integrity sha1-xbHNFPUK6uCatsWf5jujOV/k36M=
+
+iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13:
+  version "0.4.24"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+  integrity sha1-ICK0sl+93CHS9SSXSkdKr+czkIs=
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3"
+
+ignore@^4.0.3, ignore@^4.0.6:
+  version "4.0.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
+  integrity sha1-dQ49tYYgh7RzfrrIIH/9HvJ7Jfw=
+
+ignore@^5.1.1:
+  version "5.1.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/ignore/-/ignore-5.1.2.tgz#e28e584d43ad7e92f96995019cc43b9e1ac49558"
+  integrity sha1-4o5YTUOtfpL5aZUBnMQ7nhrElVg=
+
+import-fresh@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
+  integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY=
+  dependencies:
+    caller-path "^2.0.0"
+    resolve-from "^3.0.0"
+
+import-fresh@^3.0.0:
+  version "3.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/import-fresh/-/import-fresh-3.1.0.tgz#6d33fa1dcef6df930fae003446f33415af905118"
+  integrity sha1-bTP6Hc7235MPrgA0RvM0Fa+QURg=
+  dependencies:
+    parent-module "^1.0.0"
+    resolve-from "^4.0.0"
+
+import-local@^3.0.2:
+  version "3.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6"
+  integrity sha1-qM/QQx0d5KIZlwPQA+PmI2T6bbY=
+  dependencies:
+    pkg-dir "^4.2.0"
+    resolve-cwd "^3.0.0"
+
+imurmurhash@^0.1.4:
+  version "0.1.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+  integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
+
+indent-string@^3.0.0:
+  version "3.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289"
+  integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=
+
+inflight@^1.0.4:
+  version "1.0.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+  dependencies:
+    once "^1.3.0"
+    wrappy "1"
+
+inherits@2, inherits@^2.0.1, inherits@^2.0.3:
+  version "2.0.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=
+
+inquirer@^7.0.0:
+  version "7.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/inquirer/-/inquirer-7.1.0.tgz#1298a01859883e17c7264b82870ae1034f92dd29"
+  integrity sha1-EpigGFmIPhfHJkuChwrhA0+S3Sk=
+  dependencies:
+    ansi-escapes "^4.2.1"
+    chalk "^3.0.0"
+    cli-cursor "^3.1.0"
+    cli-width "^2.0.0"
+    external-editor "^3.0.3"
+    figures "^3.0.0"
+    lodash "^4.17.15"
+    mute-stream "0.0.8"
+    run-async "^2.4.0"
+    rxjs "^6.5.3"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+    through "^2.3.6"
+
+internal-slot@^1.0.2:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/internal-slot/-/internal-slot-1.0.2.tgz#9c2e9fb3cd8e5e4256c6f45fe310067fcfa378a3"
+  integrity sha1-nC6fs82OXkJWxvRf4xAGf8+jeKM=
+  dependencies:
+    es-abstract "^1.17.0-next.1"
+    has "^1.0.3"
+    side-channel "^1.0.2"
+
+intl-format-cache@^2.0.5:
+  version "2.2.9"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/intl-format-cache/-/intl-format-cache-2.2.9.tgz#fb560de20c549cda20b569cf1ffb6dc62b5b93b4"
+  integrity sha1-+1YN4gxUnNogtWnPH/ttxitbk7Q=
+
+intl-messageformat-parser@1.4.0:
+  version "1.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/intl-messageformat-parser/-/intl-messageformat-parser-1.4.0.tgz#b43d45a97468cadbe44331d74bb1e8dea44fc075"
+  integrity sha1-tD1FqXRoytvkQzHXS7Ho3qRPwHU=
+
+intl-messageformat@^2.0.0, intl-messageformat@^2.1.0:
+  version "2.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/intl-messageformat/-/intl-messageformat-2.2.0.tgz#345bcd46de630b7683330c2e52177ff5eab484fc"
+  integrity sha1-NFvNRt5jC3aDMwwuUhd/9eq0hPw=
+  dependencies:
+    intl-messageformat-parser "1.4.0"
+
+intl-relativeformat@^2.1.0:
+  version "2.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/intl-relativeformat/-/intl-relativeformat-2.2.0.tgz#6aca95d019ec8d30b6c5653b6629f9983ea5b6c5"
+  integrity sha1-asqV0BnsjTC2xWU7Zin5mD6ltsU=
+  dependencies:
+    intl-messageformat "^2.0.0"
+
+invariant@^2.1.1, invariant@^2.2.1:
+  version "2.2.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
+  integrity sha1-YQ88ksk1nOHbYW5TgAjSP/NRWOY=
+  dependencies:
+    loose-envify "^1.0.0"
+
+ip-regex@^2.1.0:
+  version "2.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
+  integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=
+
+is-accessor-descriptor@^0.1.6:
+  version "0.1.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
+  integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=
+  dependencies:
+    kind-of "^3.0.2"
+
+is-accessor-descriptor@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
+  integrity sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=
+  dependencies:
+    kind-of "^6.0.0"
+
+is-arrayish@^0.2.1:
+  version "0.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+  integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
+
+is-boolean-object@^1.0.1:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e"
+  integrity sha1-EO3AkA3RJ2l6kvb5gHx2F9aKxI4=
+
+is-buffer@^1.1.5:
+  version "1.1.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+  integrity sha1-76ouqdqg16suoTqXsritUf776L4=
+
+is-callable@^1.1.4:
+  version "1.1.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
+  integrity sha1-HhrfIZ4e62hNaR+dagX/DTCiTXU=
+
+is-callable@^1.1.5:
+  version "1.1.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
+  integrity sha1-9+RrWWiQRW23Tn9ul2yzJz0G+qs=
+
+is-ci@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
+  integrity sha1-a8YzQYGBDgS1wis9WJ/cpVAmQEw=
+  dependencies:
+    ci-info "^2.0.0"
+
+is-data-descriptor@^0.1.4:
+  version "0.1.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
+  integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=
+  dependencies:
+    kind-of "^3.0.2"
+
+is-data-descriptor@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
+  integrity sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=
+  dependencies:
+    kind-of "^6.0.0"
+
+is-date-object@^1.0.1:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
+  integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=
+
+is-descriptor@^0.1.0:
+  version "0.1.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+  integrity sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=
+  dependencies:
+    is-accessor-descriptor "^0.1.6"
+    is-data-descriptor "^0.1.4"
+    kind-of "^5.0.0"
+
+is-descriptor@^1.0.0, is-descriptor@^1.0.2:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
+  integrity sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=
+  dependencies:
+    is-accessor-descriptor "^1.0.0"
+    is-data-descriptor "^1.0.0"
+    kind-of "^6.0.2"
+
+is-directory@^0.3.1:
+  version "0.3.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
+  integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=
+
+is-extendable@^0.1.0, is-extendable@^0.1.1:
+  version "0.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+  integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=
+
+is-extendable@^1.0.1:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+  integrity sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=
+  dependencies:
+    is-plain-object "^2.0.4"
+
+is-extglob@^2.1.0, is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+
+is-fullwidth-code-point@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+  integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
+
+is-fullwidth-code-point@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+  integrity sha1-8Rb4Bk/pCz94RKOJl8C3UFEmnx0=
+
+is-generator-fn@^2.0.0:
+  version "2.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118"
+  integrity sha1-fRQK3DiarzARqPKipM+m+q3/sRg=
+
+is-glob@^3.1.0:
+  version "3.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
+  integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=
+  dependencies:
+    is-extglob "^2.1.0"
+
+is-glob@^4.0.0, is-glob@^4.0.1:
+  version "4.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
+  integrity sha1-dWfb6fL14kZ7x3q4PEopSCQHpdw=
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-number-object@^1.0.4:
+  version "1.0.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197"
+  integrity sha1-NqyV50HPGLKD/B3fXoPaeY4+wZc=
+
+is-number@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+  integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=
+  dependencies:
+    kind-of "^3.0.2"
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha1-dTU0W4lnNNX4DE0GxQlVUnoU8Ss=
+
+is-plain-obj@^1.1.0:
+  version "1.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
+  integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
+
+is-plain-object@^2.0.3, is-plain-object@^2.0.4:
+  version "2.0.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+  integrity sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=
+  dependencies:
+    isobject "^3.0.1"
+
+is-promise@^2.1.0:
+  version "2.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
+  integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=
+
+is-regex@^1.0.4:
+  version "1.0.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
+  integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=
+  dependencies:
+    has "^1.0.1"
+
+is-regex@^1.0.5:
+  version "1.0.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae"
+  integrity sha1-OdWJo1i/GJZ/cmlnEguPwa7XTq4=
+  dependencies:
+    has "^1.0.3"
+
+is-stream@^1.0.1, is-stream@^1.1.0:
+  version "1.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+  integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
+
+is-stream@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
+  integrity sha1-venDJoDW+uBBKdasnZIc54FfeOM=
+
+is-string@^1.0.5:
+  version "1.0.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
+  integrity sha1-QEk+0ZjvP/R3uMf5L2ROyCpc06Y=
+
+is-subset@^0.1.1:
+  version "0.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6"
+  integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=
+
+is-symbol@^1.0.2:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38"
+  integrity sha1-oFX2rlcZLK7jKeeoYBGLSXqVDzg=
+  dependencies:
+    has-symbols "^1.0.0"
+
+is-typedarray@^1.0.0, is-typedarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+  integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+
+is-windows@^1.0.2:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+  integrity sha1-0YUOuXkezRjmGCzhKjDzlmNLsZ0=
+
+is-wsl@^2.1.1:
+  version "2.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/is-wsl/-/is-wsl-2.1.1.tgz#4a1c152d429df3d441669498e2486d3596ebaf1d"
+  integrity sha1-ShwVLUKd89RBZpSY4khtNZbrrx0=
+
+isarray@1.0.0, isarray@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+  integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+
+isexe@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+  integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+isobject@^2.0.0:
+  version "2.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+  integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=
+  dependencies:
+    isarray "1.0.0"
+
+isobject@^3.0.0, isobject@^3.0.1:
+  version "3.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+  integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+
+isomorphic-fetch@^2.1.1:
+  version "2.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
+  integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=
+  dependencies:
+    node-fetch "^1.0.1"
+    whatwg-fetch ">=0.10.0"
+
+isstream@~0.1.2:
+  version "0.1.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+  integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
+
+istanbul-lib-coverage@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec"
+  integrity sha1-9ZRKN8cLVQsCp4pcOyBVsoDOyOw=
+
+istanbul-lib-instrument@^4.0.0:
+  version "4.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz#61f13ac2c96cfefb076fe7131156cc05907874e6"
+  integrity sha1-YfE6wsls/vsHb+cTEVbMBZB4dOY=
+  dependencies:
+    "@babel/core" "^7.7.5"
+    "@babel/parser" "^7.7.5"
+    "@babel/template" "^7.7.4"
+    "@babel/traverse" "^7.7.4"
+    "@istanbuljs/schema" "^0.1.2"
+    istanbul-lib-coverage "^3.0.0"
+    semver "^6.3.0"
+
+istanbul-lib-report@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6"
+  integrity sha1-dRj+UupE3jcvRgp2tezan/tz2KY=
+  dependencies:
+    istanbul-lib-coverage "^3.0.0"
+    make-dir "^3.0.0"
+    supports-color "^7.1.0"
+
+istanbul-lib-source-maps@^4.0.0:
+  version "4.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz#75743ce6d96bb86dc7ee4352cf6366a23f0b1ad9"
+  integrity sha1-dXQ85tlruG3H7kNSz2Nmoj8LGtk=
+  dependencies:
+    debug "^4.1.1"
+    istanbul-lib-coverage "^3.0.0"
+    source-map "^0.6.1"
+
+istanbul-reports@^3.0.2:
+  version "3.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b"
+  integrity sha1-1ZMhDlAAaDdQywn8BkTktuJ/1Ts=
+  dependencies:
+    html-escaper "^2.0.0"
+    istanbul-lib-report "^3.0.0"
+
+jest-changed-files@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-changed-files/-/jest-changed-files-25.4.0.tgz#e573db32c2fd47d2b90357ea2eda0622c5c5cbd6"
+  integrity sha1-5XPbMsL9R9K5A1fqLtoGIsXFy9Y=
+  dependencies:
+    "@jest/types" "^25.4.0"
+    execa "^3.2.0"
+    throat "^5.0.0"
+
+jest-cli@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-cli/-/jest-cli-25.4.0.tgz#5dac8be0fece6ce39f0d671395a61d1357322bab"
+  integrity sha1-XayL4P7ObOOfDWcTlaYdE1cyK6s=
+  dependencies:
+    "@jest/core" "^25.4.0"
+    "@jest/test-result" "^25.4.0"
+    "@jest/types" "^25.4.0"
+    chalk "^3.0.0"
+    exit "^0.1.2"
+    import-local "^3.0.2"
+    is-ci "^2.0.0"
+    jest-config "^25.4.0"
+    jest-util "^25.4.0"
+    jest-validate "^25.4.0"
+    prompts "^2.0.1"
+    realpath-native "^2.0.0"
+    yargs "^15.3.1"
+
+jest-config@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-config/-/jest-config-25.4.0.tgz#56e5df3679a96ff132114b44fb147389c8c0a774"
+  integrity sha1-VuXfNnmpb/EyEUtE+xRzicjAp3Q=
+  dependencies:
+    "@babel/core" "^7.1.0"
+    "@jest/test-sequencer" "^25.4.0"
+    "@jest/types" "^25.4.0"
+    babel-jest "^25.4.0"
+    chalk "^3.0.0"
+    deepmerge "^4.2.2"
+    glob "^7.1.1"
+    jest-environment-jsdom "^25.4.0"
+    jest-environment-node "^25.4.0"
+    jest-get-type "^25.2.6"
+    jest-jasmine2 "^25.4.0"
+    jest-regex-util "^25.2.6"
+    jest-resolve "^25.4.0"
+    jest-util "^25.4.0"
+    jest-validate "^25.4.0"
+    micromatch "^4.0.2"
+    pretty-format "^25.4.0"
+    realpath-native "^2.0.0"
+
+jest-diff@^25.2.1, jest-diff@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-diff/-/jest-diff-25.4.0.tgz#260b70f19a46c283adcad7f081cae71eb784a634"
+  integrity sha1-Jgtw8ZpGwoOtytfwgcrnHreEpjQ=
+  dependencies:
+    chalk "^3.0.0"
+    diff-sequences "^25.2.6"
+    jest-get-type "^25.2.6"
+    pretty-format "^25.4.0"
+
+jest-docblock@^25.3.0:
+  version "25.3.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-docblock/-/jest-docblock-25.3.0.tgz#8b777a27e3477cd77a168c05290c471a575623ef"
+  integrity sha1-i3d6J+NHfNd6FowFKQxHGldWI+8=
+  dependencies:
+    detect-newline "^3.0.0"
+
+jest-each@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-each/-/jest-each-25.4.0.tgz#ad4e46164764e8e77058f169a0076a7f86f6b7d4"
+  integrity sha1-rU5GFkdk6OdwWPFpoAdqf4b2t9Q=
+  dependencies:
+    "@jest/types" "^25.4.0"
+    chalk "^3.0.0"
+    jest-get-type "^25.2.6"
+    jest-util "^25.4.0"
+    pretty-format "^25.4.0"
+
+jest-emotion@10.0.32:
+  version "10.0.32"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-emotion/-/jest-emotion-10.0.32.tgz#8e36a871911f78841701224a95b7c535c65b70b6"
+  integrity sha1-jjaocZEfeIQXASJKlbfFNcZbcLY=
+  dependencies:
+    "@babel/runtime" "^7.5.5"
+    "@types/jest" "^23.0.2"
+    chalk "^2.4.1"
+    css "^2.2.1"
+
+jest-environment-jsdom@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-environment-jsdom/-/jest-environment-jsdom-25.4.0.tgz#bbfc7f85bb6ade99089062a830c79cb454565cf0"
+  integrity sha1-u/x/hbtq3pkIkGKoMMectFRWXPA=
+  dependencies:
+    "@jest/environment" "^25.4.0"
+    "@jest/fake-timers" "^25.4.0"
+    "@jest/types" "^25.4.0"
+    jest-mock "^25.4.0"
+    jest-util "^25.4.0"
+    jsdom "^15.2.1"
+
+jest-environment-node@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-environment-node/-/jest-environment-node-25.4.0.tgz#188aef01ae6418e001c03fdd1c299961e1439082"
+  integrity sha1-GIrvAa5kGOABwD/dHCmZYeFDkII=
+  dependencies:
+    "@jest/environment" "^25.4.0"
+    "@jest/fake-timers" "^25.4.0"
+    "@jest/types" "^25.4.0"
+    jest-mock "^25.4.0"
+    jest-util "^25.4.0"
+    semver "^6.3.0"
+
+jest-get-type@^25.2.6:
+  version "25.2.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-get-type/-/jest-get-type-25.2.6.tgz#0b0a32fab8908b44d508be81681487dbabb8d877"
+  integrity sha1-Cwoy+riQi0TVCL6BaBSH26u42Hc=
+
+jest-haste-map@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-haste-map/-/jest-haste-map-25.4.0.tgz#da7c309dd7071e0a80c953ba10a0ec397efb1ae2"
+  integrity sha1-2nwwndcHHgqAyVO6EKDsOX77GuI=
+  dependencies:
+    "@jest/types" "^25.4.0"
+    anymatch "^3.0.3"
+    fb-watchman "^2.0.0"
+    graceful-fs "^4.2.3"
+    jest-serializer "^25.2.6"
+    jest-util "^25.4.0"
+    jest-worker "^25.4.0"
+    micromatch "^4.0.2"
+    sane "^4.0.3"
+    walker "^1.0.7"
+    which "^2.0.2"
+  optionalDependencies:
+    fsevents "^2.1.2"
+
+jest-jasmine2@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-jasmine2/-/jest-jasmine2-25.4.0.tgz#3d3d19514022e2326e836c2b66d68b4cb63c5861"
+  integrity sha1-PT0ZUUAi4jJug2wrZtaLTLY8WGE=
+  dependencies:
+    "@babel/traverse" "^7.1.0"
+    "@jest/environment" "^25.4.0"
+    "@jest/source-map" "^25.2.6"
+    "@jest/test-result" "^25.4.0"
+    "@jest/types" "^25.4.0"
+    chalk "^3.0.0"
+    co "^4.6.0"
+    expect "^25.4.0"
+    is-generator-fn "^2.0.0"
+    jest-each "^25.4.0"
+    jest-matcher-utils "^25.4.0"
+    jest-message-util "^25.4.0"
+    jest-runtime "^25.4.0"
+    jest-snapshot "^25.4.0"
+    jest-util "^25.4.0"
+    pretty-format "^25.4.0"
+    throat "^5.0.0"
+
+jest-leak-detector@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-leak-detector/-/jest-leak-detector-25.4.0.tgz#cf94a160c78e53d810e7b2f40b5fd7ee263375b3"
+  integrity sha1-z5ShYMeOU9gQ57L0C1/X7iYzdbM=
+  dependencies:
+    jest-get-type "^25.2.6"
+    pretty-format "^25.4.0"
+
+jest-matcher-utils@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-matcher-utils/-/jest-matcher-utils-25.4.0.tgz#dc3e7aec402a1e567ed80b572b9ad285878895e6"
+  integrity sha1-3D567EAqHlZ+2AtXK5rShYeIleY=
+  dependencies:
+    chalk "^3.0.0"
+    jest-diff "^25.4.0"
+    jest-get-type "^25.2.6"
+    pretty-format "^25.4.0"
+
+jest-message-util@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-message-util/-/jest-message-util-25.4.0.tgz#2899e8bc43f5317acf8dfdfe89ea237d354fcdab"
+  integrity sha1-KJnovEP1MXrPjf3+ieojfTVPzas=
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    "@jest/types" "^25.4.0"
+    "@types/stack-utils" "^1.0.1"
+    chalk "^3.0.0"
+    micromatch "^4.0.2"
+    slash "^3.0.0"
+    stack-utils "^1.0.1"
+
+jest-mock@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-mock/-/jest-mock-25.4.0.tgz#ded7d64b5328d81d78d2138c825d3a45e30ec8ca"
+  integrity sha1-3tfWS1Mo2B140hOMgl06ReMOyMo=
+  dependencies:
+    "@jest/types" "^25.4.0"
+
+jest-pnp-resolver@^1.2.1:
+  version "1.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a"
+  integrity sha1-7NrmBMB3p/vHDe+21RfDwciYkjo=
+
+jest-regex-util@^25.2.6:
+  version "25.2.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-regex-util/-/jest-regex-util-25.2.6.tgz#d847d38ba15d2118d3b06390056028d0f2fd3964"
+  integrity sha1-2EfTi6FdIRjTsGOQBWAo0PL9OWQ=
+
+jest-resolve-dependencies@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-resolve-dependencies/-/jest-resolve-dependencies-25.4.0.tgz#783937544cfc40afcc7c569aa54748c4b3f83f5a"
+  integrity sha1-eDk3VEz8QK/MfFaapUdIxLP4P1o=
+  dependencies:
+    "@jest/types" "^25.4.0"
+    jest-regex-util "^25.2.6"
+    jest-snapshot "^25.4.0"
+
+jest-resolve@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-resolve/-/jest-resolve-25.4.0.tgz#6f4540ce0d419c4c720e791e871da32ba4da7a60"
+  integrity sha1-b0VAzg1BnExyDnkehx2jK6TaemA=
+  dependencies:
+    "@jest/types" "^25.4.0"
+    browser-resolve "^1.11.3"
+    chalk "^3.0.0"
+    jest-pnp-resolver "^1.2.1"
+    read-pkg-up "^7.0.1"
+    realpath-native "^2.0.0"
+    resolve "^1.15.1"
+    slash "^3.0.0"
+
+jest-runner@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-runner/-/jest-runner-25.4.0.tgz#6ca4a3d52e692bbc081228fa68f750012f1f29e5"
+  integrity sha1-bKSj1S5pK7wIEij6aPdQAS8fKeU=
+  dependencies:
+    "@jest/console" "^25.4.0"
+    "@jest/environment" "^25.4.0"
+    "@jest/test-result" "^25.4.0"
+    "@jest/types" "^25.4.0"
+    chalk "^3.0.0"
+    exit "^0.1.2"
+    graceful-fs "^4.2.3"
+    jest-config "^25.4.0"
+    jest-docblock "^25.3.0"
+    jest-haste-map "^25.4.0"
+    jest-jasmine2 "^25.4.0"
+    jest-leak-detector "^25.4.0"
+    jest-message-util "^25.4.0"
+    jest-resolve "^25.4.0"
+    jest-runtime "^25.4.0"
+    jest-util "^25.4.0"
+    jest-worker "^25.4.0"
+    source-map-support "^0.5.6"
+    throat "^5.0.0"
+
+jest-runtime@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-runtime/-/jest-runtime-25.4.0.tgz#1e5227a9e2159d26ae27dcd426ca6bc041983439"
+  integrity sha1-HlInqeIVnSauJ9zUJsprwEGYNDk=
+  dependencies:
+    "@jest/console" "^25.4.0"
+    "@jest/environment" "^25.4.0"
+    "@jest/source-map" "^25.2.6"
+    "@jest/test-result" "^25.4.0"
+    "@jest/transform" "^25.4.0"
+    "@jest/types" "^25.4.0"
+    "@types/yargs" "^15.0.0"
+    chalk "^3.0.0"
+    collect-v8-coverage "^1.0.0"
+    exit "^0.1.2"
+    glob "^7.1.3"
+    graceful-fs "^4.2.3"
+    jest-config "^25.4.0"
+    jest-haste-map "^25.4.0"
+    jest-message-util "^25.4.0"
+    jest-mock "^25.4.0"
+    jest-regex-util "^25.2.6"
+    jest-resolve "^25.4.0"
+    jest-snapshot "^25.4.0"
+    jest-util "^25.4.0"
+    jest-validate "^25.4.0"
+    realpath-native "^2.0.0"
+    slash "^3.0.0"
+    strip-bom "^4.0.0"
+    yargs "^15.3.1"
+
+jest-serializer@^25.2.6:
+  version "25.2.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-serializer/-/jest-serializer-25.2.6.tgz#3bb4cc14fe0d8358489dbbefbb8a4e708ce039b7"
+  integrity sha1-O7TMFP4Ng1hInbvvu4pOcIzgObc=
+
+jest-snapshot@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-snapshot/-/jest-snapshot-25.4.0.tgz#e0b26375e2101413fd2ccb4278a5711b1922545c"
+  integrity sha1-4LJjdeIQFBP9LMtCeKVxGxkiVFw=
+  dependencies:
+    "@babel/types" "^7.0.0"
+    "@jest/types" "^25.4.0"
+    "@types/prettier" "^1.19.0"
+    chalk "^3.0.0"
+    expect "^25.4.0"
+    jest-diff "^25.4.0"
+    jest-get-type "^25.2.6"
+    jest-matcher-utils "^25.4.0"
+    jest-message-util "^25.4.0"
+    jest-resolve "^25.4.0"
+    make-dir "^3.0.0"
+    natural-compare "^1.4.0"
+    pretty-format "^25.4.0"
+    semver "^6.3.0"
+
+jest-util@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-util/-/jest-util-25.4.0.tgz#6a093d09d86d2b41ef583e5fe7dd3976346e1acd"
+  integrity sha1-agk9CdhtK0HvWD5f5905djRuGs0=
+  dependencies:
+    "@jest/types" "^25.4.0"
+    chalk "^3.0.0"
+    is-ci "^2.0.0"
+    make-dir "^3.0.0"
+
+jest-validate@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-validate/-/jest-validate-25.4.0.tgz#2e177a93b716a137110eaf2768f3d9095abd3f38"
+  integrity sha1-Lhd6k7cWoTcRDq8naPPZCVq9Pzg=
+  dependencies:
+    "@jest/types" "^25.4.0"
+    camelcase "^5.3.1"
+    chalk "^3.0.0"
+    jest-get-type "^25.2.6"
+    leven "^3.1.0"
+    pretty-format "^25.4.0"
+
+jest-watcher@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-watcher/-/jest-watcher-25.4.0.tgz#63ec0cd5c83bb9c9d1ac95be7558dd61c995ff05"
+  integrity sha1-Y+wM1cg7ucnRrJW+dVjdYcmV/wU=
+  dependencies:
+    "@jest/test-result" "^25.4.0"
+    "@jest/types" "^25.4.0"
+    ansi-escapes "^4.2.1"
+    chalk "^3.0.0"
+    jest-util "^25.4.0"
+    string-length "^3.1.0"
+
+jest-worker@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest-worker/-/jest-worker-25.4.0.tgz#ee0e2ceee5a36ecddf5172d6d7e0ab00df157384"
+  integrity sha1-7g4s7uWjbs3fUXLW1+CrAN8Vc4Q=
+  dependencies:
+    merge-stream "^2.0.0"
+    supports-color "^7.0.0"
+
+jest@25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jest/-/jest-25.4.0.tgz#fb96892c5c4e4a6b9bcb12068849cddf4c5f8cc7"
+  integrity sha1-+5aJLFxOSmubyxIGiEnN30xfjMc=
+  dependencies:
+    "@jest/core" "^25.4.0"
+    import-local "^3.0.2"
+    jest-cli "^25.4.0"
+
+"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
+  version "4.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+  integrity sha1-GSA/tZmR35jjoocFDUZHzerzJJk=
+
+js-yaml@^3.13.1:
+  version "3.13.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
+  integrity sha1-r/FRswv9+o5J4F2iLnQV6d+jeEc=
+  dependencies:
+    argparse "^1.0.7"
+    esprima "^4.0.0"
+
+jsbn@~0.1.0:
+  version "0.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+  integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
+
+jsdom@^15.2.1:
+  version "15.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jsdom/-/jsdom-15.2.1.tgz#d2feb1aef7183f86be521b8c6833ff5296d07ec5"
+  integrity sha1-0v6xrvcYP4a+UhuMaDP/UpbQfsU=
+  dependencies:
+    abab "^2.0.0"
+    acorn "^7.1.0"
+    acorn-globals "^4.3.2"
+    array-equal "^1.0.0"
+    cssom "^0.4.1"
+    cssstyle "^2.0.0"
+    data-urls "^1.1.0"
+    domexception "^1.0.1"
+    escodegen "^1.11.1"
+    html-encoding-sniffer "^1.0.2"
+    nwsapi "^2.2.0"
+    parse5 "5.1.0"
+    pn "^1.1.0"
+    request "^2.88.0"
+    request-promise-native "^1.0.7"
+    saxes "^3.1.9"
+    symbol-tree "^3.2.2"
+    tough-cookie "^3.0.1"
+    w3c-hr-time "^1.0.1"
+    w3c-xmlserializer "^1.1.2"
+    webidl-conversions "^4.0.2"
+    whatwg-encoding "^1.0.5"
+    whatwg-mimetype "^2.3.0"
+    whatwg-url "^7.0.0"
+    ws "^7.0.0"
+    xml-name-validator "^3.0.0"
+
+jsesc@^2.5.1:
+  version "2.5.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
+  integrity sha1-gFZNLkg9rPbo7yCWUKZ98/DCg6Q=
+
+json-parse-better-errors@^1.0.1:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+  integrity sha1-u4Z8+zRQ5pEHwTHRxRS6s9yLyqk=
+
+json-schema-traverse@^0.4.1:
+  version "0.4.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+  integrity sha1-afaofZUTq4u4/mO9sJecRI5oRmA=
+
+json-schema@0.2.3:
+  version "0.2.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+  integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
+
+json-stable-stringify-without-jsonify@^1.0.1:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
+  integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
+
+json-stringify-safe@~5.0.1:
+  version "5.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+  integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
+
+json5@2.x, json5@^2.1.0:
+  version "2.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850"
+  integrity sha1-56DGLEgoXGKNIKELhcibuAfDKFA=
+  dependencies:
+    minimist "^1.2.0"
+
+json5@^2.1.2:
+  version "2.1.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
+  integrity sha1-ybD3+pIzv+WAf+ZvzzpWF+1ZfUM=
+  dependencies:
+    minimist "^1.2.5"
+
+jsprim@^1.2.2:
+  version "1.4.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+  integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
+  dependencies:
+    assert-plus "1.0.0"
+    extsprintf "1.3.0"
+    json-schema "0.2.3"
+    verror "1.10.0"
+
+jsx-ast-utils@^2.2.1, jsx-ast-utils@^2.2.3:
+  version "2.2.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz#8a9364e402448a3ce7f14d357738310d9248054f"
+  integrity sha1-ipNk5AJEijzn8U01dzgxDZJIBU8=
+  dependencies:
+    array-includes "^3.0.3"
+    object.assign "^4.1.0"
+
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
+  version "3.2.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+  integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=
+  dependencies:
+    is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+  version "4.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+  integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc=
+  dependencies:
+    is-buffer "^1.1.5"
+
+kind-of@^5.0.0:
+  version "5.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+  integrity sha1-cpyR4thXt6QZofmqZWhcTDP1hF0=
+
+kind-of@^6.0.0, kind-of@^6.0.2:
+  version "6.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
+  integrity sha1-ARRrNqYhjmTljzqNZt5df8b20FE=
+
+kleur@^3.0.2:
+  version "3.0.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
+  integrity sha1-p5yezIbuHOP6YgbRIWxQHxR/wH4=
+
+leven@^3.1.0:
+  version "3.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
+  integrity sha1-d4kd6DQGTMy6gq54QrtrFKE+1/I=
+
+levn@^0.3.0, levn@~0.3.0:
+  version "0.3.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+  integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
+  dependencies:
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+
+lines-and-columns@^1.1.6:
+  version "1.1.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
+  integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
+
+load-json-file@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
+  integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=
+  dependencies:
+    graceful-fs "^4.1.2"
+    parse-json "^2.2.0"
+    pify "^2.0.0"
+    strip-bom "^3.0.0"
+
+load-json-file@^4.0.0:
+  version "4.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
+  integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs=
+  dependencies:
+    graceful-fs "^4.1.2"
+    parse-json "^4.0.0"
+    pify "^3.0.0"
+    strip-bom "^3.0.0"
+
+locate-path@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+  integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=
+  dependencies:
+    p-locate "^2.0.0"
+    path-exists "^3.0.0"
+
+locate-path@^5.0.0:
+  version "5.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+  integrity sha1-Gvujlq/WdqbUJQTQpno6frn2KqA=
+  dependencies:
+    p-locate "^4.1.0"
+
+lodash.clonedeep@^4.5.0:
+  version "4.5.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
+  integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
+
+lodash.escape@^4.0.1:
+  version "4.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
+  integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=
+
+lodash.flattendeep@^4.4.0:
+  version "4.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
+  integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=
+
+lodash.isequal@^4.5.0:
+  version "4.5.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
+  integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
+
+lodash.memoize@4.x:
+  version "4.1.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
+
+lodash.sortby@^4.7.0:
+  version "4.7.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
+  integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
+
+lodash.topath@4.5.2:
+  version "4.5.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/lodash.topath/-/lodash.topath-4.5.2.tgz#3616351f3bba61994a0931989660bd03254fd009"
+  integrity sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=
+
+lodash@4.17.21:
+  version "4.17.21"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+  integrity sha1-Z5WRxWTDv/quhFTPCz3zcMPWkRw=
+
+lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15:
+  version "4.17.15"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
+  integrity sha1-tEf2ZwoEVbv+7dETku/zMOoJdUg=
+
+lolex@^5.0.0:
+  version "5.1.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/lolex/-/lolex-5.1.2.tgz#953694d098ce7c07bc5ed6d0e42bc6c0c6d5a367"
+  integrity sha1-lTaU0JjOfAe8XtbQ5CvGwMbVo2c=
+  dependencies:
+    "@sinonjs/commons" "^1.7.0"
+
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
+  version "1.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+  integrity sha1-ce5R+nvkyuwaY4OffmgtgTLTDK8=
+  dependencies:
+    js-tokens "^3.0.0 || ^4.0.0"
+
+loud-rejection@^1.0.0:
+  version "1.6.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
+  integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=
+  dependencies:
+    currently-unhandled "^0.4.1"
+    signal-exit "^3.0.0"
+
+make-dir@^2.0.0:
+  version "2.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
+  integrity sha1-XwMQ4YuL6JjMBwCSlaMK5B6R5vU=
+  dependencies:
+    pify "^4.0.1"
+    semver "^5.6.0"
+
+make-dir@^3.0.0:
+  version "3.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/make-dir/-/make-dir-3.0.2.tgz#04a1acbf22221e1d6ef43559f43e05a90dbb4392"
+  integrity sha1-BKGsvyIiHh1u9DVZ9D4FqQ27Q5I=
+  dependencies:
+    semver "^6.0.0"
+
+make-error@1.x:
+  version "1.3.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8"
+  integrity sha1-7+ToH22yjK3WBccPKcgxtY73dsg=
+
+makeerror@1.0.x:
+  version "1.0.11"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
+  integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=
+  dependencies:
+    tmpl "1.0.x"
+
+map-cache@^0.2.2:
+  version "0.2.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+  integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
+
+map-obj@^1.0.0:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
+  integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
+
+map-obj@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9"
+  integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk=
+
+map-visit@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
+  integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=
+  dependencies:
+    object-visit "^1.0.0"
+
+meow@^5.0.0:
+  version "5.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/meow/-/meow-5.0.0.tgz#dfc73d63a9afc714a5e371760eb5c88b91078aa4"
+  integrity sha1-38c9Y6mvxxSl43F2DrXIi5EHiqQ=
+  dependencies:
+    camelcase-keys "^4.0.0"
+    decamelize-keys "^1.0.0"
+    loud-rejection "^1.0.0"
+    minimist-options "^3.0.1"
+    normalize-package-data "^2.3.4"
+    read-pkg-up "^3.0.0"
+    redent "^2.0.0"
+    trim-newlines "^2.0.0"
+    yargs-parser "^10.0.0"
+
+merge-stream@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+  integrity sha1-UoI2KaFN0AyXcPtq1H3GMQ8sH2A=
+
+merge2@^1.2.3:
+  version "1.2.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/merge2/-/merge2-1.2.3.tgz#7ee99dbd69bb6481689253f018488a1b902b0ed5"
+  integrity sha1-fumdvWm7ZIFoklPwGEiKG5ArDtU=
+
+micromatch@4.x, micromatch@^4.0.2:
+  version "4.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
+  integrity sha1-T8sJmb+fvC/L3SEvbWKbmlbDklk=
+  dependencies:
+    braces "^3.0.1"
+    picomatch "^2.0.5"
+
+micromatch@^3.1.10, micromatch@^3.1.4:
+  version "3.1.10"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+  integrity sha1-cIWbyVyYQJUvNZoGij/En57PrCM=
+  dependencies:
+    arr-diff "^4.0.0"
+    array-unique "^0.3.2"
+    braces "^2.3.1"
+    define-property "^2.0.2"
+    extend-shallow "^3.0.2"
+    extglob "^2.0.4"
+    fragment-cache "^0.2.1"
+    kind-of "^6.0.2"
+    nanomatch "^1.2.9"
+    object.pick "^1.3.0"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.2"
+
+mime-db@1.40.0:
+  version "1.40.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32"
+  integrity sha1-plBX6ZjbCQ9zKmj2wnbTh9QSbDI=
+
+mime-types@^2.1.12, mime-types@~2.1.19:
+  version "2.1.24"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81"
+  integrity sha1-tvjQs+lR77d97eyhlM/20W9nb4E=
+  dependencies:
+    mime-db "1.40.0"
+
+mimic-fn@^2.1.0:
+  version "2.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+  integrity sha1-ftLCzMyvhNP/y3pptXcR/CCDQBs=
+
+minimatch@^3.0.4:
+  version "3.0.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+  integrity sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=
+  dependencies:
+    brace-expansion "^1.1.7"
+
+minimist-options@^3.0.1:
+  version "3.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954"
+  integrity sha1-+6TIGRM54T7PTWG+sD8HAQPz2VQ=
+  dependencies:
+    arrify "^1.0.1"
+    is-plain-obj "^1.1.0"
+
+minimist@0.0.8:
+  version "0.0.8"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+  integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
+
+minimist@^1.1.1, minimist@^1.2.0:
+  version "1.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+  integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
+
+minimist@^1.2.5:
+  version "1.2.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
+  integrity sha1-Z9ZgFLZqaoqqDAg8X9WN9OTpdgI=
+
+mixin-deep@^1.2.0:
+  version "1.3.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
+  integrity sha1-ESC0PcNZp4Xc5ltVuC4lfM9HlWY=
+  dependencies:
+    for-in "^1.0.2"
+    is-extendable "^1.0.1"
+
+mkdirp@1.x:
+  version "1.0.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
+  integrity sha1-PrXtYmInVteaXw4qIh3+utdcL34=
+
+mkdirp@^0.5.1:
+  version "0.5.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+  integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
+  dependencies:
+    minimist "0.0.8"
+
+moo@^0.4.3:
+  version "0.4.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e"
+  integrity sha1-P4R6JvMc9iWpVqh/KxD7wBO/0Q4=
+
+ms@2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+  integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+ms@^2.1.1:
+  version "2.1.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+  integrity sha1-0J0fNXtEP0kzgqjrPM0YOHKuYAk=
+
+mute-stream@0.0.8:
+  version "0.0.8"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
+  integrity sha1-FjDEKyJR/4HiooPelqVJfqkuXg0=
+
+nanomatch@^1.2.9:
+  version "1.2.13"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
+  integrity sha1-uHqKpPwN6P5r6IiVs4mD/yZb0Rk=
+  dependencies:
+    arr-diff "^4.0.0"
+    array-unique "^0.3.2"
+    define-property "^2.0.2"
+    extend-shallow "^3.0.2"
+    fragment-cache "^0.2.1"
+    is-windows "^1.0.2"
+    kind-of "^6.0.2"
+    object.pick "^1.3.0"
+    regex-not "^1.0.0"
+    snapdragon "^0.8.1"
+    to-regex "^3.0.1"
+
+natural-compare@^1.4.0:
+  version "1.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+  integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
+
+nearley@^2.7.10:
+  version "2.16.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/nearley/-/nearley-2.16.0.tgz#77c297d041941d268290ec84b739d0ee297e83a7"
+  integrity sha1-d8KX0EGUHSaCkOyEtznQ7il+g6c=
+  dependencies:
+    commander "^2.19.0"
+    moo "^0.4.3"
+    railroad-diagrams "^1.0.0"
+    randexp "0.4.6"
+    semver "^5.4.1"
+
+nested-error-stacks@^2.0.0, nested-error-stacks@^2.1.0:
+  version "2.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61"
+  integrity sha1-D73PPhP+SZR4EoBST4uWsM3/nGE=
+
+nice-try@^1.0.4:
+  version "1.0.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+  integrity sha1-ozeKdpbOfSI+iPybdkvX7xCJ42Y=
+
+node-fetch@^1.0.1:
+  version "1.7.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
+  integrity sha1-mA9vcthSEaU0fGsrwYxbhMPrR+8=
+  dependencies:
+    encoding "^0.1.11"
+    is-stream "^1.0.1"
+
+node-int64@^0.4.0:
+  version "0.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
+  integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=
+
+node-modules-regexp@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40"
+  integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=
+
+node-notifier@^6.0.0:
+  version "6.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/node-notifier/-/node-notifier-6.0.0.tgz#cea319e06baa16deec8ce5cd7f133c4a46b68e12"
+  integrity sha1-zqMZ4GuqFt7sjOXNfxM8Ska2jhI=
+  dependencies:
+    growly "^1.3.0"
+    is-wsl "^2.1.1"
+    semver "^6.3.0"
+    shellwords "^0.1.1"
+    which "^1.3.1"
+
+normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0:
+  version "2.5.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
+  integrity sha1-5m2xg4sgDB38IzIl0SyzZSDiNKg=
+  dependencies:
+    hosted-git-info "^2.1.4"
+    resolve "^1.10.0"
+    semver "2 || 3 || 4 || 5"
+    validate-npm-package-license "^3.0.1"
+
+normalize-path@^2.1.1:
+  version "2.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+  integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=
+  dependencies:
+    remove-trailing-separator "^1.0.1"
+
+normalize-path@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+  integrity sha1-Dc1p/yOhybEf0JeDFmRKA4ghamU=
+
+npm-run-path@^2.0.0:
+  version "2.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+  integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
+  dependencies:
+    path-key "^2.0.0"
+
+npm-run-path@^4.0.0:
+  version "4.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
+  integrity sha1-t+zR5e1T2o43pV4cImnguX7XSOo=
+  dependencies:
+    path-key "^3.0.0"
+
+nth-check@~1.0.1:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
+  integrity sha1-sr0pXDfj3VijvwcAN2Zjuk2c8Fw=
+  dependencies:
+    boolbase "~1.0.0"
+
+nwsapi@^2.2.0:
+  version "2.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
+  integrity sha1-IEh5qePQaP8qVROcLHcngGgaOLc=
+
+oauth-sign@~0.9.0:
+  version "0.9.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
+  integrity sha1-R6ewFrqmi1+g7PPe4IqFxnmsZFU=
+
+object-assign@^4.1.0, object-assign@^4.1.1:
+  version "4.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+  integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+
+object-copy@^0.1.0:
+  version "0.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+  integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw=
+  dependencies:
+    copy-descriptor "^0.1.0"
+    define-property "^0.2.5"
+    kind-of "^3.0.3"
+
+object-inspect@^1.7.0:
+  version "1.7.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
+  integrity sha1-9Pa9GBrXfwBrXs5gvQtvOY/3Smc=
+
+object-is@^1.0.1:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6"
+  integrity sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=
+
+object-is@^1.0.2:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/object-is/-/object-is-1.0.2.tgz#6b80eb84fe451498f65007982f035a5b445edec4"
+  integrity sha1-a4DrhP5FFJj2UAeYLwNaW0Re3sQ=
+
+object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
+  version "1.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+  integrity sha1-HEfyct8nfzsdrwYWd9nILiMixg4=
+
+object-visit@^1.0.0:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+  integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=
+  dependencies:
+    isobject "^3.0.0"
+
+object.assign@^4.1.0:
+  version "4.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+  integrity sha1-lovxEA15Vrs8oIbwBvhGs7xACNo=
+  dependencies:
+    define-properties "^1.1.2"
+    function-bind "^1.1.1"
+    has-symbols "^1.0.0"
+    object-keys "^1.0.11"
+
+object.entries@^1.1.0:
+  version "1.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519"
+  integrity sha1-ICT8bWuiRq7ji9sP/Vz7zzcbdRk=
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.12.0"
+    function-bind "^1.1.1"
+    has "^1.0.3"
+
+object.entries@^1.1.1:
+  version "1.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/object.entries/-/object.entries-1.1.1.tgz#ee1cf04153de02bb093fec33683900f57ce5399b"
+  integrity sha1-7hzwQVPeArsJP+wzaDkA9XzlOZs=
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+    function-bind "^1.1.1"
+    has "^1.0.3"
+
+object.fromentries@^2.0.2:
+  version "2.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/object.fromentries/-/object.fromentries-2.0.2.tgz#4a09c9b9bb3843dd0f89acdb517a794d4f355ac9"
+  integrity sha1-SgnJubs4Q90PiazbUXp5TU81Wsk=
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+    function-bind "^1.1.1"
+    has "^1.0.3"
+
+object.pick@^1.3.0:
+  version "1.3.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
+  integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=
+  dependencies:
+    isobject "^3.0.1"
+
+object.values@^1.1.0:
+  version "1.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9"
+  integrity sha1-v2gQ712j5TJXkOqqK+IT6oRiTak=
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.12.0"
+    function-bind "^1.1.1"
+    has "^1.0.3"
+
+object.values@^1.1.1:
+  version "1.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e"
+  integrity sha1-aKmezeNWt+kpWjxeDOMdyMlT3l4=
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+    function-bind "^1.1.1"
+    has "^1.0.3"
+
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
+  version "1.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+  dependencies:
+    wrappy "1"
+
+onetime@^5.1.0:
+  version "5.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5"
+  integrity sha1-//DzyRYX/mK7UBiWNumayKbfe+U=
+  dependencies:
+    mimic-fn "^2.1.0"
+
+optionator@^0.8.1:
+  version "0.8.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
+  integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=
+  dependencies:
+    deep-is "~0.1.3"
+    fast-levenshtein "~2.0.4"
+    levn "~0.3.0"
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+    wordwrap "~1.0.0"
+
+optionator@^0.8.3:
+  version "0.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
+  integrity sha1-hPodA2/p08fiHZmIS2ARZ+yPtJU=
+  dependencies:
+    deep-is "~0.1.3"
+    fast-levenshtein "~2.0.6"
+    levn "~0.3.0"
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+    word-wrap "~1.2.3"
+
+os-tmpdir@~1.0.2:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+  integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
+
+p-each-series@^2.1.0:
+  version "2.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48"
+  integrity sha1-lhyN0/GV6pbHR+Y2smK4AKaxr0g=
+
+p-finally@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+  integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
+
+p-finally@^2.0.0:
+  version "2.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561"
+  integrity sha1-vW/KqcVZoJa2gIBvTWV7Pw8kBWE=
+
+p-limit@^1.1.0:
+  version "1.3.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
+  integrity sha1-uGvV8MJWkJEcdZD8v8IBDVSzzLg=
+  dependencies:
+    p-try "^1.0.0"
+
+p-limit@^2.2.0:
+  version "2.3.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+  integrity sha1-PdM8ZHohT9//2DWTPrCG2g3CHbE=
+  dependencies:
+    p-try "^2.0.0"
+
+p-locate@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+  integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=
+  dependencies:
+    p-limit "^1.1.0"
+
+p-locate@^4.1.0:
+  version "4.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+  integrity sha1-o0KLtwiLOmApL2aRkni3wpetTwc=
+  dependencies:
+    p-limit "^2.2.0"
+
+p-try@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
+  integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=
+
+p-try@^2.0.0:
+  version "2.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+  integrity sha1-yyhoVA4xPWHeWPr741zpAE1VQOY=
+
+parent-module@^1.0.0:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
+  integrity sha1-aR0nCeeMefrjoVZiJFLQB2LKqqI=
+  dependencies:
+    callsites "^3.0.0"
+
+parse-json@^2.2.0:
+  version "2.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+  integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=
+  dependencies:
+    error-ex "^1.2.0"
+
+parse-json@^4.0.0:
+  version "4.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
+  integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
+  dependencies:
+    error-ex "^1.3.1"
+    json-parse-better-errors "^1.0.1"
+
+parse-json@^5.0.0:
+  version "5.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/parse-json/-/parse-json-5.0.0.tgz#73e5114c986d143efa3712d4ea24db9a4266f60f"
+  integrity sha1-c+URTJhtFD76NxLU6iTbmkJm9g8=
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    error-ex "^1.3.1"
+    json-parse-better-errors "^1.0.1"
+    lines-and-columns "^1.1.6"
+
+parse5@5.1.0:
+  version "5.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"
+  integrity sha1-xZNByXI/QUxFKXVWTHwApo1YrNI=
+
+parse5@^3.0.1:
+  version "3.0.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
+  integrity sha1-BC95L/3TaFFVHPTp4Gazh0q0W1w=
+  dependencies:
+    "@types/node" "*"
+
+pascalcase@^0.1.1:
+  version "0.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+  integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
+
+path-dirname@^1.0.0:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
+  integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=
+
+path-exists@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+  integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+
+path-exists@^4.0.0:
+  version "4.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+  integrity sha1-UTvb4tO5XXdi6METfvoZXGxhtbM=
+
+path-is-absolute@^1.0.0:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+  integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+path-key@^2.0.0, path-key@^2.0.1:
+  version "2.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+  integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
+
+path-key@^3.0.0, path-key@^3.1.0:
+  version "3.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+  integrity sha1-WB9q3mWMu6ZaDTOA3ndTKVBU83U=
+
+path-parse@^1.0.6:
+  version "1.0.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
+  integrity sha1-1i27VnlAXXLEc37FhgDp3c8G0kw=
+
+path-type@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
+  integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=
+  dependencies:
+    pify "^2.0.0"
+
+path-type@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
+  integrity sha1-zvMdyOCho7sNEFwM2Xzzv0f0428=
+  dependencies:
+    pify "^3.0.0"
+
+path-type@^4.0.0:
+  version "4.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+  integrity sha1-hO0BwKe6OAr+CdkKjBgNzZ0DBDs=
+
+performance-now@^2.1.0:
+  version "2.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+  integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+
+picomatch@^2.0.4:
+  version "2.2.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
+  integrity sha1-IfMz6ba46v8CRo9RRupAbTRfTa0=
+
+picomatch@^2.0.5:
+  version "2.0.7"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6"
+  integrity sha1-UUFp2MfNC9vuzIomCeNKcWPeafY=
+
+pify@^2.0.0:
+  version "2.3.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+  integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
+
+pify@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+  integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
+
+pify@^4.0.1:
+  version "4.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
+  integrity sha1-SyzSXFDVmHNcUCkiJP2MbfQeMjE=
+
+pirates@^4.0.1:
+  version "4.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87"
+  integrity sha1-ZDqSyviUVm+RsrmG0sZpUKji+4c=
+  dependencies:
+    node-modules-regexp "^1.0.0"
+
+pkg-dir@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
+  integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=
+  dependencies:
+    find-up "^2.1.0"
+
+pkg-dir@^4.2.0:
+  version "4.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
+  integrity sha1-8JkTPfft5CLoHR2ESCcO6z5CYfM=
+  dependencies:
+    find-up "^4.0.0"
+
+pn@^1.1.0:
+  version "1.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
+  integrity sha1-4vTO8OIZ9GPBeas3Rj5OHs3Muvs=
+
+posix-character-classes@^0.1.0:
+  version "0.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
+  integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
+
+prelude-ls@~1.1.2:
+  version "1.1.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+  integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
+
+prettier@2.0.4:
+  version "2.0.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/prettier/-/prettier-2.0.4.tgz#2d1bae173e355996ee355ec9830a7a1ee05457ef"
+  integrity sha1-LRuuFz41WZbuNV7Jgwp6HuBUV+8=
+
+pretty-format@^25.2.1, pretty-format@^25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/pretty-format/-/pretty-format-25.4.0.tgz#c58801bb5c4926ff4a677fe43f9b8b99812c7830"
+  integrity sha1-xYgBu1xJJv9KZ3/kP5uLmYEseDA=
+  dependencies:
+    "@jest/types" "^25.4.0"
+    ansi-regex "^5.0.0"
+    ansi-styles "^4.0.0"
+    react-is "^16.12.0"
+
+progress@^2.0.0:
+  version "2.0.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
+  integrity sha1-foz42PW48jnBvGi+tOt4Vn1XLvg=
+
+promise@^7.1.1:
+  version "7.3.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+  integrity sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=
+  dependencies:
+    asap "~2.0.3"
+
+prompts@^2.0.1:
+  version "2.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/prompts/-/prompts-2.1.0.tgz#bf90bc71f6065d255ea2bdc0fe6520485c1b45db"
+  integrity sha1-v5C8cfYGXSVeor3A/mUgSFwbRds=
+  dependencies:
+    kleur "^3.0.2"
+    sisteransi "^1.0.0"
+
+prop-types-exact@^1.2.0:
+  version "1.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869"
+  integrity sha1-gl1r5GCUZjhII345JamMbpROmGk=
+  dependencies:
+    has "^1.0.3"
+    object.assign "^4.1.0"
+    reflect.ownkeys "^0.2.0"
+
+prop-types@15.7.2, prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
+  version "15.7.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
+  integrity sha1-UsQedbjIfnK52TYOAga5ncv/psU=
+  dependencies:
+    loose-envify "^1.4.0"
+    object-assign "^4.1.1"
+    react-is "^16.8.1"
+
+psl@^1.1.28:
+  version "1.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/psl/-/psl-1.2.0.tgz#df12b5b1b3a30f51c329eacbdef98f3a6e136dc6"
+  integrity sha1-3xK1sbOjD1HDKerL3vmPOm4TbcY=
+
+pump@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+  integrity sha1-tKIRaBW94vTh6mAjVOjHVWUQemQ=
+  dependencies:
+    end-of-stream "^1.1.0"
+    once "^1.3.1"
+
+punycode@^2.1.0, punycode@^2.1.1:
+  version "2.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+  integrity sha1-tYsBCsQMIsVldhbI0sLALHv0eew=
+
+qs@~6.5.2:
+  version "6.5.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+  integrity sha1-yzroBuh0BERYTvFUzo7pjUA/PjY=
+
+query-string@^4.2.2:
+  version "4.3.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
+  integrity sha1-u7aTucqRXCMlFbIosaArYJBD2+s=
+  dependencies:
+    object-assign "^4.1.0"
+    strict-uri-encode "^1.0.0"
+
+quick-lru@^1.0.0:
+  version "1.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
+  integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=
+
+raf@^3.4.1:
+  version "3.4.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
+  integrity sha1-B0LpmkplUvRF1z4+4DKK8P8e3jk=
+  dependencies:
+    performance-now "^2.1.0"
+
+railroad-diagrams@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
+  integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=
+
+randexp@0.4.6:
+  version "0.4.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
+  integrity sha1-6YatXl4x2uE93W97MBmqfIf2DKM=
+  dependencies:
+    discontinuous-range "1.0.0"
+    ret "~0.1.10"
+
+react-dom@16.8.6:
+  version "16.8.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f"
+  integrity sha1-cdYwP2MeiwCX9WFl72CPBR/24Q8=
+  dependencies:
+    loose-envify "^1.1.0"
+    object-assign "^4.1.1"
+    prop-types "^15.6.2"
+    scheduler "^0.13.6"
+
+react-draggable@4.2.0:
+  version "4.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/react-draggable/-/react-draggable-4.2.0.tgz#40cc5209082ca7d613104bf6daf31372cc0e1114"
+  integrity sha1-QMxSCQgsp9YTEEv22vMTcswOERQ=
+  dependencies:
+    classnames "^2.2.5"
+    prop-types "^15.6.0"
+
+react-fast-compare@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/react-fast-compare/-/react-fast-compare-1.0.0.tgz#813a039155e49b43ceffe99528fe5e9d97a6c938"
+  integrity sha1-gToDkVXkm0PO/+mVKP5enZemyTg=
+
+react-input-autosize@^2.1.2:
+  version "2.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/react-input-autosize/-/react-input-autosize-2.2.1.tgz#ec428fa15b1592994fb5f9aa15bb1eb6baf420f8"
+  integrity sha1-7EKPoVsVkplPtfmqFbsetrr0IPg=
+  dependencies:
+    prop-types "^15.5.8"
+
+react-intl@2.8.0:
+  version "2.8.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/react-intl/-/react-intl-2.8.0.tgz#20b0c1f01d1292427768aa8ec9e51ab7e36503ba"
+  integrity sha1-ILDB8B0SkkJ3aKqOyeUat+NlA7o=
+  dependencies:
+    hoist-non-react-statics "^2.5.5"
+    intl-format-cache "^2.0.5"
+    intl-messageformat "^2.1.0"
+    intl-relativeformat "^2.1.0"
+    invariant "^2.1.1"
+
+react-is@^16.12.0, react-is@^16.9.0:
+  version "16.13.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/react-is/-/react-is-16.13.0.tgz#0f37c3613c34fe6b37cd7f763a0d6293ab15c527"
+  integrity sha1-DzfDYTw0/ms3zX92Og1ik6sVxSc=
+
+react-is@^16.7.0:
+  version "16.9.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb"
+  integrity sha1-IcqVYTmarQ/xp3AcAWg+jKmB7cs=
+
+react-is@^16.8.1, react-is@^16.8.6:
+  version "16.8.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
+  integrity sha1-W7weLSkUHJ+9/tRWND/ivEMKahY=
+
+react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4:
+  version "3.0.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
+  integrity sha1-TxonOv38jzSIqMUWv9p4+HI1I2I=
+
+react-modal@3.8.2:
+  version "3.8.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/react-modal/-/react-modal-3.8.2.tgz#c47397a8602beb7aae0059a3b404f20416241d03"
+  integrity sha1-xHOXqGAr63quAFmjtATyBBYkHQM=
+  dependencies:
+    exenv "^1.2.0"
+    prop-types "^15.5.10"
+    react-lifecycles-compat "^3.0.0"
+    warning "^4.0.3"
+
+react-router@3.2.1:
+  version "3.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/react-router/-/react-router-3.2.1.tgz#b9a3279962bdfbe684c8bd0482b81ef288f0f244"
+  integrity sha1-uaMnmWK9++aEyL0Egrge8ojw8kQ=
+  dependencies:
+    create-react-class "^15.5.1"
+    history "^3.0.0"
+    hoist-non-react-statics "^2.3.1"
+    invariant "^2.2.1"
+    loose-envify "^1.2.0"
+    prop-types "^15.5.6"
+    warning "^3.0.0"
+
+react-select@1.2.1:
+  version "1.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/react-select/-/react-select-1.2.1.tgz#a2fe58a569eb14dcaa6543816260b97e538120d1"
+  integrity sha1-ov5YpWnrFNyqZUOBYmC5flOBINE=
+  dependencies:
+    classnames "^2.2.4"
+    prop-types "^15.5.8"
+    react-input-autosize "^2.1.2"
+
+react-test-renderer@16.8.6, react-test-renderer@^16.0.0-0:
+  version "16.8.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/react-test-renderer/-/react-test-renderer-16.8.6.tgz#188d8029b8c39c786f998aa3efd3ffe7642d5ba1"
+  integrity sha1-GI2AKbjDnHhvmYqj79P/52QtW6E=
+  dependencies:
+    object-assign "^4.1.1"
+    prop-types "^15.6.2"
+    react-is "^16.8.6"
+    scheduler "^0.13.6"
+
+react-virtualized@9.21.0:
+  version "9.21.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/react-virtualized/-/react-virtualized-9.21.0.tgz#8267c40ffb48db35b242a36dea85edcf280a6506"
+  integrity sha1-gmfED/tI2zWyQqNt6oXtzygKZQY=
+  dependencies:
+    babel-runtime "^6.26.0"
+    classnames "^2.2.3"
+    dom-helpers "^2.4.0 || ^3.0.0"
+    loose-envify "^1.3.0"
+    prop-types "^15.6.0"
+    react-lifecycles-compat "^3.0.4"
+
+react@16.8.6:
+  version "16.8.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"
+  integrity sha1-rWw6lhT9Ok6e9REX9U2IjaAfK74=
+  dependencies:
+    loose-envify "^1.1.0"
+    object-assign "^4.1.1"
+    prop-types "^15.6.2"
+    scheduler "^0.13.6"
+
+read-pkg-up@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
+  integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=
+  dependencies:
+    find-up "^2.0.0"
+    read-pkg "^2.0.0"
+
+read-pkg-up@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07"
+  integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=
+  dependencies:
+    find-up "^2.0.0"
+    read-pkg "^3.0.0"
+
+read-pkg-up@^7.0.1:
+  version "7.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
+  integrity sha1-86YTV1hFlzOuK5VjgFbhhU5+9Qc=
+  dependencies:
+    find-up "^4.1.0"
+    read-pkg "^5.2.0"
+    type-fest "^0.8.1"
+
+read-pkg@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
+  integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=
+  dependencies:
+    load-json-file "^2.0.0"
+    normalize-package-data "^2.3.2"
+    path-type "^2.0.0"
+
+read-pkg@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
+  integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=
+  dependencies:
+    load-json-file "^4.0.0"
+    normalize-package-data "^2.3.2"
+    path-type "^3.0.0"
+
+read-pkg@^5.2.0:
+  version "5.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
+  integrity sha1-e/KVQ4yloz5WzTDgU7NO5yUMk8w=
+  dependencies:
+    "@types/normalize-package-data" "^2.4.0"
+    normalize-package-data "^2.5.0"
+    parse-json "^5.0.0"
+    type-fest "^0.6.0"
+
+readable-stream@^3.1.1:
+  version "3.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc"
+  integrity sha1-pRwmdUZY4KPCHb9ZFjvUW6b0R/w=
+  dependencies:
+    inherits "^2.0.3"
+    string_decoder "^1.1.1"
+    util-deprecate "^1.0.1"
+
+realpath-native@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/realpath-native/-/realpath-native-2.0.0.tgz#7377ac429b6e1fd599dc38d08ed942d0d7beb866"
+  integrity sha1-c3esQptuH9WZ3DjQjtlC0Ne+uGY=
+
+redent@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa"
+  integrity sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=
+  dependencies:
+    indent-string "^3.0.0"
+    strip-indent "^2.0.0"
+
+reflect.ownkeys@^0.2.0:
+  version "0.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
+  integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=
+
+regenerator-runtime@^0.11.0:
+  version "0.11.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
+  integrity sha1-vgWtf5v30i4Fb5cmzuUBf78Z4uk=
+
+regenerator-runtime@^0.13.2:
+  version "0.13.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447"
+  integrity sha1-MuWcmm+5saSv8JtJMMotRHc0NEc=
+
+regenerator-runtime@^0.13.4:
+  version "0.13.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
+  integrity sha1-2Hih0JS0MG0QuQlkhLM+vVXiZpc=
+
+regex-not@^1.0.0, regex-not@^1.0.2:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
+  integrity sha1-H07OJ+ALC2XgJHpoEOaoXYOldSw=
+  dependencies:
+    extend-shallow "^3.0.2"
+    safe-regex "^1.1.0"
+
+regexp.prototype.flags@^1.3.0:
+  version "1.3.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
+  integrity sha1-erqJs8E6ZFCdq888qNn7ub31y3U=
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+
+regexpp@^2.0.1:
+  version "2.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
+  integrity sha1-jRnTHPYySCtYkEn4KB+T28uk0H8=
+
+remove-trailing-separator@^1.0.1:
+  version "1.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
+  integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
+
+repeat-element@^1.1.2:
+  version "1.1.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
+  integrity sha1-eC4NglwMWjuzlzH4Tv7mt0Lmsc4=
+
+repeat-string@^1.6.1:
+  version "1.6.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+  integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
+
+request-promise-core@1.1.3:
+  version "1.1.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9"
+  integrity sha1-6aPAgbUTgN/qZ3M2Bh/qh5qCnuk=
+  dependencies:
+    lodash "^4.17.15"
+
+request-promise-native@^1.0.7:
+  version "1.0.8"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36"
+  integrity sha1-pFW5YLgm5E4r+Jma9k3/K/5YyzY=
+  dependencies:
+    request-promise-core "1.1.3"
+    stealthy-require "^1.1.1"
+    tough-cookie "^2.3.3"
+
+request@^2.88.0:
+  version "2.88.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
+  integrity sha1-1zyRhzHLWofaBH4gcjQUb2ZNErM=
+  dependencies:
+    aws-sign2 "~0.7.0"
+    aws4 "^1.8.0"
+    caseless "~0.12.0"
+    combined-stream "~1.0.6"
+    extend "~3.0.2"
+    forever-agent "~0.6.1"
+    form-data "~2.3.2"
+    har-validator "~5.1.3"
+    http-signature "~1.2.0"
+    is-typedarray "~1.0.0"
+    isstream "~0.1.2"
+    json-stringify-safe "~5.0.1"
+    mime-types "~2.1.19"
+    oauth-sign "~0.9.0"
+    performance-now "^2.1.0"
+    qs "~6.5.2"
+    safe-buffer "^5.1.2"
+    tough-cookie "~2.5.0"
+    tunnel-agent "^0.6.0"
+    uuid "^3.3.2"
+
+require-directory@^2.1.1:
+  version "2.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+  integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+
+require-main-filename@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
+  integrity sha1-0LMp7MfMD2Fkn2IhW+aa9UqomJs=
+
+resolve-cwd@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
+  integrity sha1-DwB18bslRHZs9zumpuKt/ryxPy0=
+  dependencies:
+    resolve-from "^5.0.0"
+
+resolve-from@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
+  integrity sha1-six699nWiBvItuZTM17rywoYh0g=
+
+resolve-from@^4.0.0:
+  version "4.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
+  integrity sha1-SrzYUq0y3Xuqv+m0DgCjbbXzkuY=
+
+resolve-from@^5.0.0:
+  version "5.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
+  integrity sha1-w1IlhD3493bfIcV1V7wIfp39/Gk=
+
+resolve-url@^0.2.1:
+  version "0.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+  integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
+
+resolve@1.1.7:
+  version "1.1.7"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
+  integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
+
+resolve@1.x, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.5.0:
+  version "1.11.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/resolve/-/resolve-1.11.1.tgz#ea10d8110376982fef578df8fc30b9ac30a07a3e"
+  integrity sha1-6hDYEQN2mC/vV434/DC5rDCgej4=
+  dependencies:
+    path-parse "^1.0.6"
+
+resolve@^1.12.0:
+  version "1.15.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/resolve/-/resolve-1.15.0.tgz#1b7ca96073ebb52e741ffd799f6b39ea462c67f5"
+  integrity sha1-G3ypYHPrtS50H/15n2s56kYsZ/U=
+  dependencies:
+    path-parse "^1.0.6"
+
+resolve@^1.15.1:
+  version "1.16.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/resolve/-/resolve-1.16.1.tgz#49fac5d8bacf1fd53f200fa51247ae736175832c"
+  integrity sha1-SfrF2LrPH9U/IA+lEkeuc2F1gyw=
+  dependencies:
+    path-parse "^1.0.6"
+
+restore-cursor@^3.1.0:
+  version "3.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
+  integrity sha1-OfZ8VLOnpYzqUjbZXPADQjljH34=
+  dependencies:
+    onetime "^5.1.0"
+    signal-exit "^3.0.2"
+
+ret@~0.1.10:
+  version "0.1.15"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+  integrity sha1-uKSCXVvbH8P29Twrwz+BOIaBx7w=
+
+reusify@^1.0.0:
+  version "1.0.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+  integrity sha1-kNo4Kx4SbvwCFG6QhFqI2xKSXXY=
+
+rimraf@2.6.3:
+  version "2.6.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
+  integrity sha1-stEE/g2Psnz54KHNqCYt04M8bKs=
+  dependencies:
+    glob "^7.1.3"
+
+rimraf@^3.0.0:
+  version "3.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+  integrity sha1-8aVAK6YiCtUswSgrrBrjqkn9Bho=
+  dependencies:
+    glob "^7.1.3"
+
+rst-selector-parser@^2.2.3:
+  version "2.2.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
+  integrity sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=
+  dependencies:
+    lodash.flattendeep "^4.4.0"
+    nearley "^2.7.10"
+
+rsvp@^4.8.4:
+  version "4.8.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
+  integrity sha1-yPFVMR0Wf2jyHhaN9x7FsIMRNzQ=
+
+run-async@^2.4.0:
+  version "2.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/run-async/-/run-async-2.4.0.tgz#e59054a5b86876cfae07f431d18cbaddc594f1e8"
+  integrity sha1-5ZBUpbhods+uB/Qx0Yy63cWU8eg=
+  dependencies:
+    is-promise "^2.1.0"
+
+run-parallel@^1.1.9:
+  version "1.1.9"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679"
+  integrity sha1-yd06fPn0ssS2JE4XOm7YZuYd1nk=
+
+rxjs@^6.5.3:
+  version "6.5.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec"
+  integrity sha1-xciE4wlMjP7jG/J+uH5UzPyH+ew=
+  dependencies:
+    tslib "^1.9.0"
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.2:
+  version "5.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
+  integrity sha1-t02uxJsRSPiMZLaNSbHoFcHy9Rk=
+
+safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+  version "5.1.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+  integrity sha1-mR7GnSluAxN0fVm9/St0XDX4go0=
+
+safe-regex@^1.1.0:
+  version "1.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
+  integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4=
+  dependencies:
+    ret "~0.1.10"
+
+"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+  version "2.1.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+  integrity sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=
+
+sane@^4.0.3:
+  version "4.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded"
+  integrity sha1-7Ygf2SJzOmxGG8GJ3CtsAG8//e0=
+  dependencies:
+    "@cnakazawa/watch" "^1.0.3"
+    anymatch "^2.0.0"
+    capture-exit "^2.0.0"
+    exec-sh "^0.3.2"
+    execa "^1.0.0"
+    fb-watchman "^2.0.0"
+    micromatch "^3.1.4"
+    minimist "^1.1.1"
+    walker "~1.0.5"
+
+saxes@^3.1.9:
+  version "3.1.11"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/saxes/-/saxes-3.1.11.tgz#d59d1fd332ec92ad98a2e0b2ee644702384b1c5b"
+  integrity sha1-1Z0f0zLskq2YouCy7mRHAjhLHFs=
+  dependencies:
+    xmlchars "^2.1.1"
+
+scheduler@^0.13.6:
+  version "0.13.6"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889"
+  integrity sha1-RmpOwzJGezGpG5v3TlNHBy5M2Ik=
+  dependencies:
+    loose-envify "^1.1.0"
+    object-assign "^4.1.1"
+
+select@^1.1.2:
+  version "1.1.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
+  integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=
+
+"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.0:
+  version "5.7.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
+  integrity sha1-eQp89v6lRZuslhELKbYEEtyP+Ws=
+
+semver@6.x, semver@^6.1.2, semver@^6.3.0:
+  version "6.3.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+  integrity sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=
+
+semver@^5.7.1:
+  version "5.7.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+  integrity sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=
+
+semver@^6.0.0:
+  version "6.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/semver/-/semver-6.2.0.tgz#4d813d9590aaf8a9192693d6c85b9344de5901db"
+  integrity sha1-TYE9lZCq+KkZJpPWyFuTRN5ZAds=
+
+set-blocking@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+  integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
+
+set-value@^2.0.0, set-value@^2.0.1:
+  version "2.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
+  integrity sha1-oY1AUw5vB95CKMfe/kInr4ytAFs=
+  dependencies:
+    extend-shallow "^2.0.1"
+    is-extendable "^0.1.1"
+    is-plain-object "^2.0.3"
+    split-string "^3.0.1"
+
+setimmediate@^1.0.5:
+  version "1.0.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+  integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
+
+shebang-command@^1.2.0:
+  version "1.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+  integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
+  dependencies:
+    shebang-regex "^1.0.0"
+
+shebang-command@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+  integrity sha1-zNCvT4g1+9wmW4JGGq8MNmY/NOo=
+  dependencies:
+    shebang-regex "^3.0.0"
+
+shebang-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+  integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+
+shebang-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+  integrity sha1-rhbxZE2HPsrYQ7AwexQzYtTEIXI=
+
+shellwords@^0.1.1:
+  version "0.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
+  integrity sha1-1rkYHBpI05cyTISHHvvPxz/AZUs=
+
+side-channel@^1.0.2:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947"
+  integrity sha1-310auttOS/SvHNiFK/Ey0veHaUc=
+  dependencies:
+    es-abstract "^1.17.0-next.1"
+    object-inspect "^1.7.0"
+
+signal-exit@^3.0.0, signal-exit@^3.0.2:
+  version "3.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+  integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
+
+sisteransi@^1.0.0:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/sisteransi/-/sisteransi-1.0.2.tgz#ec57d64b6f25c4f26c0e2c7dd23f2d7f12f7e418"
+  integrity sha1-7FfWS28lxPJsDix90j8tfxL35Bg=
+
+slash@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
+  integrity sha1-3lUoUaF1nfOo8gZTVEL17E3eq0Q=
+
+slash@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
+  integrity sha1-ZTm+hwwWWtvVJAIg2+Nh8bxNRjQ=
+
+slice-ansi@^2.1.0:
+  version "2.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
+  integrity sha1-ys12k0YaY3pXiNkqfdT7oGjoFjY=
+  dependencies:
+    ansi-styles "^3.2.0"
+    astral-regex "^1.0.0"
+    is-fullwidth-code-point "^2.0.0"
+
+snapdragon-node@^2.0.1:
+  version "2.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
+  integrity sha1-bBdfhv8UvbByRWPo88GwIaKGhTs=
+  dependencies:
+    define-property "^1.0.0"
+    isobject "^3.0.0"
+    snapdragon-util "^3.0.1"
+
+snapdragon-util@^3.0.1:
+  version "3.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
+  integrity sha1-+VZHlIbyrNeXAGk/b3uAXkWrVuI=
+  dependencies:
+    kind-of "^3.2.0"
+
+snapdragon@^0.8.1:
+  version "0.8.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
+  integrity sha1-ZJIufFZbDhQgS6GqfWlkJ40lGC0=
+  dependencies:
+    base "^0.11.1"
+    debug "^2.2.0"
+    define-property "^0.2.5"
+    extend-shallow "^2.0.1"
+    map-cache "^0.2.2"
+    source-map "^0.5.6"
+    source-map-resolve "^0.5.0"
+    use "^3.1.0"
+
+source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
+  version "0.5.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259"
+  integrity sha1-cuLMNAlVQ+Q7LGKyxMENSpBU8lk=
+  dependencies:
+    atob "^2.1.1"
+    decode-uri-component "^0.2.0"
+    resolve-url "^0.2.1"
+    source-map-url "^0.4.0"
+    urix "^0.1.0"
+
+source-map-support@^0.5.6:
+  version "0.5.12"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599"
+  integrity sha1-tPOxDVGFelrwE4086AA7IBYT1Zk=
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
+source-map-url@^0.4.0:
+  version "0.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
+  integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=
+
+source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7:
+  version "0.5.7"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+  integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
+
+source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
+  version "0.6.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha1-dHIq8y6WFOnCh6jQu95IteLxomM=
+
+source-map@^0.7.3:
+  version "0.7.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
+  integrity sha1-UwL4FpAxc1ImVECS5kmB91F1A4M=
+
+spdx-correct@^3.0.0:
+  version "3.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4"
+  integrity sha1-+4PlBERSaPFUsHTiGMh8ADzTHfQ=
+  dependencies:
+    spdx-expression-parse "^3.0.0"
+    spdx-license-ids "^3.0.0"
+
+spdx-exceptions@^2.1.0:
+  version "2.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977"
+  integrity sha1-LqRQrudPKom/uUUZwH/Nb0EyKXc=
+
+spdx-expression-parse@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0"
+  integrity sha1-meEZt6XaAOBUkcn6M4t5BII7QdA=
+  dependencies:
+    spdx-exceptions "^2.1.0"
+    spdx-license-ids "^3.0.0"
+
+spdx-license-ids@^3.0.0:
+  version "3.0.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654"
+  integrity sha1-NpS1gEVnpFjTyARYQqY1hjL2JlQ=
+
+split-string@^3.0.1, split-string@^3.0.2:
+  version "3.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
+  integrity sha1-fLCd2jqGWFcFxks5pkZgOGguj+I=
+  dependencies:
+    extend-shallow "^3.0.0"
+
+sprintf-js@~1.0.2:
+  version "1.0.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+  integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+
+sshpk@^1.7.0:
+  version "1.16.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
+  integrity sha1-+2YcC+8ps520B2nuOfpwCT1vaHc=
+  dependencies:
+    asn1 "~0.2.3"
+    assert-plus "^1.0.0"
+    bcrypt-pbkdf "^1.0.0"
+    dashdash "^1.12.0"
+    ecc-jsbn "~0.1.1"
+    getpass "^0.1.1"
+    jsbn "~0.1.0"
+    safer-buffer "^2.0.2"
+    tweetnacl "~0.14.0"
+
+stack-utils@^1.0.1:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8"
+  integrity sha1-M+ujiXeIVYvr/C2wWdwVjsNs67g=
+
+static-extend@^0.1.1:
+  version "0.1.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+  integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=
+  dependencies:
+    define-property "^0.2.5"
+    object-copy "^0.1.0"
+
+stealthy-require@^1.1.1:
+  version "1.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
+  integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
+
+strict-uri-encode@^1.0.0:
+  version "1.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
+  integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
+
+string-length@^3.1.0:
+  version "3.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/string-length/-/string-length-3.1.0.tgz#107ef8c23456e187a8abd4a61162ff4ac6e25837"
+  integrity sha1-EH74wjRW4Yeoq9SmEWL/SsbiWDc=
+  dependencies:
+    astral-regex "^1.0.0"
+    strip-ansi "^5.2.0"
+
+string-width@^4.1.0:
+  version "4.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/string-width/-/string-width-4.1.0.tgz#ba846d1daa97c3c596155308063e075ed1c99aff"
+  integrity sha1-uoRtHaqXw8WWFVMIBj4HXtHJmv8=
+  dependencies:
+    emoji-regex "^8.0.0"
+    is-fullwidth-code-point "^3.0.0"
+    strip-ansi "^5.2.0"
+
+string-width@^4.2.0:
+  version "4.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
+  integrity sha1-lSGCxGzHssMT0VluYjmSvRY7crU=
+  dependencies:
+    emoji-regex "^8.0.0"
+    is-fullwidth-code-point "^3.0.0"
+    strip-ansi "^6.0.0"
+
+string.prototype.matchall@^4.0.2:
+  version "4.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz#48bb510326fb9fdeb6a33ceaa81a6ea04ef7648e"
+  integrity sha1-SLtRAyb7n962ozzqqBpuoE73ZI4=
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0"
+    has-symbols "^1.0.1"
+    internal-slot "^1.0.2"
+    regexp.prototype.flags "^1.3.0"
+    side-channel "^1.0.2"
+
+string.prototype.trim@^1.2.1:
+  version "1.2.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/string.prototype.trim/-/string.prototype.trim-1.2.1.tgz#141233dff32c82bfad80684d7e5f0869ee0fb782"
+  integrity sha1-FBIz3/Msgr+tgGhNfl8Iae4Pt4I=
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+    function-bind "^1.1.1"
+
+string.prototype.trimleft@^2.1.1:
+  version "2.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74"
+  integrity sha1-m9uKxqvW1gKxek7TIYcNL43O/HQ=
+  dependencies:
+    define-properties "^1.1.3"
+    function-bind "^1.1.1"
+
+string.prototype.trimright@^2.1.1:
+  version "2.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz#440314b15996c866ce8a0341894d45186200c5d9"
+  integrity sha1-RAMUsVmWyGbOigNBiU1FGGIAxdk=
+  dependencies:
+    define-properties "^1.1.3"
+    function-bind "^1.1.1"
+
+string_decoder@^1.1.1:
+  version "1.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
+  integrity sha1-/obnOLGVRK/nBGkkOyoe6SQOro0=
+  dependencies:
+    safe-buffer "~5.1.0"
+
+strip-ansi@^5.2.0:
+  version "5.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
+  integrity sha1-jJpTb+tq/JYr36WxBKUJHBrZwK4=
+  dependencies:
+    ansi-regex "^4.1.0"
+
+strip-ansi@^6.0.0:
+  version "6.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
+  integrity sha1-CxVx3XZpzNTz4G4U7x7tJiJa5TI=
+  dependencies:
+    ansi-regex "^5.0.0"
+
+strip-bom@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+  integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
+
+strip-bom@^4.0.0:
+  version "4.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
+  integrity sha1-nDUFwdtFvO3KPZz3oW9cWqOQGHg=
+
+strip-eof@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+  integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
+
+strip-final-newline@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
+  integrity sha1-ibhS+y/L6Tb29LMYevsKEsGrWK0=
+
+strip-indent@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
+  integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=
+
+strip-json-comments@^3.0.1:
+  version "3.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180"
+  integrity sha1-djjTFCISns9EV0QACfugP5+awYA=
+
+supports-color@^5.3.0:
+  version "5.5.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+  integrity sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=
+  dependencies:
+    has-flag "^3.0.0"
+
+supports-color@^7.0.0, supports-color@^7.1.0:
+  version "7.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
+  integrity sha1-aOMlkd9z4lrRxLSRCKLsUHliv9E=
+  dependencies:
+    has-flag "^4.0.0"
+
+supports-hyperlinks@^2.0.0:
+  version "2.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz#f663df252af5f37c5d49bbd7eeefa9e0b9e59e47"
+  integrity sha1-9mPfJSr183xdSbvX7u+p4Lnlnkc=
+  dependencies:
+    has-flag "^4.0.0"
+    supports-color "^7.0.0"
+
+symbol-tree@^3.2.2:
+  version "3.2.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
+  integrity sha1-QwY30ki6d+B4iDlR+5qg7tfGP6I=
+
+table@^5.2.3:
+  version "5.4.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/table/-/table-5.4.3.tgz#1f6a1377966ce70a33230d9457fb48ac173d216b"
+  integrity sha1-H2oTd5Zs5wozIw2UV/tIrBc9IWs=
+  dependencies:
+    ajv "^6.10.2"
+    lodash "^4.17.14"
+    slice-ansi "^2.1.0"
+    string-width "^4.1.0"
+
+terminal-link@^2.0.0:
+  version "2.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"
+  integrity sha1-FKZKJ6s8Dfkz6lRvulXy0HjtyZQ=
+  dependencies:
+    ansi-escapes "^4.2.1"
+    supports-hyperlinks "^2.0.0"
+
+test-exclude@^6.0.0:
+  version "6.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
+  integrity sha1-BKhphmHYBepvopO2y55jrARO8V4=
+  dependencies:
+    "@istanbuljs/schema" "^0.1.2"
+    glob "^7.1.4"
+    minimatch "^3.0.4"
+
+text-table@^0.2.0:
+  version "0.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+  integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
+
+throat@^5.0.0:
+  version "5.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
+  integrity sha1-xRmSNYA6rRh1SmZ9ZZtecs4Wdks=
+
+through@^2.3.6:
+  version "2.3.8"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+  integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
+
+tiny-emitter@^2.0.0:
+  version "2.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
+  integrity sha1-HRpW7fxRxD6GPLtTgqcjMONVVCM=
+
+tmp@^0.0.33:
+  version "0.0.33"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+  integrity sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=
+  dependencies:
+    os-tmpdir "~1.0.2"
+
+tmpl@1.0.x:
+  version "1.0.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
+  integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=
+
+to-fast-properties@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+  integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
+
+to-object-path@^0.3.0:
+  version "0.3.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
+  integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=
+  dependencies:
+    kind-of "^3.0.2"
+
+to-regex-range@^2.1.0:
+  version "2.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
+  integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=
+  dependencies:
+    is-number "^3.0.0"
+    repeat-string "^1.6.1"
+
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha1-FkjESq58jZiKMmAY7XL1tN0DkuQ=
+  dependencies:
+    is-number "^7.0.0"
+
+to-regex@^3.0.1, to-regex@^3.0.2:
+  version "3.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+  integrity sha1-E8/dmzNlUvMLUfM6iuG0Knp1mc4=
+  dependencies:
+    define-property "^2.0.2"
+    extend-shallow "^3.0.2"
+    regex-not "^1.0.2"
+    safe-regex "^1.1.0"
+
+tough-cookie@^2.3.3, tough-cookie@~2.5.0:
+  version "2.5.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+  integrity sha1-zZ+yoKodWhK0c72fuW+j3P9lreI=
+  dependencies:
+    psl "^1.1.28"
+    punycode "^2.1.1"
+
+tough-cookie@^3.0.1:
+  version "3.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2"
+  integrity sha1-nfT1fnOcJpMKAYGEiH9K233Kc7I=
+  dependencies:
+    ip-regex "^2.1.0"
+    psl "^1.1.28"
+    punycode "^2.1.1"
+
+tr46@^1.0.1:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
+  integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=
+  dependencies:
+    punycode "^2.1.0"
+
+trim-newlines@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20"
+  integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=
+
+trim-right@^1.0.1:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+  integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
+
+ts-jest@25.4.0:
+  version "25.4.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/ts-jest/-/ts-jest-25.4.0.tgz#5ad504299f8541d463a52e93e5e9d76876be0ba4"
+  integrity sha1-WtUEKZ+FQdRjpS6T5enXaHa+C6Q=
+  dependencies:
+    bs-logger "0.x"
+    buffer-from "1.x"
+    fast-json-stable-stringify "2.x"
+    json5 "2.x"
+    lodash.memoize "4.x"
+    make-error "1.x"
+    micromatch "4.x"
+    mkdirp "1.x"
+    resolve "1.x"
+    semver "6.x"
+    yargs-parser "18.x"
+
+tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
+  version "1.10.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
+  integrity sha1-w8GflZc/sKYpc/sJ2Q2WHuQ+XIo=
+
+tsutils@^3.17.1:
+  version "3.17.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
+  integrity sha1-7XGZF/EcoN7lhicrKsSeAVot11k=
+  dependencies:
+    tslib "^1.8.1"
+
+tunnel-agent@^0.6.0:
+  version "0.6.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+  integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
+  dependencies:
+    safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+  version "0.14.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+  integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
+
+type-check@~0.3.2:
+  version "0.3.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+  integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
+  dependencies:
+    prelude-ls "~1.1.2"
+
+type-detect@4.0.8:
+  version "4.0.8"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
+  integrity sha1-dkb7XxiHHPu3dJ5pvTmmOI63RQw=
+
+type-fest@^0.11.0:
+  version "0.11.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1"
+  integrity sha1-l6vwhyMQ/tiKXEZrJWgVdhReM/E=
+
+type-fest@^0.6.0:
+  version "0.6.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
+  integrity sha1-jSojcNPfiG61yQraHFv2GIrPg4s=
+
+type-fest@^0.8.1:
+  version "0.8.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
+  integrity sha1-CeJJ696FHTseSNJ8EFREZn8XuD0=
+
+typedarray-to-buffer@^3.1.5:
+  version "3.1.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
+  integrity sha1-qX7nqf9CaRufeD/xvFES/j/KkIA=
+  dependencies:
+    is-typedarray "^1.0.0"
+
+typescript@3.8.3:
+  version "3.8.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
+  integrity sha1-QJ64VE6gM1cRIFhp7EWKsQnuEGE=
+
+ua-parser-js@^0.7.18:
+  version "0.7.20"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/ua-parser-js/-/ua-parser-js-0.7.20.tgz#7527178b82f6a62a0f243d1f94fd30e3e3c21098"
+  integrity sha1-dScXi4L2pioPJD0flP0w4+PCEJg=
+
+union-value@^1.0.0:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
+  integrity sha1-C2/nuDWuzaYcbqTU8CwUIh4QmEc=
+  dependencies:
+    arr-union "^3.1.0"
+    get-value "^2.0.6"
+    is-extendable "^0.1.1"
+    set-value "^2.0.1"
+
+unset-value@^1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
+  integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=
+  dependencies:
+    has-value "^0.3.1"
+    isobject "^3.0.0"
+
+uri-js@^4.2.2:
+  version "4.2.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
+  integrity sha1-lMVA4f93KVbiKZUHwBCupsiDjrA=
+  dependencies:
+    punycode "^2.1.0"
+
+urix@^0.1.0:
+  version "0.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+  integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
+
+use@^3.1.0:
+  version "3.1.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
+  integrity sha1-1QyMrHmhn7wg8pEfVuuXP04QBw8=
+
+util-deprecate@^1.0.1:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+uuid@^3.3.2:
+  version "3.3.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
+  integrity sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE=
+
+v8-compile-cache@^2.0.3:
+  version "2.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"
+  integrity sha1-4U3jezGm0ZT1aQ1n78Tn9vxqsw4=
+
+v8-to-istanbul@^4.1.3:
+  version "4.1.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/v8-to-istanbul/-/v8-to-istanbul-4.1.3.tgz#22fe35709a64955f49a08a7c7c959f6520ad6f20"
+  integrity sha1-Iv41cJpklV9JoIp8fJWfZSCtbyA=
+  dependencies:
+    "@types/istanbul-lib-coverage" "^2.0.1"
+    convert-source-map "^1.6.0"
+    source-map "^0.7.3"
+
+validate-npm-package-license@^3.0.1:
+  version "3.0.4"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
+  integrity sha1-/JH2uce6FchX9MssXe/uw51PQQo=
+  dependencies:
+    spdx-correct "^3.0.0"
+    spdx-expression-parse "^3.0.0"
+
+verror@1.10.0:
+  version "1.10.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+  integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
+  dependencies:
+    assert-plus "^1.0.0"
+    core-util-is "1.0.2"
+    extsprintf "^1.2.0"
+
+w3c-hr-time@^1.0.1:
+  version "1.0.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045"
+  integrity sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=
+  dependencies:
+    browser-process-hrtime "^0.1.2"
+
+w3c-xmlserializer@^1.1.2:
+  version "1.1.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz#30485ca7d70a6fd052420a3d12fd90e6339ce794"
+  integrity sha1-MEhcp9cKb9BSQgo9Ev2Q5jOc55Q=
+  dependencies:
+    domexception "^1.0.1"
+    webidl-conversions "^4.0.2"
+    xml-name-validator "^3.0.0"
+
+walker@^1.0.7, walker@~1.0.5:
+  version "1.0.7"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
+  integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=
+  dependencies:
+    makeerror "1.0.x"
+
+warning@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c"
+  integrity sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=
+  dependencies:
+    loose-envify "^1.0.0"
+
+warning@^4.0.3:
+  version "4.0.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
+  integrity sha1-Fungd+uKhtavfWSqHgX9hbRnjKM=
+  dependencies:
+    loose-envify "^1.0.0"
+
+webidl-conversions@^4.0.2:
+  version "4.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
+  integrity sha1-qFWYCx8LazWbodXZ+zmulB+qY60=
+
+whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.5:
+  version "1.0.5"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
+  integrity sha1-WrrPd3wyFmpR0IXWtPPn0nET3bA=
+  dependencies:
+    iconv-lite "0.4.24"
+
+whatwg-fetch@3.0.0, whatwg-fetch@>=0.10.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
+  integrity sha1-/IBORYzEYACbGiuWa8iBfSV4rvs=
+
+whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0:
+  version "2.3.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
+  integrity sha1-PUseAxLSB5h5+Cav8Y2+7KWWD78=
+
+whatwg-url@^7.0.0:
+  version "7.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd"
+  integrity sha1-/ekm+lSlmfOt+C3/Jan3vgLcbt0=
+  dependencies:
+    lodash.sortby "^4.7.0"
+    tr46 "^1.0.1"
+    webidl-conversions "^4.0.2"
+
+which-module@^2.0.0:
+  version "2.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+  integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+
+which@^1.2.9, which@^1.3.1:
+  version "1.3.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+  integrity sha1-pFBD1U9YBTFtqNYvn1CRjT2nCwo=
+  dependencies:
+    isexe "^2.0.0"
+
+which@^2.0.1, which@^2.0.2:
+  version "2.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+  integrity sha1-fGqN0KY2oDJ+ELWckobu6T8/UbE=
+  dependencies:
+    isexe "^2.0.0"
+
+word-wrap@~1.2.3:
+  version "1.2.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
+  integrity sha1-YQY29rH3A4kb00dxzLF/uTtHB5w=
+
+wordwrap@~1.0.0:
+  version "1.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+  integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
+
+wrap-ansi@^6.2.0:
+  version "6.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
+  integrity sha1-6Tk7oHEC5skaOyIUePAlfNKFblM=
+  dependencies:
+    ansi-styles "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+write-file-atomic@^3.0.0:
+  version "3.0.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
+  integrity sha1-Vr1cWlxwSBzRnFcb05q5ZaXeVug=
+  dependencies:
+    imurmurhash "^0.1.4"
+    is-typedarray "^1.0.0"
+    signal-exit "^3.0.2"
+    typedarray-to-buffer "^3.1.5"
+
+write@1.0.3:
+  version "1.0.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"
+  integrity sha1-CADhRSO5I6OH5BUSPIZWFqrg9cM=
+  dependencies:
+    mkdirp "^0.5.1"
+
+ws@^7.0.0:
+  version "7.2.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/ws/-/ws-7.2.3.tgz#a5411e1fb04d5ed0efee76d26d5c46d830c39b46"
+  integrity sha1-pUEeH7BNXtDv7nbSbVxG2DDDm0Y=
+
+xml-name-validator@^3.0.0:
+  version "3.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
+  integrity sha1-auc+Bt5NjG5H+fsYH3jWSK1FfGo=
+
+xmlchars@^2.1.1:
+  version "2.2.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
+  integrity sha1-Bg/hvLf5x2/ioX24apvDq4lCEMs=
+
+xregexp@^4.3.0:
+  version "4.3.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/xregexp/-/xregexp-4.3.0.tgz#7e92e73d9174a99a59743f67a4ce879a04b5ae50"
+  integrity sha1-fpLnPZF0qZpZdD9npM6HmgS1rlA=
+  dependencies:
+    "@babel/runtime-corejs3" "^7.8.3"
+
+y18n@^4.0.0:
+  version "4.0.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
+  integrity sha1-le+U+F7MgdAHwmThkKEg8KPIVms=
+
+yargs-parser@18.x, yargs-parser@^18.1.1:
+  version "18.1.3"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
+  integrity sha1-vmjEl1xrKr9GkjawyHA2L6sJp7A=
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
+yargs-parser@^10.0.0:
+  version "10.1.0"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8"
+  integrity sha1-cgImW4n36eny5XZeD+c1qQXtuqg=
+  dependencies:
+    camelcase "^4.1.0"
+
+yargs@^15.3.1:
+  version "15.3.1"
+  resolved "https://repox.jfrog.io/repox/api/npm/npm/yargs/-/yargs-15.3.1.tgz#9505b472763963e54afe60148ad27a330818e98b"
+  integrity sha1-lQW0cnY5Y+VK/mAUitJ6MwgY6Ys=
+  dependencies:
+    cliui "^6.0.0"
+    decamelize "^1.2.0"
+    find-up "^4.1.0"
+    get-caller-file "^2.0.1"
+    require-directory "^2.1.1"
+    require-main-filename "^2.0.0"
+    set-blocking "^2.0.0"
+    string-width "^4.2.0"
+    which-module "^2.0.0"
+    y18n "^4.0.0"
+    yargs-parser "^18.1.1"