Commit da1083c5 authored by Daniel's avatar Daniel Committed by Tamika Tannis

Merge search to master (#315)

* Merge `master` into `feature/search_v2` (#253)

* Clean up doc (#249)

* Remove example folder in FE (#251)

* Add better logging for profile page views (#239)

* Added new search page layout (#250)

* Added SearchPanel to search page, with new wide layout
* Added 'ResourceSelector'

* Add SearchBar to NavBar + update styles (#252)

* Add SearchBar to NavBar + update styles

* Lint fix

* Code cleanup

* Fix tests

* Update ResourceListItem UI (#255)

* WIP

* Update layout

* Some code cleanup

* Tests

* Add some starter doc for the AppConfig

* Code cleanup + update UserListItem description UI

* start icon -> resource icon

* Moved Feedback tool to NavBar (#256)

* Update Feedback Component

* Update tests

* Clean up some styles

* reposition form

* Tweak feedback style

* Initial implementation of SearchFilter (#260)

* Initial implementation of SearchFilter

* Clean some code + add CheckBoxItem tests

* Fixed warning on ResourceSelector

* Add/fix some more tests

* Some tweaks before merge

* Implement New color theme (#276)

* Added a color palette file
* Replaced $gray-light/dark colors with semantic variables for each purpose
* Update brand colors to indigo

* Merge `master` into  `feature/search_v2` (#286)

* Update searchv2 master (#313)

* Clean up doc (#249)

* Remove example folder in FE (#251)

* Add better logging for profile page views (#239)

* add stalebot config (#264)

* Update stalebot setting (#268)

* Adds the request timeout configuration (#275)

* Adds the request timeout configuration

* Bumps the version to take advantage of request timeout

* Update AxiosError handling (#283)

* Update v0.ts

* lint fix

* Use Redux-Saga for Search Actions (#265)

* Added redux actions and sagas instead for each search action: `submitSearch`, `setResource`, `setPageIndex`, `loadPreviousSearch`, and `UrlDidUpdate`. This greatly simplifies the `SearchPage` logic in preparation for adding filters.
* Added `navigation-utils`.

* Removed `SQUASH COMMITS` from the PR template (#287)

Removed `SQUASH COMMITS` from the PR template

* TagInfo now fires 'submitSearch' instead of using navigation (#285)

* Remove render_template for feedback email (#293)

* Remove render_template for feedback email

* Fix html markup

* Added auto-select resource after searching (#292)

* Truncate the column type (#297)

* Truncate the column type

* Update truncation method

* Cleanup logic

* Improve table & columns description formatting (#98) (#298)

* Add support for React-Markdown to editable text fields
* Add support for windows via cross-env

* Update EditableText component (#299)

* Fix some styling issues with react-markdown
* Update EditableText to use React Refs instead of ref callbacks
* Add configs for table and column description max lengths

* Change 'json' to 'data' field for Python Requests (#303)

* Switch usage of 'json' to 'data' in metadata APIs
* Use 'raw_request=True' for Envoy client post/put APIs

* Update README.md (#302)

* Link deeper to doc content (#304)

Summary of Changes

Will scroll past GitHub repo folders (specially more handy on small screens like phones/tablets)
Like https://github.com/lyft/amundsendatabuilder/pull/147

* Fixed linebreaks with Markdown descriptions (#305)

* Amundsen Notifications Without Preferences (#273)

* Initial start to notifications API (#215)

* initial start to notifications API

* fixing some styling

* fixed lint errors

* update types

* added tests

* linter, moved notification types

* addressed comments regarding imports/enum naming

* fixed alphabetical order

* Notifs post email functionality (#222)

* initial start to notifications API

* fixing some styling

* fixed lint errors

* update types

* added tests

* linter, moved notification types

* added template support

* made changes to reflect private changes

* added helper function

* fixed lint issue

* addressed comments, added some type checking and cleaned up comments

* testing removing test

* fixed linter

* fixed lint

* fixed linting issues

* skip type checking

* fixed lint

* fixed typing on get request args

* removed typing for get request to fix lint issues

* fixed linter again

* re added test

* raise exception inside of getmailclient

* added exceptions

* addressed comments

* whitespace issue

* removed calls to get_query_param

* fixed syntax error

* Send notification when adding/removing owner from table (#237)

* basic e2e functionality for adding/removing

* send_notification refactor

* fix lint errors

* blank line lint error

* fixed syntax issue

* arg typing

* addressed comments, fixed code style

* Prevent Self-Notifications (#243)

* Prevent user from notifying themselves

* removed exception

* added owner check to send_notification

* Fixed return for no recipients (#244)

* fixed return for no recipients

* fixed linter issue

* Request notifications component (#238)

* init of request form

* basic request component

* getting basic functionality in

* clearing out css

* removed z-index fixes and add constants

* fixed string casting

* added redux-saga calls

* removed reset request notification

* fixed tests

* addressed comments, added basic test, added redux state management for opening/closing component

* added tests, just need to add render test

* cleaned up component tests:

* addressed html/css comments

* removed unecessary styling

* removed collapsed class

* cleaned up render method

* fixed test

* Open request component (#254)

* added button to open up request component

* removed tabledetail changes

* className styling

* fixed text-decoration

* added tests, changed naming for OpenRequest

* styling formatting

* Add, Request, and Remove Email Copy (#257)

* init for fixing email copy for request, add, and remove

* removed print statement

* fixed python unit test

* fixed linter issues

* addressed comments, fixed linter issues

* added notification unit test

* fixed test positional arg

* fix test

* Add notification action logging (#258)

* init of adding action logging

* changed location of action logging

* fixed linter errors

* fixed comment

* addressed comments

* remove request test call (#259)

* hide request if description already exists (#269)

* fixed open request button, request form styling (#267)

* Added request dropdown component (#262)

* init

* made fixes

* cleaned up code

* fixed color issues

* fixed import order

* fixed styling, changed ducks/sagas

* User dropdown (#263)

* init

* fixed sty;es

* fixed test issue

* fixed test

* added tests, addressed comments

* Request Metadata Component Tests (#270)

* added tests + readonly field to stop errors

* fixed tslint

* addressed comments, added header tests

* Request form navigation fix, dropdown fix (#272)

* Request form navigation fix, dropdown fix

* added test

* added unique id to dropdown

* Creates User Preferences page with no functionality (#266)

* init

* added event handlers

* removed test file

* added constants

* addressed comments

* fixed test, removed all links to page

* updated test

* fixed call to onclick

* removed preferences page

* Python cleanup + tests (#277)

* Python cleanup + tests

* More tests + revert some unecessary changes

* Bring dropdown UI closer to design (#278)

* Rename OpenRequestDescription for clarity + code cleanup + test additions (#279)

* Notifications ducks cleanup + tests (#280)

* Notifications ducks cleanup + tests

* Fix issues

* Fix template for edge case of empty form (#281)

* Temporary debugging code, will revert

* Temporary debugging code, will revert

* Implement notification form confirmation (#289)

* Preserve compatibility in base_mail_client (#290)

* Notifications Configs + Doc (#291)

* Add notification config

* Code cleanup

* More cleanup + add a test

* Add some doc for how to enable features

* Add config utils test + fix type error

* Relative URLs to child configuration docs (#294)

* Relative URLs to child configuration docs

Relative URLs to docs in the same folder should do. They work for any branch, local copies of the docs - and should work better if we ever (or whenever :-) we get to having e.g a Sphinx generated site.

* Update application_config.md

Relative doc link

* Update flask_config.md

Relative doc link

* Update flask_config.md

Relative doc link

* Remove temporary debugging code

* Improve behavior of notification sending for owner editing (#296)

* Initial Implementation: Notification only on success

* Cleanup + tests: Notification only on success

* Cleanup: Remove test code to trigger failure

* Cleanup: Lint fix

* Workaround for not notifying teams or alumni

* Cleanup: Remove import mistake

* Utilize NotificationType enums instead of hardcoded string

* Remove use of render_template

* More minor cleanups

* Address some feedback

* Cleanup

* More cleanup

* Notifications Improvements (#301)

* Initial start to notifications API (#215)

* initial start to notifications API

* fixing some styling

* fixed lint errors

* update types

* added tests

* linter, moved notification types

* addressed comments regarding imports/enum naming

* fixed alphabetical order

* Notifs post email functionality (#222)

* initial start to notifications API

* fixing some styling

* fixed lint errors

* update types

* added tests

* linter, moved notification types

* added template support

* made changes to reflect private changes

* added helper function

* fixed lint issue

* addressed comments, added some type checking and cleaned up comments

* testing removing test

* fixed linter

* fixed lint

* fixed linting issues

* skip type checking

* fixed lint

* fixed typing on get request args

* removed typing for get request to fix lint issues

* fixed linter again

* re added test

* raise exception inside of getmailclient

* added exceptions

* addressed comments

* whitespace issue

* removed calls to get_query_param

* fixed syntax error

* Send notification when adding/removing owner from table (#237)

* basic e2e functionality for adding/removing

* send_notification refactor

* fix lint errors

* blank line lint error

* fixed syntax issue

* arg typing

* addressed comments, fixed code style

* Prevent Self-Notifications (#243)

* Prevent user from notifying themselves

* removed exception

* added owner check to send_notification

* Fixed return for no recipients (#244)

* fixed return for no recipients

* fixed linter issue

* Request notifications component (#238)

* init of request form

* basic request component

* getting basic functionality in

* clearing out css

* removed z-index fixes and add constants

* fixed string casting

* added redux-saga calls

* removed reset request notification

* fixed tests

* addressed comments, added basic test, added redux state management for opening/closing component

* added tests, just need to add render test

* cleaned up component tests:

* addressed html/css comments

* removed unecessary styling

* removed collapsed class

* cleaned up render method

* fixed test

* Open request component (#254)

* added button to open up request component

* removed tabledetail changes

* className styling

* fixed text-decoration

* added tests, changed naming for OpenRequest

* styling formatting

* Add, Request, and Remove Email Copy (#257)

* init for fixing email copy for request, add, and remove

* removed print statement

* fixed python unit test

* fixed linter issues

* addressed comments, fixed linter issues

* added notification unit test

* fixed test positional arg

* fix test

* Add notification action logging (#258)

* init of adding action logging

* changed location of action logging

* fixed linter errors

* fixed comment

* addressed comments

* remove request test call (#259)

* hide request if description already exists (#269)

* fixed open request button, request form styling (#267)

* Added request dropdown component (#262)

* init

* made fixes

* cleaned up code

* fixed color issues

* fixed import order

* fixed styling, changed ducks/sagas

* User dropdown (#263)

* init

* fixed sty;es

* fixed test issue

* fixed test

* added tests, addressed comments

* Request Metadata Component Tests (#270)

* added tests + readonly field to stop errors

* fixed tslint

* addressed comments, added header tests

* Request form navigation fix, dropdown fix (#272)

* Request form navigation fix, dropdown fix

* added test

* added unique id to dropdown

* Creates User Preferences page with no functionality (#266)

* init

* added event handlers

* removed test file

* added constants

* addressed comments

* fixed test, removed all links to page

* updated test

* fixed call to onclick

* removed preferences page

* Python cleanup + tests (#277)

* Python cleanup + tests

* More tests + revert some unecessary changes

* Bring dropdown UI closer to design (#278)

* Rename OpenRequestDescription for clarity + code cleanup + test additions (#279)

* Notifications ducks cleanup + tests (#280)

* Notifications ducks cleanup + tests

* Fix issues

* Fix template for edge case of empty form (#281)

* Temporary debugging code, will revert

* Temporary debugging code, will revert

* Implement notification form confirmation (#289)

* Preserve compatibility in base_mail_client (#290)

* Notifications Configs + Doc (#291)

* Add notification config

* Code cleanup

* More cleanup + add a test

* Add some doc for how to enable features

* Add config utils test + fix type error

* Relative URLs to child configuration docs (#294)

* Relative URLs to child configuration docs

Relative URLs to docs in the same folder should do. They work for any branch, local copies of the docs - and should work better if we ever (or whenever :-) we get to having e.g a Sphinx generated site.

* Update application_config.md

Relative doc link

* Update flask_config.md

Relative doc link

* Update flask_config.md

Relative doc link

* Remove temporary debugging code

* Improve behavior of notification sending for owner editing (#296)

* Initial Implementation: Notification only on success

* Cleanup + tests: Notification only on success

* Cleanup: Remove test code to trigger failure

* Cleanup: Lint fix

* Workaround for not notifying teams or alumni

* Cleanup: Remove import mistake

* Utilize NotificationType enums instead of hardcoded string

* Remove use of render_template

* More minor cleanups

* Address some feedback

* Cleanup

* More cleanup

* Updates for RequestMetadataForm

* Switch to generating a  for url + comment required for request column descriptions

* Update some tests + comment out ones that need update before merge

* Update some tests + comment out ones that need update before merge

* Code cleanup

* Update and rename notification_utils python tests

* Modify resource_url check + add docstrings for python tests

* Component cleanup

* Cleanup component tests

* Fix some typos

* optimize docker (#300)

* Update developer_guide.md (#310)

Fix a 404

* Custom routes + Further notification cleanup (#309)

* Further notification cleanup

* Catch any errors getting the user for metrics + add doc for INIT_CUSTOM_ROUTES

* Revert change to _build_metrics

* Fix some merge conflicts

* Fix some merge conflicts

* Add source for notification email links (#312)

* Add source for notification email links

* Update TableDetail logic

* Fix FlashMessage styles

* Update config-types.ts (#314)

* Convert usages of `white` to `$white`

* - ResourceSelector now uses `setResource` action instead of onChange prop
- Added/fixed test cases for ResourceSelector/SearchPage
- Deleted unused code in SearchPage

* Added additional test cases for SearchBar

* Modified list-group shoadows

* Added a test case for ResourceSelector
parent 74c652fb
.sb-avatar > div {
border: 1px solid white;
border: 1px solid $white;
}
.sb-avatar > img {
......
......@@ -73,7 +73,7 @@
&.btn-flat-icon {
border: none;
background-color: transparent;
color: $text-medium;
color: $text-secondary;
box-shadow: none !important;
padding: 0px;
text-align: left;
......@@ -96,7 +96,7 @@
margin: 4px 0 0 0;
padding: 0;
background-color: $gray-light;
background-color: $icon-bg;
border: none;
mask-image: url('/static/images/icons/Close.svg');
-webkit-mask-image: url('/static/images/icons/Close.svg');
......@@ -110,7 +110,7 @@
&:focus,
&:not(.disabled):hover,
&:not([disabled]):hover {
background-color: $gray-dark;
background-color: $icon-bg-dark;
}
}
......@@ -119,11 +119,11 @@
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
color: $text-medium;
color: $text-secondary;
pointer-events: none;
&:hover {
color: $text-medium;
color: $text-secondary;
}
}
}
/**
Avoid using these color definitions directly.
Define semantic variables that reference this color palette instead.
The color palette can be swapped out or modified without
revisiting each individual color usage.
---------------
Do this:
$text-primary: $gray100;
body {
color: $text-primary;
}
----------------
Don't do this:
body {
color: $gray100;
}
*/
$white: #FFFFFF;
$black: #000000;
/* Red */
$red0: #FFFAFB;
$red5: #FFE5E9;
$red10: #FFCFD5;
$red20: #FFA0AC;
$red30: #FF7689;
$red40: #FF516B;
$red50: #FF3354;
$red60: #E6193F;
$red70: #B8072C;
$red80: #8C0020;
$red90: #670019;
$red100: #560015;
/* Sunset */
$sunset0: #FFFBFA;
$sunset5: #FFE4DD;
$sunset10: #FFCCBF;
$sunset20: #FF9E87;
$sunset30: #FF7B5C;
$sunset40: #FF623E;
$sunset50: #FF4E28;
$sunset60: #DB3615;
$sunset70: #AF230A;
$sunset80: #841604;
$sunset90: #5F0E01;
$sunset100: #4E0B00;
/* Orange */
$orange0: #FFF6F2;
$orange5: #FFE8DD;
$orange10: #FFD9C7;
$orange20: #FFB38F;
$orange30: #FF915D;
$orange40: #FF7232;
$orange50: #F9560E;
$orange60: #D03D00;
$orange70: #A82E00;
$orange80: #832300;
$orange90: #651A00;
$orange100: #581600;
/* Amber */
$amber0: #FFFDFA;
$amber5: #FFF6E7;
$amber10: #FFF0D4;
$amber20: #FFE0A9;
$amber30: #FFD082;
$amber40: #FFC161;
$amber50: #FFB146;
$amber60: #FFA030;
$amber70: #FF8D1F;
$amber80: #FE7E13;
$amber90: #E66909;
$amber100: #CB5803;
/* Yellow */
$yellow0: #FFFEFA;
$yellow5: #FFF8D9;
$yellow10: #FFF3B8;
$yellow20: #FFE77B;
$yellow30: #FFDD4C;
$yellow40: #FFD32A;
$yellow50: #FFCA13;
$yellow60: #FFC002;
$yellow70: #EFAC00;
$yellow80: #DC9900;
$yellow90: #C78700;
$yellow100: #B07600;
/* Citron */
$citron0: #FFFFF2;
$citron5: #FFFFD2;
$citron10: #FEFFB2;
$citron20: #FBFF6F;
$citron30: #F1FB3B;
$citron40: #E2F316;
$citron50: #CCE700;
$citron60: #B5D900;
$citron70: #9AC800;
$citron80: #82B400;
$citron90: #6C9C00;
$citron100: #578000;
/* Lime */
$lime0: #FDFFFA;
$lime5: #EDFED0;
$lime10: #D6F3A0;
$lime20: #A4DC48;
$lime30: #75C404;
$lime40: #5EAB00;
$lime50: #499300;
$lime60: #347D00;
$lime70: #216800;
$lime80: #155600;
$lime90: #0E4400;
$lime100: #0A3600;
/* Green */
$green0: #FAFFFC;
$green5: #D1FFE2;
$green10: #A8FFC4;
$green20: #4BE77A;
$green30: #04CD3D;
$green40: #00B32E;
$green50: #009B22;
$green60: #008316;
$green70: #006E0B;
$green80: #005A05;
$green90: #004802;
$green100: #003901;
/* Mint */
$mint0: #FAFFFD;
$mint5: #D1FFEE;
$mint10: #A6FBDE;
$mint20: #4AE3AE;
$mint30: #04CA83;
$mint40: #00B16F;
$mint50: #00985D;
$mint60: #00824C;
$mint70: #006C3C;
$mint80: #00592F;
$mint90: #004724;
$mint100: #00381C;
/* Teal */
$teal0: #FAFFFE;
$teal5: #D1FFF7;
$teal10: #A8FFF4;
$teal20: #4CEAE4;
$teal30: #04CED2;
$teal40: #00B0B9;
$teal50: #00949F;
$teal60: #007B85;
$teal70: #00626B;
$teal80: #004C53;
$teal90: #003B40;
$teal100: #003338;
/* Cyan */
$cyan0: #FAFDFF;
$cyan5: #E7F6FF;
$cyan10: #D4F0FF;
$cyan20: #A9E1FF;
$cyan30: #82D2FF;
$cyan40: #5DBCF4;
$cyan50: #3A97D3;
$cyan60: #2277B3;
$cyan70: #135B96;
$cyan80: #09457B;
$cyan90: #043563;
$cyan100: #01284E;
/* Blue */
$blue0: #FAFBFF;
$blue5: #E8ECFF;
$blue10: #D5DCFF;
$blue20: #ACBBFF;
$blue30: #869DFF;
$blue40: #6686FF;
$blue50: #4B73FF;
$blue60: #3668FF;
$blue70: #2156DB;
$blue80: #1242AF;
$blue90: #093186;
$blue100: #042260;
/* Indigo */
$indigo0: #FAFAFF;
$indigo5: #EBEBFF;
$indigo10: #DCDCFF;
$indigo20: #BABAFF;
$indigo30: #9C9BFF;
$indigo40: #8481FF;
$indigo50: #726BFF;
$indigo60: #665AFF;
$indigo70: #604CFF;
$indigo80: #523BE4;
$indigo90: #3E29B1;
$indigo100: #2B1B81;
/* Purple */
$purple0: #FDFAFF;
$purple5: #F6EBFF;
$purple10: #ECDCFF;
$purple20: #D7B8FF;
$purple30: #C294FF;
$purple40: #AD71FF;
$purple50: #9B52FF;
$purple60: #8B37FF;
$purple70: #7B20F9;
$purple80: #590DC4;
$purple90: #420499;
$purple100: #390188;
/* Pink */
$pink0: #FFFAFD;
$pink5: #FFE1F2;
$pink10: #FFC7E4;
$pink20: #FF8FCC;
$pink30: #FF5DBB;
$pink40: #FF32B1;
$pink50: #FF0EB0;
$pink60: #DE00A7;
$pink70: #BD00A0;
$pink80: #A00093;
$pink90: #860081;
$pink100: #71006F;
/* Rose */
$rose0: #FFF2F5;
$rose5: #FFE1E9;
$rose10: #FFCFDC;
$rose20: #FFA0BA;
$rose30: #FF769E;
$rose40: #FF5187;
$rose50: #FF3378;
$rose60: #E51966;
$rose70: #B70752;
$rose80: #8B0040;
$rose90: #660031;
$rose100: #55002A;
/* Gray */
$gray0: #FCFCFF;
$gray5: #F4F4FA;
$gray10: #E7E7EF;
$gray15: #D8D8E4;
$gray20: #CACAD9;
$gray30: #ACACC0;
$gray40: #9191A8;
$gray50: #787891;
$gray60: #63637B;
$gray70: #515167;
$gray80: #414155;
$gray90: #333344;
$gray100: #292936;
@import 'variables';
.dropdown-menu {
box-shadow: 0 0 24px -2px rgba(0, 0, 0, .2);
border-radius: 5px;
......@@ -7,7 +9,7 @@
li {
&:hover {
background-color: $gray-lightest;
background-color: $body-bg-secondary;
}
a {
padding: 8px;
......
@import 'variables';
img.icon {
background-color: $gray-light;
background-color: $icon-bg;
border: none;
height: 24px;
margin: -3px 4px -3px 0px;
......@@ -11,11 +11,11 @@ img.icon {
width: 24px;
&.icon-color {
background-color: $brand-color-3;
background-color: $icon-bg-brand;
}
&.icon-dark {
background-color: $text-medium;
background-color: $icon-bg-dark;
}
// TODO - Add other icons here
......@@ -26,6 +26,11 @@ img.icon {
mask-image: url('/static/images/icons/Alert-Triangle.svg');
}
&.icon-bigquery {
content: url('/static/images/icons/logo-bigquery.svg');
background-color: transparent;
}
&.icon-bookmark {
-webkit-mask-image: url('/static/images/icons/Favorite.svg');
mask-image: url('/static/images/icons/Favorite.svg');
......@@ -51,6 +56,16 @@ img.icon {
mask-image: url('/static/images/icons/Down.svg');
}
&.icon-help {
-webkit-mask-image: url('/static/images/icons/Help-Circle.svg');
mask-image: url('/static/images/icons/Help-Circle.svg');
}
&.icon-hive {
content: url('/static/images/icons/logo-hive.svg');
background-color: transparent;
}
&.icon-github {
-webkit-mask-image: url('/static/images/icons/github.svg');
mask-image: url('/static/images/icons/github.svg');
......@@ -76,11 +91,21 @@ img.icon {
mask-image: url('/static/images/icons/Plus-Circle.svg');
}
&.icon-postgres {
content: url('/static/images/icons/logo-postgres.svg');
background-color: transparent;
}
&.icon-preview {
-webkit-mask-image: url('/static/images/icons/Preview.svg');
mask-image: url('/static/images/icons/Preview.svg');
}
&.icon-redshift {
content: url('/static/images/icons/logo-redshift.svg');
background-color: transparent;
}
&.icon-refresh {
-webkit-mask-image: url('/static/images/icons/Refresh-cw.svg');
mask-image: url('/static/images/icons/Refresh-cw.svg');
......@@ -121,6 +146,6 @@ img.icon {
:disabled {
> img.icon,
> img.icon.icon-color {
background-color: $gray-light;
background-color: $icon-bg-disabled;
}
}
@import 'variables';
input {
&::-webkit-input-placeholder,
&::-moz-placeholder,
&:-ms-input-placeholder,
&:-moz-placeholder,
&::placeholder {
color: $text-placeholder !important;
}
&[type="radio"] {
margin: 5px;
}
&[type="text"] {
color: $text-secondary !important;
}
}
textarea {
width: 100%;
color: $text-secondary !important;
border: 1px solid $stroke;
border-radius: 5px;
padding: 10px;
}
@import 'variables';
.label-danger {
color: $badge-text-color;
background-color: $badge-danger-color;
}
.label-primary {
color: $badge-text-color;
background-color: $badge-primary-color;
}
.label-success {
color: $badge-text-color;
background-color: $badge-success-color;
}
.label-warning {
color: $badge-text-color;
background-color: $badge-warning-color;
}
// TODO - Try to implement this with bootstrap variables
@import 'variables';
.list-group {
margin: 24px 0;
......@@ -6,16 +5,15 @@
.list-group-item {
border-left: none;
border-right: none;
cursor: pointer;
padding: 0;
&:hover {
border-color: $gray;
background-color: $gray-lightest;
box-shadow: -2px 2px 4px 1px rgba(0, 0, 0, 0.16);
cursor: pointer;
}
&:hover + li {
border-top-color: $gray;
&:hover + .list-group-item {
box-shadow: inset 0px 8px 6px -6px rgba(0, 0, 0, 0.16)
}
}
}
......@@ -7,12 +7,12 @@
li {
> a,
> span {
border: 1px solid $gray-lighter;
border: 1px solid $stroke;
color: $brand-color-4;
&:focus,
&:hover {
background-color: $gray-lighter;
background-color: $body-bg-secondary;
color: $link-hover-color;
z-index: 0;
}
......@@ -27,7 +27,7 @@
&:focus {
background-color: $brand-color-4;
border-color: $brand-color-4;
color: white;
color: $white;
z-index: 0;
}
}
......
......@@ -2,15 +2,14 @@
@import 'variables';
.popover {
background-color: $gray-base;
border: 1px solid $gray-base;
background-color: $body-bg-dark;
border: 1px solid $body-bg-dark;
color: $text-light;
font-size: 12px;
padding: 5px;
}
.popover-title {
background-color: $gray-darker;
border-bottom: 1px solid $gray-light;
border-bottom: 1px solid $stroke;
color: $text-light;
font-size: 14px;
padding: 5px;
......@@ -19,22 +18,22 @@
padding: 2px 5px;
}
.popover.right .arrow:after {
border-right-color: $gray-base;
border-right-color: $body-bg-dark;
}
.popover.bottom .arrow:after {
border-bottom-color: $gray-base;
border-bottom-color: $body-bg-dark;
}
.popover.top .arrow:after {
border-top-color: $gray-base;
border-top-color: $body-bg-dark;
}
.popover.left .arrow:after {
border-left-color: $gray-base;
border-left-color: $body-bg-dark;
}
.tooltip-inner {
padding: 0;
border-radius: 3px;
background-color: $gray-base;
background-color: $body-bg-dark;
}
.tooltip-inner button {
......@@ -43,13 +42,13 @@
font-size: 14px;
border-radius: 3px;
border: none;
background-color: $gray-base;
background-color: $body-bg-dark;
color: $body-bg;
font-weight: $font-weight-body-bold;
outline: none;
}
.tooltip-inner button:hover {
color: $text-medium;
color: $text-secondary;
}
.error-tooltip {
......
......@@ -6,7 +6,7 @@
h1, h2, h3,
.h1, .h2, .h3 {
color: $text-dark;
color: $text-primary;
font-family: $font-family-header;
margin: 0;
}
......@@ -33,7 +33,7 @@ h3,
}
body {
color: $text-dark;
color: $text-primary;
font-size: 14px;
font-family: $font-family-body;
font-weight: $font-weight-body-regular;
......@@ -58,7 +58,7 @@ body {
}
.title-3 {
color: $text-medium;
color: $text-secondary;
font-size: 14px;
font-weight: $font-weight-body-bold;
}
......@@ -97,13 +97,13 @@ body {
}
.body-secondary-3 {
color: $text-medium;
color: $text-secondary;
font-size: 14px;
font-weight: $font-weight-body-regular;
}
.body-placeholder {
color: $text-medium;
color: $text-placeholder;
font-size: 14px;
font-weight: $font-weight-body-regular;
}
......@@ -114,7 +114,7 @@ body {
}
.caption {
color: $text-medium;
color: $text-secondary;
font-size: 13px;
font-weight: $font-weight-body-bold;
}
......@@ -126,13 +126,13 @@ body {
}
.resource-type {
color: $gray;
color: $text-placeholder;
font-size: 13px;
font-family: $font-family-monospace;
}
.helper-text {
color: $text-medium;
color: $text-secondary;
font-size: 12px;
font-family: $font-family-body;
}
/**
The following are used in Amundsen but are not part of Bootstrap.
These must be defined here:
$brand-color-1, $brand-color-2, $brand-color-3, $brand-color-4, $brand-color-5
$font-family-header, $font-weight-header-regular, $font-weight-header-bold
$font-family-body, $font-weight-body-regular, $font-weight-body-semi-bold, $font-weight-body-bold
$text-dark, $text-medium, $text-light
$btn-primary-bg-hover, $btn-default-border-hover,
$btn-primary-border-hover, $btn-default-bg-hover,
*/
@import 'colors';
// TODO - consider using more descriptive names, or replacing with more specific variables.
// Colors
$brand-color-1: #A9E1FF !default;
$brand-color-2: #82D2FF !default;
$brand-color-3: #3A97D3 !default;
$brand-color-4: #2277B3 !default;
$brand-color-5: #09457B !default;
$brand-color-1: $indigo10 !default;
$brand-color-2: $indigo20 !default;
$brand-color-3: $indigo40 !default;
$brand-color-4: $indigo60 !default;
$brand-color-5: $indigo80 !default;
$brand-primary: $brand-color-4 !default;
$gray-base: #292936 !default;
$gray-darker: #63637B !default;
$gray-dark: #9191A8 !default;
$gray: #ACACC0 !default;
$gray-light: #CACAD9 !default;
$gray-lighter: #E7E7EF !default;
$gray-lightest: #F4F4FA !default;
// Scaffolding
$body-bg: #fff !default;
// TODO - consider replacing $text-dark with $text-color
$text-dark: $gray-base !default;
$text-color: $gray-base !default;
$text-medium: $gray-darker !default;
$text-light: $gray-light !default;
/* Scaffolding */
$body-bg: $white !default;
$body-bg-secondary: $gray5 !default;
$body-bg-dark: $gray100 !default;
$divider: $gray15 !default;
$stroke: $gray20 !default;
$stroke-focus: $gray60 !default;
// Typography
$text-primary: $gray100 !default;
$text-secondary: $gray60 !default;
$text-light: $white !default;
$text-placeholder: $gray40;
$link-color: $brand-color-4;
$link-hover-color: $brand-color-5;
$font-family-body: "Open Sans", sans-serif !default;
$font-weight-body-regular: 400 !default;
$font-weight-body-semi-bold: 600 !default;
......@@ -62,6 +49,14 @@ $line-height-small: 1.5 !default;
$line-height-large: 1.5 !default;
// Badges
$badge-text-color: $text-primary;
$badge-danger-color: $sunset10;
$badge-primary-color: $cyan10;
$badge-success-color: $mint10;
$badge-warning-color: $yellow10;
// Buttons
$btn-border-radius-base: 4px;
......@@ -69,24 +64,47 @@ $btn-primary-bg: $brand-color-4 !default;
$btn-primary-bg-hover: $brand-color-5 !default;
$btn-primary-border: transparent !default;
$btn-primary-border-hover: transparent !default;
$btn-primary-color: white !default;
$btn-primary-color-hover: white !default;
$btn-primary-color: $white !default;
$btn-primary-color-hover: $white !default;
$btn-default-bg: $white !default;
$btn-default-bg-hover: $gray5 !default;
$btn-default-border: $gray20 !default;
$btn-default-border-hover: $gray30 !default;
$btn-default-color: $gray100 !default;
$btn-default-color-hover: $gray90 !default;
// Icons
$icon-bg: $gray20 !default;
$icon-bg-brand: $brand-color-3 !default;
$icon-bg-dark: $gray60 !default;
$icon-bg-disabled: $gray20 !default;
$btn-default-bg: white !default;
$btn-default-bg-hover: $gray-lightest;
$btn-default-border: $gray-light !default;
$btn-default-border-hover: $gray !default;
$btn-default-color: $gray-base !default;
// Header & Footer
$nav-bar-color: $indigo100;
$nav-bar-height: 60px;
$footer-height: 60px;
// List Group
$list-group-border: $gray-light !default;
$list-group-border: $stroke !default;
$list-group-border-radius: 0 !default;
// Labels
$label-primary-bg: $brand-color-3 !default;
// Tags
$tag-bg: $gray5;
$tag-bg-hover: $gray10;
// TODO Temp Colors
$resource-title-color: $indigo60;
// Spacing
$spacer-size: 8px;
$spacer-1: $spacer-size;
......
......@@ -5,6 +5,8 @@
@import 'dropdowns';
@import 'fonts';
@import 'icons';
@import 'inputs';
@import 'labels';
@import 'list-group';
@import 'pagination';
@import 'popovers';
......@@ -39,16 +41,6 @@ form {
margin-bottom: 0;
}
input {
&::-webkit-input-placeholder,
&::-moz-placeholder,
&:-ms-input-placeholder,
&:-moz-placeholder,
&::placeholder {
color: $gray-dark !important;
}
}
.truncated {
white-space: nowrap;
overflow: hidden;
......
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10" stroke-width="2"></circle><path d="M9.09 10a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12" y2="17"></line></svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24px" height="24px" viewBox="0 0 24 24" version="1.1">
<defs>
<linearGradient id="linear0" gradientUnits="userSpaceOnUse" x1="63.9997" y1="7.0336" x2="63.9997" y2="120.7894" gradientTransform="matrix(0.1875,0,0,0.1875,0,0)">
<stop offset="0" style="stop-color:rgb(26.27451%,52.941176%,99.215686%);stop-opacity:1;"/>
<stop offset="1" style="stop-color:rgb(27.45098%,51.372549%,91.764706%);stop-opacity:1;"/>
</linearGradient>
<clipPath id="clip1">
<path d="M 0 1 L 24 1 L 24 23 L 0 23 Z M 0 1 "/>
</clipPath>
<clipPath id="clip2">
<path d="M 5.210938 21.601562 L 0.289062 13.078125 C -0.0976562 12.410156 -0.0976562 11.589844 0.289062 10.921875 L 5.210938 2.398438 C 5.597656 1.730469 6.308594 1.320312 7.078125 1.320312 L 16.921875 1.320312 C 17.691406 1.320312 18.402344 1.730469 18.789062 2.398438 L 23.710938 10.921875 C 24.097656 11.589844 24.097656 12.410156 23.710938 13.078125 L 18.789062 21.601562 C 18.402344 22.269531 17.691406 22.679688 16.921875 22.679688 L 7.078125 22.679688 C 6.308594 22.679688 5.597656 22.269531 5.210938 21.601562 Z M 5.210938 21.601562 "/>
</clipPath>
<filter id="alpha" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">
<feColorMatrix type="matrix" in="SourceGraphic" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<mask id="mask0">
<g filter="url(#alpha)">
<rect x="0" y="0" width="24" height="24" style="fill:rgb(0%,0%,0%);fill-opacity:0.0705882;stroke:none;"/>
</g>
</mask>
<clipPath id="clip3">
<rect x="0" y="0" width="24" height="24"/>
</clipPath>
<g id="surface5" clip-path="url(#clip3)">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 22.355469 16.214844 L 15.117188 8.976562 L 12 8.140625 L 9.199219 9.183594 L 8.117188 12 L 8.988281 15.136719 L 16.664062 22.8125 L 18.292969 22.738281 Z M 22.355469 16.214844 "/>
</g>
</defs>
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear0);" d="M 5.210938 21.601562 L 0.289062 13.078125 C -0.0976562 12.410156 -0.0976562 11.589844 0.289062 10.921875 L 5.210938 2.398438 C 5.597656 1.730469 6.308594 1.320312 7.078125 1.320312 L 16.921875 1.320312 C 17.691406 1.320312 18.402344 1.730469 18.789062 2.398438 L 23.710938 10.921875 C 24.097656 11.589844 24.097656 12.410156 23.710938 13.078125 L 18.789062 21.601562 C 18.402344 22.269531 17.691406 22.679688 16.921875 22.679688 L 7.078125 22.679688 C 6.308594 22.679688 5.597656 22.269531 5.210938 21.601562 Z M 5.210938 21.601562 "/>
<g clip-path="url(#clip1)" clip-rule="nonzero">
<g clip-path="url(#clip2)" clip-rule="nonzero">
<use xlink:href="#surface5" mask="url(#mask0)"/>
</g>
</g>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 12 7.652344 C 9.597656 7.652344 7.652344 9.597656 7.652344 12 C 7.652344 14.402344 9.597656 16.347656 12 16.347656 C 14.402344 16.347656 16.347656 14.402344 16.347656 12 C 16.347656 9.597656 14.402344 7.652344 12 7.652344 M 12 15.300781 C 10.175781 15.300781 8.699219 13.824219 8.699219 12 C 8.699219 10.175781 10.175781 8.699219 12 8.699219 C 13.824219 8.699219 15.300781 10.175781 15.300781 12 C 15.300781 13.824219 13.824219 15.300781 12 15.300781 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 9.9375 11.832031 L 9.9375 13.183594 C 10.136719 13.527344 10.417969 13.816406 10.757812 14.023438 L 10.757812 11.832031 Z M 9.9375 11.832031 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 11.5625 10.691406 L 11.5625 14.332031 C 11.703125 14.359375 11.847656 14.375 11.992188 14.375 C 12.128906 14.375 12.257812 14.359375 12.386719 14.335938 L 12.386719 10.691406 Z M 11.5625 10.691406 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 13.269531 12.394531 L 13.269531 14 C 13.613281 13.78125 13.894531 13.476562 14.089844 13.117188 L 14.089844 12.394531 Z M 13.269531 12.394531 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 15.128906 14.679688 L 14.679688 15.128906 C 14.597656 15.207031 14.597656 15.339844 14.679688 15.417969 L 16.386719 17.125 C 16.46875 17.207031 16.597656 17.207031 16.675781 17.125 L 17.125 16.675781 C 17.207031 16.597656 17.207031 16.46875 17.125 16.386719 L 15.417969 14.679688 C 15.339844 14.601562 15.207031 14.601562 15.128906 14.679688 "/>
</g>
</svg>
This diff is collapsed.
<?xml version="1.0"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="432.071pt" height="445.383pt" viewBox="0 0 432.071 445.383" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<g id="orginal" style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
</g>
<g id="Layer_x0020_3" style="fill-rule:nonzero;clip-rule:nonzero;fill:none;stroke:#FFFFFF;stroke-width:12.4651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;">
<path style="fill:#000000;stroke:#000000;stroke-width:37.3953;stroke-linecap:butt;stroke-linejoin:miter;" d="M323.205,324.227c2.833-23.601,1.984-27.062,19.563-23.239l4.463,0.392c13.517,0.615,31.199-2.174,41.587-7c22.362-10.376,35.622-27.7,13.572-23.148c-50.297,10.376-53.755-6.655-53.755-6.655c53.111-78.803,75.313-178.836,56.149-203.322 C352.514-5.534,262.036,26.049,260.522,26.869l-0.482,0.089c-9.938-2.062-21.06-3.294-33.554-3.496c-22.761-0.374-40.032,5.967-53.133,15.904c0,0-161.408-66.498-153.899,83.628c1.597,31.936,45.777,241.655,98.47,178.31 c19.259-23.163,37.871-42.748,37.871-42.748c9.242,6.14,20.307,9.272,31.912,8.147l0.897-0.765c-0.281,2.876-0.157,5.689,0.359,9.019c-13.572,15.167-9.584,17.83-36.723,23.416c-27.457,5.659-11.326,15.734-0.797,18.367c12.768,3.193,42.305,7.716,62.268-20.224 l-0.795,3.188c5.325,4.26,4.965,30.619,5.72,49.452c0.756,18.834,2.017,36.409,5.856,46.771c3.839,10.36,8.369,37.05,44.036,29.406c29.809-6.388,52.6-15.582,54.677-101.107"/>
<path style="fill:#336791;stroke:none;" d="M402.395,271.23c-50.302,10.376-53.76-6.655-53.76-6.655c53.111-78.808,75.313-178.843,56.153-203.326c-52.27-66.785-142.752-35.2-144.262-34.38l-0.486,0.087c-9.938-2.063-21.06-3.292-33.56-3.496c-22.761-0.373-40.026,5.967-53.127,15.902 c0,0-161.411-66.495-153.904,83.63c1.597,31.938,45.776,241.657,98.471,178.312c19.26-23.163,37.869-42.748,37.869-42.748c9.243,6.14,20.308,9.272,31.908,8.147l0.901-0.765c-0.28,2.876-0.152,5.689,0.361,9.019c-13.575,15.167-9.586,17.83-36.723,23.416 c-27.459,5.659-11.328,15.734-0.796,18.367c12.768,3.193,42.307,7.716,62.266-20.224l-0.796,3.188c5.319,4.26,9.054,27.711,8.428,48.969c-0.626,21.259-1.044,35.854,3.147,47.254c4.191,11.4,8.368,37.05,44.042,29.406c29.809-6.388,45.256-22.942,47.405-50.555 c1.525-19.631,4.976-16.729,5.194-34.28l2.768-8.309c3.192-26.611,0.507-35.196,18.872-31.203l4.463,0.392c13.517,0.615,31.208-2.174,41.591-7c22.358-10.376,35.618-27.7,13.573-23.148z"/>
<path d="M215.866,286.484c-1.385,49.516,0.348,99.377,5.193,111.495c4.848,12.118,15.223,35.688,50.9,28.045c29.806-6.39,40.651-18.756,45.357-46.051c3.466-20.082,10.148-75.854,11.005-87.281"/>
<path d="M173.104,38.256c0,0-161.521-66.016-154.012,84.109c1.597,31.938,45.779,241.664,98.473,178.316c19.256-23.166,36.671-41.335,36.671-41.335"/>
<path d="M260.349,26.207c-5.591,1.753,89.848-34.889,144.087,34.417c19.159,24.484-3.043,124.519-56.153,203.329"/>
<path style="stroke-linejoin:bevel;" d="M348.282,263.953c0,0,3.461,17.036,53.764,6.653c22.04-4.552,8.776,12.774-13.577,23.155c-18.345,8.514-59.474,10.696-60.146-1.069c-1.729-30.355,21.647-21.133,19.96-28.739c-1.525-6.85-11.979-13.573-18.894-30.338 c-6.037-14.633-82.796-126.849,21.287-110.183c3.813-0.789-27.146-99.002-124.553-100.599c-97.385-1.597-94.19,119.762-94.19,119.762"/>
<path d="M188.604,274.334c-13.577,15.166-9.584,17.829-36.723,23.417c-27.459,5.66-11.326,15.733-0.797,18.365c12.768,3.195,42.307,7.718,62.266-20.229c6.078-8.509-0.036-22.086-8.385-25.547c-4.034-1.671-9.428-3.765-16.361,3.994z"/>
<path d="M187.715,274.069c-1.368-8.917,2.93-19.528,7.536-31.942c6.922-18.626,22.893-37.255,10.117-96.339c-9.523-44.029-73.396-9.163-73.436-3.193c-0.039,5.968,2.889,30.26-1.067,58.548c-5.162,36.913,23.488,68.132,56.479,64.938"/>
<path style="fill:#FFFFFF;stroke-width:4.155;stroke-linecap:butt;stroke-linejoin:miter;" d="M172.517,141.7c-0.288,2.039,3.733,7.48,8.976,8.207c5.234,0.73,9.714-3.522,9.998-5.559c0.284-2.039-3.732-4.285-8.977-5.015c-5.237-0.731-9.719,0.333-9.996,2.367z"/>
<path style="fill:#FFFFFF;stroke-width:2.0775;stroke-linecap:butt;stroke-linejoin:miter;" d="M331.941,137.543c0.284,2.039-3.732,7.48-8.976,8.207c-5.238,0.73-9.718-3.522-10.005-5.559c-0.277-2.039,3.74-4.285,8.979-5.015c5.239-0.73,9.718,0.333,10.002,2.368z"/>
<path d="M350.676,123.432c0.863,15.994-3.445,26.888-3.988,43.914c-0.804,24.748,11.799,53.074-7.191,81.435"/>
<path style="stroke-width:3;" d="M0,60.232"/>
</g>
</svg>
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 100 100" height="100px" version="1.1" viewBox="0 0 100 100" width="100px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Layer_1"><g><polygon fill="#205B98" points="50.002,67.462 75.166,73.552 75.166,26.305 50.002,32.397 "/><polygon fill="#5294CF" points="75.164,26.305 80,28.723 80,71.148 75.164,73.564 "/><polygon fill="#5294CF" points="50.002,67.462 24.836,73.552 24.836,26.305 50.002,32.397 "/><polygon fill="#205B98" points="24.836,26.305 20,28.723 20,71.148 24.836,73.564 "/><polygon fill="#5294CF" points="56.443,83.056 65.998,78.279 65.998,21.721 56.443,16.944 53.441,48.384 "/><polygon fill="#205B98" points="43.557,83.056 34.004,78.279 34.004,21.721 43.557,16.944 46.561,48.275 "/><rect fill="#2D72B8" height="66.111" width="12.887" x="43.557" y="16.944"/></g></g></svg>
\ No newline at end of file
......@@ -11,7 +11,7 @@
.post-header {
padding-right: 16px;
border-right: 1px solid $gray-lighter;
border-right: 1px solid $stroke;
.post-title {
width: 150px;
......
......@@ -71,45 +71,43 @@ export default class Feedback extends React.Component<FeedbackProps, FeedbackSta
};
render() {
const expandedClass = this.state.isOpen ? 'expanded' : 'collapsed';
return (
<div className={`feedback-component ${expandedClass}`}>
{
this.state.isOpen &&
<div>
<div className="feedback-header">
<div className="title">
{this.props.title.toUpperCase()}
<>
<button className={`btn btn-flat-icon feedback-icon${this.state.isOpen ? ' is-open' : ''}`} onClick={this.toggle}>
<img className='icon icon-help'/>
</button>
{
this.state.isOpen &&
<div className="feedback-component">
<div className="feedback-header">
<div className="title">
{this.props.title.toUpperCase()}
</div>
<button type="button" className="btn btn-close" aria-label={BUTTON_CLOSE_TEXT} onClick={this.toggle} />
</div>
<button type="button" className="btn btn-close" aria-label={BUTTON_CLOSE_TEXT} onClick={this.toggle} />
</div>
<div className="text-center">
<div className="btn-group" role="group" aria-label={FEEDBACK_TYPE_TEXT}>
<button type="button"
className={"btn btn-default" + (this.state.feedbackType === FeedbackType.Rating? " active": "")}
onClick={this.changeType(FeedbackType.Rating)}>
{RATING_TEXT}
</button>
<button type="button"
className={"btn btn-default" + (this.state.feedbackType === FeedbackType.Bug? " active": "")}
onClick={this.changeType(FeedbackType.Bug)}>
{BUG_REPORT_TEXT}
</button>
<button type="button"
className={"btn btn-default" + (this.state.feedbackType === FeedbackType.Request? " active": "")}
onClick={this.changeType(FeedbackType.Request)}>
{REQUEST_TEXT}
</button>
<div className="text-center">
<div className="btn-group" role="group" aria-label={FEEDBACK_TYPE_TEXT}>
<button type="button"
className={"btn btn-default" + (this.state.feedbackType === FeedbackType.Rating? " active": "")}
onClick={this.changeType(FeedbackType.Rating)}>
{RATING_TEXT}
</button>
<button type="button"
className={"btn btn-default" + (this.state.feedbackType === FeedbackType.Bug? " active": "")}
onClick={this.changeType(FeedbackType.Bug)}>
{BUG_REPORT_TEXT}
</button>
<button type="button"
className={"btn btn-default" + (this.state.feedbackType === FeedbackType.Request? " active": "")}
onClick={this.changeType(FeedbackType.Request)}>
{REQUEST_TEXT}
</button>
</div>
</div>
{this.state.content}
</div>
{this.state.content}
</div>
}
{
!(this.state.isOpen) &&
<img className='icon-speech' src='/static/images/icons/Speech.svg' onClick={this.toggle}/>
}
</div>
}
</>
);
}
}
@import 'variables';
.feedback-icon {
height: 32px;
width: 32px;
outline: 0 !important;
img {
margin: 4px;
background-color: $white;
}
&:hover,
&:focus,
&.is-open {
border-radius: 50%;
border: 6px solid $white;
img {
margin: -2px 0 0 -2px;
&.icon.icon-help {
background-color: white !important;
}
}
}
}
.feedback-component {
box-shadow: 0 0 24px -2px rgba(0, 0, 0, .2);
bottom: 75px;
top: $nav-bar-height + 4px;
position: fixed;
right: 25px;
right: 76px;
z-index: 2;
&.expanded {
background-color: white;
border-radius: 0 0 6px 6px;
border-top: 4px solid $brand-primary;
height: auto;
min-height: 450px;
padding: 32px;
width: 400px;
}
&.collapsed {
align-items: center;
background-color: $brand-primary;
border: 2px solid white;
border-radius: 50%; /* makes it a circle */
cursor: pointer;
display: flex;
height: 48px;
justify-content: center;
margin: 0;
width: 48px;
background-color: $white;
border-radius: 6px;
height: auto;
min-height: 450px;
padding: 32px;
width: 400px;
&:hover {
opacity: 0.5;
}
}
color: $text-primary;
.title {
color: $text-medium;
color: $text-secondary;
flex-grow: 1;
font-size: 12px;
font-family: $font-family-header;
......@@ -44,12 +50,6 @@
line-height: 32px;
}
img.icon-speech {
background-color: $brand-primary;
height: 36px;
width: 24px;
}
.feedback-header {
display: flex;
margin-bottom: 8px;
......@@ -99,7 +99,7 @@
text-align: center;
position: absolute;
font-size: 20px;
color: $text-medium;
color: $text-secondary;
/* for centering when parent has automatic height */
top: 50%;
left: 50%;
......@@ -112,12 +112,12 @@
}
input[type="text"] {
color: $text-medium !important;
color: $text-secondary !important;
}
textarea {
width: 100%;
color: $text-medium !important;
color: $text-secondary !important;
border: 1px solid $gray-lighter;
border-radius: 5px;
padding: 10px;
......
......@@ -83,7 +83,8 @@ describe('Feedback', () => {
describe('render', () => {
describe('if state.isOpen', () => {
let element;
let feedbackIcon;
let feedbackComponent;
let props;
let wrapper;
......@@ -98,17 +99,23 @@ describe('Feedback', () => {
changeTypeMockResult = jest.fn(() => {});
changeTypeSpy = jest.spyOn(wrapper.instance(), 'changeType').mockImplementation(() => changeTypeMockResult);
wrapper.update();
element = wrapper.children().at(0);
feedbackIcon = wrapper.children().at(0);
feedbackComponent = wrapper.children().at(1);
});
it('renders help button with correct props', () => {
expect(feedbackIcon.exists()).toBe(true);
expect(feedbackIcon.props().className).toEqual('btn btn-flat-icon feedback-icon is-open');
});
it('renders wrapper with correct className', () => {
expect(wrapper.props().className).toEqual('feedback-component expanded');
expect(feedbackComponent.props().className).toEqual('feedback-component');
});
describe('correct feedback-header', () => {
let button;
let title;
beforeAll(() => {
const header = element.children().at(0);
const header = feedbackComponent.children().at(0);
title = header.children().at(0);
button = header.children().at(1);
});
......@@ -130,7 +137,7 @@ describe('Feedback', () => {
let buttonGroupParent;
let buttonGroup;
beforeAll(() => {
buttonGroupParent = element.children().at(1);
buttonGroupParent = feedbackComponent.children().at(1);
buttonGroup = buttonGroupParent.children().at(0);
});
it('renders button group parent with correct className', () => {
......@@ -149,7 +156,7 @@ describe('Feedback', () => {
let button;
beforeAll(() => {
wrapper.setState({ feedbackType: FeedbackType.Rating });
button = wrapper.children().at(0).children().at(1).children().at(0).find('button').at(0);
button = wrapper.children().at(1).children().at(1).children().at(0).find('button').at(0);
});
it('has correct props if active', () => {
expect(button.props()).toMatchObject({
......@@ -165,7 +172,7 @@ describe('Feedback', () => {
it('has correct props if not active', () => {
wrapper.setState({ feedbackType: FeedbackType.Bug });
button = wrapper.children().at(0).children().at(1).children().at(0).find('button').at(0);
button = wrapper.children().at(1).children().at(1).children().at(0).find('button').at(0);
expect(button.props()).toMatchObject({
type: 'button',
className: 'btn btn-default',
......@@ -178,7 +185,7 @@ describe('Feedback', () => {
let button;
beforeAll(() => {
wrapper.setState({ feedbackType: FeedbackType.Bug });
button = wrapper.children().at(0).children().at(1).children().at(0).find('button').at(1);
button = wrapper.children().at(1).children().at(1).children().at(0).find('button').at(1);
});
it('has correct props if active', () => {
expect(button.props()).toMatchObject({
......@@ -194,7 +201,7 @@ describe('Feedback', () => {
it('has correct props if not active', () => {
wrapper.setState({ feedbackType: FeedbackType.Request });
button = wrapper.children().at(0).children().at(1).children().at(0).find('button').at(1);
button = wrapper.children().at(1).children().at(1).children().at(0).find('button').at(1);
expect(button.props()).toMatchObject({
type: 'button',
className: 'btn btn-default',
......@@ -207,7 +214,7 @@ describe('Feedback', () => {
let button;
beforeAll(() => {
wrapper.setState({ feedbackType: FeedbackType.Request });
button = wrapper.children().at(0).children().at(1).children().at(0).find('button').at(2);
button = wrapper.children().at(1).children().at(1).children().at(0).find('button').at(2);
});
it('has correct props if active', () => {
expect(button.props()).toMatchObject({
......@@ -223,7 +230,7 @@ describe('Feedback', () => {
it('has correct props if not active', () => {
wrapper.setState({ feedbackType: FeedbackType.Rating });
button = wrapper.children().at(0).children().at(1).children().at(0).find('button').at(2);
button = wrapper.children().at(1).children().at(1).children().at(0).find('button').at(2);
expect(button.props()).toMatchObject({
type: 'button',
className: 'btn btn-default',
......@@ -240,7 +247,7 @@ describe('Feedback', () => {
of its child form and refreshing it.
*/
it('renders state.content', () => {
expect(wrapper.children().at(0).children().at(2).debug()).toEqual('<Connect(RatingFeedbackForm) />');
expect(wrapper.children().at(1).children().at(2).debug()).toEqual('<Connect(RatingFeedbackForm) />');
});
afterAll(() => {
......@@ -256,16 +263,14 @@ describe('Feedback', () => {
props = setupResult.props;
wrapper = setupResult.wrapper;
});
it('renders wrapper with correct className', () => {
expect(wrapper.props().className).toEqual('feedback-component collapsed');
it('renders help button with correct props', () => {
const feedbackIcon = wrapper.children().at(0);
expect(feedbackIcon.exists()).toBe(true);
expect(feedbackIcon.props().className).toEqual('btn btn-flat-icon feedback-icon');
});
it('renders img correct props', () => {
expect(wrapper.find('img').props()).toMatchObject({
className: 'icon-speech',
src: '/static/images/icons/Speech.svg',
onClick: wrapper.instance().toggle,
});
it('does not render expanded form', () => {
expect(wrapper.children().at(1).exists()).toBe(false);
});
});
});
......
......@@ -2,16 +2,16 @@
/* TODO: Not the greatest sticky footer implementation */
.footer {
border-top: 1px solid $gray-lighter;
background-color: white;
color: $text-medium;
border-top: 1px solid $stroke;
background-color: $white;
color: $text-secondary;
font-size: 14px;
text-align: center;
padding: 20px;
position: fixed;
left: 0;
bottom: 0;
height: 60px;
height: $footer-height;
width: 100%;
}
......
......@@ -10,7 +10,7 @@ import MyBookmarks from 'components/common/Bookmark/MyBookmarks';
import PopularTables from 'components/common/PopularTables';
import { SearchAllReset } from 'ducks/search/types';
import { searchReset } from 'ducks/search/reducer';
import SearchBar from 'components/SearchPage/SearchBar';
import SearchBar from 'components/common/SearchBar';
import TagsList from 'components/common/TagsList';
......@@ -24,7 +24,7 @@ export class HomePage extends React.Component<HomePageProps> {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.searchReset();
}
......
......@@ -6,7 +6,7 @@ import { mapDispatchToProps, HomePage, HomePageProps } from '../';
import MyBookmarks from 'components/common/Bookmark/MyBookmarks';
import PopularTables from 'components/common/PopularTables';
import SearchBar from 'components/SearchPage/SearchBar';
import SearchBar from 'components/common/SearchBar';
import TagsList from 'components/common/TagsList';
import { getMockRouterProps } from 'fixtures/mockRouter';
......@@ -39,14 +39,14 @@ describe('HomePage', () => {
});
it('contains TagsList', () => {
expect(wrapper.find('#browse-tags-header').text()).toEqual('Browse Tags');
expect(wrapper.find('#browse-tags-header').text()).toEqual('Browse Tags');
expect(wrapper.contains(<TagsList />));
});
it('contains MyBookmarks', () => {
expect(wrapper.contains(<MyBookmarks />));
});
it('contains PopularTables', () => {
expect(wrapper.contains(<PopularTables />));
});
......
import * as React from 'react';
import * as Avatar from 'react-avatar';
import { RouteComponentProps } from 'react-router';
import { Link, NavLink, withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
......@@ -11,6 +12,11 @@ import { Dropdown } from 'react-bootstrap';
import { LoggedInUser } from 'interfaces';
import { feedbackEnabled } from 'config/config-utils';
import Feedback from 'components/Feedback';
import SearchBar from 'components/common/SearchBar';
import './styles.scss';
// Props
......@@ -18,7 +24,7 @@ interface StateFromProps {
loggedInUser: LoggedInUser;
}
export type NavBarProps = StateFromProps;
export type NavBarProps = StateFromProps & RouteComponentProps<{}>;
export class NavBar extends React.Component<NavBarProps> {
constructor(props) {
......@@ -36,6 +42,17 @@ export class NavBar extends React.Component<NavBarProps> {
});
}
renderSearchBar = () => {
if (this.props.location.pathname !== "/") {
return (
<div className="search-bar">
<SearchBar size="small" />
</div>
)
}
return null;
};
render() {
return (
<div className="container-fluid">
......@@ -50,8 +67,13 @@ export class NavBar extends React.Component<NavBarProps> {
<span className="title-3">AMUNDSEN</span>
</Link>
</div>
<div id="nav-bar-right" className="nav-bar-right">
{ this.renderSearchBar() }
<div id="nav-bar-right" className="ml-auto nav-bar-right">
{this.generateNavLinks(AppConfig.navLinks)}
{
feedbackEnabled() &&
<Feedback />
}
{
this.props.loggedInUser && AppConfig.indexUsers.enabled &&
<Dropdown id='user-dropdown' pullRight={true}>
......@@ -73,7 +95,7 @@ export class NavBar extends React.Component<NavBarProps> {
}
{
this.props.loggedInUser && !AppConfig.indexUsers.enabled &&
<div id="nav-bar-avatar">
<div id="nav-bar-avatar" className="nav-bar-avatar">
<Avatar name={this.props.loggedInUser.display_name} size={32} round={true} />
</div>
}
......@@ -91,4 +113,4 @@ export const mapStateToProps = (state: GlobalState) => {
}
};
export default withRouter(connect<StateFromProps>(mapStateToProps)(NavBar));
export default connect<StateFromProps>(mapStateToProps)(withRouter(NavBar));
@import 'variables';
.nav-bar {
height: 48px;
box-shadow: 0 2px 0 -1px $gray-lightest;
padding: 8px 32px 8px 32px;
color: $text-medium;
height: $nav-bar-height;
background: $nav-bar-color;
padding: 0px 32px 1px 32px;
color: $white;
display: flex;
flex-direction: row;
align-items: center;
}
.nav-bar sup {
font-size: 12px;
}
.title-3 {
color: $white;
}
.nav-bar a {
text-decoration: none;
color: $text-medium;
}
a {
display: inline-block;
height: 100%;
line-height: 32px;
text-decoration: none;
color: $white;
padding-left: 8px;
padding-right: 8px;
.nav-bar a:hover,
.nav-bar a:hover span {
color: $brand-color-4;
}
&:hover:not(.nav-bar-avatar-link),
&.active {
border-bottom: 4px solid $white;
}
.nav-bar a.active {
color: $brand-color-4;
}
&.nav-bar-avatar-link {
margin-top: -4px;
padding-left: 4px;
padding-right: 4px;
.nav-bar .nav-bar-right {
margin-left: auto;
display: flex;
}
.nav-bar-left > *,
.nav-bar-right > * {
margin: auto 10px;
}
.nav-bar-left > *:first-child {
margin-left: 0px;
}
.nav-bar-right > *:last-child {
margin-right: 0px;
}
.nav-bar-avatar {
height: 40px;
width: 40px;
padding: 4px;
&:hover {
/* override border-bottom style */
border-bottom: none;
/* circular background */
background-color: $white;
border-radius: 50%;
}
}
}
}
.nav-bar-left {
flex-basis: 234px;
}
.nav-bar-right {
margin-left: auto;
display: flex;
}
.nav-bar-left,
.nav-bar-right {
height: 100%;
padding-top: 12px;
}
.nav-bar-right > *:not(:last-child) {
margin-right: 12px;
}
.nav-bar .sb-avatar {
-webkit-box-shadow: 0px 0px 10px -1px rgba(0, 0, 0, .76);
-moz-box-shadow: 0px 0px 10px -1px rgba(0, 0, 0, .76);
box-shadow: 0px 0px 10px -1px rgba(0, 0, 0, .76);
.search-bar {
flex-grow: 1;
margin: auto 16px auto auto;
}
}
.logo-icon {
max-height: 32px;
max-width: 144px;
margin-right: 20px;
margin-right: 16px;
}
.avatar-dropdown {
border-style: none;
padding: 0 !important;
border-radius: 50%;
}
.profile-menu {
$profile-menu-width: 200px;
width: $profile-menu-width;
.profile-menu-header {
padding: 16px 16px 0 16px;
}
li {
padding: 16px;
a {
padding: 0;
}
}
}
.avatar-dropdown {
......
import * as React from 'react';
import * as Avatar from 'react-avatar';
import * as History from 'history';
import { shallow } from 'enzyme';
import { Dropdown } from 'react-bootstrap';
import { Link, NavLink } from 'react-router-dom';
import { NavBar, NavBarProps, mapStateToProps } from '../';
import { getMockRouterProps } from 'fixtures/mockRouter';
import Feedback from 'components/Feedback';
import SearchBar from 'components/common/SearchBar';
import { logClick } from "ducks/utilMethods";
jest.mock('ducks/utilMethods', () => {
......@@ -33,14 +38,16 @@ AppConfig.navLinks = [
}
];
AppConfig.indexUsers.enabled = true;
AppConfig.mailClientFeatures.feedbackEnabled = true;
import globalState from 'fixtures/globalState';
describe('NavBar', () => {
const setup = (propOverrides?: Partial<NavBarProps>) => {
const setup = (propOverrides?: Partial<NavBarProps>, location?: Partial<History.Location>) => {
const routerProps = getMockRouterProps<any>(null, location);
const props: NavBarProps = {
loggedInUser: globalState.user.loggedInUser,
...routerProps,
...propOverrides
};
const wrapper = shallow<NavBar>(<NavBar {...props} />);
......@@ -74,15 +81,34 @@ describe('NavBar', () => {
});
});
describe('renderSearchBar', () => {
it('returns small SearchBar when not on home page', () => {
const { props, wrapper } = setup(null, { pathname: "/search" });
const searchBar = shallow(wrapper.instance().renderSearchBar()).find(SearchBar);
expect(searchBar.exists()).toBe(true);
expect(searchBar.props()).toMatchObject({
size: "small",
});
});
it('returns null if conditions to render search bar are not met', () => {
const { props, wrapper } = setup(null, { pathname: "/" });
expect(wrapper.instance().renderSearchBar()).toBe(null);
});
});
describe('render', () => {
let element;
let props;
let wrapper;
let renderSearchBarSpy;
const spy = jest.spyOn(NavBar.prototype, 'generateNavLinks');
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
renderSearchBarSpy = jest.spyOn(wrapper.instance(), 'renderSearchBar');
wrapper.instance().forceUpdate();
});
it('renders img with AppConfig.logoPath', () => {
......@@ -108,6 +134,30 @@ describe('NavBar', () => {
expect(spy).toHaveBeenCalledWith(AppConfig.navLinks);
});
it('calls renderSearchBar', () => {
expect(renderSearchBarSpy).toHaveBeenCalled();
});
it('renders Feedback component', () => {
expect(wrapper.find(Feedback).exists()).toBe(true);
});
it('renders Avatar for loggedInUser', () => {
expect(wrapper.find(Avatar).props()).toMatchObject({
name: props.loggedInUser.display_name,
size: 32,
round: true,
})
});
it('renders a Link to the user profile if `indexUsers` is enabled', () => {
expect(wrapper.find('#nav-bar-avatar-link').exists()).toBe(true)
expect(wrapper.find('#nav-bar-avatar-link').props()).toMatchObject({
to: `/user/${props.loggedInUser.user_id}?source=navbar`
});
});
describe('if indexUsers is enabled', () => {
it('renders Avatar for loggedInUser inside of user dropdown', () => {
expect(wrapper.find(Dropdown).find(Dropdown.Toggle).find(Avatar).props()).toMatchObject({
......
import * as React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { TABLE_RESOURCE_TITLE, USER_RESOURCE_TITLE } from 'components/SearchPage/constants';
import AppConfig from 'config/config';
import { GlobalState } from 'ducks/rootReducer';
import {
DashboardSearchResults,
SetResourceRequest,
TableSearchResults,
UserSearchResults
} from 'ducks/search/types';
import { ResourceType } from 'interfaces/Resources';
import { setResource } from 'ducks/search/reducer';
export interface StateFromProps {
selectedTab: ResourceType,
tables: TableSearchResults;
dashboards: DashboardSearchResults;
users: UserSearchResults;
}
export interface DispatchFromProps {
setResource: (resource: ResourceType) => SetResourceRequest;
}
export type ResourceSelectorProps = StateFromProps & DispatchFromProps;
interface ResourceOptionConfig {
type: ResourceType;
label: string;
count: number;
}
export class ResourceSelector extends React.Component<ResourceSelectorProps > {
constructor(props) {
super(props);
}
onChange = (event) => {
this.props.setResource(event.target.value);
};
renderRadioOption = (option: ResourceOptionConfig, index: number) => {
return (
<div key={`resource-radio-item:${index}`} className="radio">
<label className="radio-label">
<input
type="radio"
name="resource"
value={ option.type }
checked={ this.props.selectedTab === option.type }
onChange={ this.onChange }
/>
<span className="subtitle-2">{ option.label }</span>
<span className="body-secondary-3 pull-right">{ option.count }</span>
</label>
</div>
);
};
render = () => {
const resourceOptions = [{
type: ResourceType.table,
label: TABLE_RESOURCE_TITLE,
count: this.props.tables.total_results,
}];
if (AppConfig.indexUsers.enabled) {
resourceOptions.push({
type: ResourceType.user,
label: USER_RESOURCE_TITLE,
count: this.props.users.total_results,
});
}
return (
<>
<div className="title-2">Resource</div>
{
resourceOptions.map((option, index) => this.renderRadioOption(option, index))
}
</>
);
}
}
export const mapStateToProps = (state: GlobalState) => {
return {
selectedTab: state.search.selectedTab,
tables: state.search.tables,
users: state.search.users,
dashboards: state.search.dashboards,
};
};
export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({ setResource }, dispatch);
};
export default connect<StateFromProps, DispatchFromProps>(mapStateToProps, mapDispatchToProps)(ResourceSelector);
import * as React from 'react';
import { shallow } from 'enzyme';
import {
mapDispatchToProps,
mapStateToProps,
ResourceSelector,
ResourceSelectorProps } from '../';
import { TABLE_RESOURCE_TITLE, USER_RESOURCE_TITLE } from 'components/SearchPage/constants';
import AppConfig from 'config/config';
import globalState from 'fixtures/globalState';
import { ResourceType } from 'interfaces/Resources';
describe('ResourceSelector', () => {
const setup = (propOverrides?: Partial<ResourceSelectorProps>) => {
const props = {
selectedTab: ResourceType.table,
tables: globalState.search.tables,
users: globalState.search.users,
dashboards: globalState.search.dashboards,
setResource: jest.fn(),
...propOverrides
};
const wrapper = shallow<ResourceSelector>(<ResourceSelector {...props} />);
return { props, wrapper };
};
describe('renderRadioOption', () => {
const { wrapper, props } = setup();
const instance = wrapper.instance();
const radioConfig = {
type: ResourceType.table,
label: TABLE_RESOURCE_TITLE,
count: 10,
};
const content = shallow(instance.renderRadioOption(radioConfig, 0));
it('renders an input with correct properties', () => {
const inputProps = content.find('input').props();
expect(inputProps.type).toEqual("radio");
expect(inputProps.name).toEqual("resource");
expect(inputProps.value).toEqual(radioConfig.type);
expect(inputProps.checked).toEqual(props.selectedTab === radioConfig.type);
expect(inputProps.onChange).toEqual(instance.onChange);
});
it('renders with the correct labels', () => {
expect(content.text()).toEqual(`${radioConfig.label}${radioConfig.count}`)
});
});
describe('onChange', () => {
it('calls setResource with the appropriate resource type', () => {
const mockEvent = {
target: {
value: ResourceType.table,
}
};
const { wrapper, props } = setup();
const setResourceSpy = jest.spyOn(props, "setResource")
wrapper.instance().onChange(mockEvent);
expect(setResourceSpy).toHaveBeenCalledWith(mockEvent.target.value)
});
});
describe('render', () => {
let props;
let wrapper;
let tableOptionConfig;
let userOptionConfig;
let renderRadioOptionSpy;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
tableOptionConfig = {
type: ResourceType.table,
label: TABLE_RESOURCE_TITLE,
count: props.tables.total_results,
};
userOptionConfig = {
type: ResourceType.user,
label: USER_RESOURCE_TITLE,
count: props.users.total_results,
};
renderRadioOptionSpy = jest.spyOn(wrapper.instance(), 'renderRadioOption');
});
it('renders the table resource option', () => {
renderRadioOptionSpy.mockClear();
wrapper.instance().render();
expect(renderRadioOptionSpy).toHaveBeenCalledWith(tableOptionConfig, 0);
});
it('renders the user resource option when enabled', () => {
AppConfig.indexUsers.enabled = true;
renderRadioOptionSpy.mockClear();
wrapper.instance().render();
expect(renderRadioOptionSpy).toHaveBeenCalledWith(userOptionConfig, 1);
});
it('does not render user resource option when disabled', () => {
AppConfig.indexUsers.enabled = false;
renderRadioOptionSpy.mockClear();
wrapper.instance().render();
expect(renderRadioOptionSpy).not.toHaveBeenCalledWith(userOptionConfig);
});
})
});
describe('mapStateToProps', () => {
let result;
beforeAll(() => {
result = mapStateToProps(globalState);
});
it('sets selectedTab on the props', () => {
expect(result.selectedTab).toEqual(globalState.search.selectedTab);
});
it('sets tables on the props', () => {
expect(result.tables).toEqual(globalState.search.tables);
});
it('sets users on the props', () => {
expect(result.users).toEqual(globalState.search.users);
});
it('sets dashboards on the props', () => {
expect(result.dashboards).toEqual(globalState.search.dashboards);
});
});
describe('mapDispatchToProps', () => {
let result;
let dispatch;
beforeAll(() => {
dispatch = jest.fn(() => Promise.resolve());
result = mapDispatchToProps(dispatch);
});
it('sets setResource on the props', () => {
expect(result.setResource).toBeInstanceOf(Function);
});
});
import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { GlobalState } from 'ducks/rootReducer';
import CheckBoxItem from 'components/common/Inputs/CheckBoxItem';
import './styles.scss'
interface SearchFilterInput {
value: string;
labelText: string;
checked: boolean;
count: number;
}
interface SearchFilterSection {
title: string;
categoryId: string;
inputProperties: SearchFilterInput[];
}
/*
TODO: Change on what becomes necessary for implementation
*/
export interface StateFromProps {
checkBoxSections: SearchFilterSection[];
}
/*
TODO: Delete if not necessary for final implementation
*/
export interface OwnProps {
}
/*
TODO: onFilterChange dispatched action to update filters. Consider:
1. Payload could contain categoryId and valueId and what to do with it
e.g. - {categoryId: 'datasets', value: 'hive', checked: false }
2. Disable component until implementing for user friendly debouncing
3. On success - Re-enable, checkedUI should update based on new state
4. On failure - Re-enable, state will not have been updated on failure,
checkedUI stays the same.
*/
export interface DispatchFromProps {
onFilterChange: () => any;
}
export type SearchFilterProps = StateFromProps & OwnProps & DispatchFromProps;
export class SearchFilter extends React.Component<SearchFilterProps> {
constructor(props) {
super(props);
}
createCheckBoxItem = (item: SearchFilterInput, categoryId: string, key: string) => {
const dummyMethod = () => { console.log('Dispatched') };
const { checked, count, labelText, value } = item;
return (
<CheckBoxItem
key={key}
checked={ checked }
disabled={ count === 0 }
name={ categoryId }
value={ value }
onChange={ dummyMethod }>
<span className="subtitle-2">{ labelText }</span>
<span className="body-secondary-3 pull-right">{ count }</span>
</CheckBoxItem>
);
};
createCheckBoxSection = (section: SearchFilterSection, key: string) => {
const { categoryId, inputProperties, title } = section;
return (
<div key={key} className="search-filter-section">
<div className="title-2">{ title }</div>
{ inputProperties.map((item, index) => this.createCheckBoxItem(item, categoryId, `item:${categoryId}:${index}`)) }
</div>
);
};
render = () => {
return this.props.checkBoxSections.map((section, index) => this.createCheckBoxSection(section, `section:${index}`));
};
};
/*
TODO: Process the global state however needed to get the necessary props
The dummy checkBoxSections property shape below is not expected to mirror how we store
filters and results in the globalState. Rather let checkBoxSections be the data shape
that works best for the component and mapStateToProps wiill translate globalState -> checkBoxSections.
*/
export const mapStateToProps = (state: GlobalState) => {
return {
checkBoxSections: [
{
title: 'Type', // category.displayName
categoryId: 'datasets', // category.id
inputProperties: [
{
value: 'bigquery', // value.id
labelText: 'BigQuery', // value.displayName
checked: true, // pull value or infer value from state
count: 100, // pull value from state
},
{
value: 'hive', // value.id
labelText: 'Hive', // value.displayName
checked: true, // pull value or infer value from state
count: 100, // pull value from state
},
{
value: 'druid', // value.id
labelText: 'Druid', // value.displayName
checked: true, // pull value or infer value from state
count: 0, // pull value from state
},
{
value: 's3', // value.id
labelText: 'S3 Buckets', // value.displayName
checked: false, // pull value or infer value from state
count: 100, // pull value from state
}
]
},
{
title: 'Badges', // category.displayName
categoryId: 'badges', // category.id
inputProperties: [
{
value: 'sla', // value.id
labelText: 'Missed SLA', // value.displayName
checked: true, // pull value or infer value from state
count: 3, // pull value from state
},
{
value: 'quality', // value.id
labelText: 'High Quality', // value.displayName
checked: true, // pull value or infer value from state
count: 12, // pull value from state
},
{
value: 'pii', // value.id
labelText: 'PII', // value.displayName
checked: false, // pull value or infer value from state
count: 34, // pull value from state
},
{
value: 'deprecated', // value.id
labelText: 'Deprecated', // value.displayName
checked: false, // pull value or infer value from state
count: 3, // pull value from state
}
]
}
]
};
};
/*
TODO: Dispatch a real action
*/
export const mapDispatchToProps = (dispatch: any) => {
// return bindActionCreators({ onFilterChange } , dispatch);
};
export default connect<StateFromProps, DispatchFromProps, OwnProps>(mapStateToProps)(SearchFilter);
.search-filter-section {
&:not(:first-child) {
margin-top: 24px;
}
}
import * as React from 'react';
import { shallow } from 'enzyme';
import { mapStateToProps, mapDispatchToProps, SearchFilter, SearchFilterProps} from '../';
import CheckBoxItem from 'components/common/Inputs/CheckBoxItem';
describe('SearchFilter', () => {
const setup = (propOverrides?: Partial<SearchFilterProps>) => {
const props = {
checkBoxSections: [
{
title: 'Type',
categoryId: 'datasets',
inputProperties: [
{
value: 'bigquery',
labelText: 'BigQuery',
checked: true,
count: 100,
},
{
value: 'hive',
labelText: 'Hive',
checked: true,
count: 100,
},
{
value: 'druid',
labelText: 'Druid',
checked: true,
count: 0,
},
{
value: 's3',
labelText: 'S3 Buckets',
checked: false,
count: 100,
}
]
}
],
onFilterChange: jest.fn(),
...propOverrides
};
const wrapper = shallow<SearchFilter>(<SearchFilter {...props} />);
return { props, wrapper };
};
describe('createCheckBoxItem', () => {
let props;
let wrapper;
let itemData;
let categoryId;
let content;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
itemData = props.checkBoxSections[0].inputProperties[0];
categoryId = 'testId'
content = shallow(wrapper.instance().createCheckBoxItem(itemData, categoryId, 'itemKey'));
});
/*
TODO: Enzyme might not allow this kind of check with shallow rendering.
Revisit on final implementation
it('returns CheckBoxItem with correct props', () => {
expect(content.type()).toEqual(CheckBoxItem);
});
*/
it('renders labelText as first CheckBoxItem child', () => {
const child = content.find('span').at(0);
expect(child.hasClass('subtitle-2')).toBe(true);
expect(child.text()).toEqual(itemData.labelText);
});
it('renders count as second CheckBoxItem child', () => {
const child = content.find('span').at(1);
expect(child.hasClass('body-secondary-3 pull-right')).toBe(true);
expect(child.text()).toEqual(itemData.count.toString());
});
});
describe('createCheckBoxSection', () => {
let props;
let wrapper;
let content;
let sectionData;
let createCheckBoxItemSpy;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
createCheckBoxItemSpy = jest.spyOn(wrapper.instance(), 'createCheckBoxItem');
sectionData = props.checkBoxSections[0];
content = shallow(wrapper.instance().createCheckBoxSection(sectionData, 'sectionKey'));
});
it('render content with correct class', () => {
expect(content.hasClass('search-filter-section')).toBe(true);
});
it('renders correct title', () => {
const title = content.children().at(0);
expect(title.hasClass('title-2')).toBe(true);
expect(title.text()).toEqual(sectionData.title);
});
it('calls createCheckBoxItem for each section.inputProperties', () => {
createCheckBoxItemSpy.mockClear();
wrapper.instance().createCheckBoxSection(sectionData, 'sectionKey');
const { categoryId } = sectionData;
sectionData.inputProperties.forEach((item, index ) => {
expect(createCheckBoxItemSpy).toHaveBeenCalledWith(item, categoryId, `item:${categoryId}:${index}`);
});
expect(createCheckBoxItemSpy).toHaveBeenCalledTimes(sectionData.inputProperties.length);
});
});
describe('render', () => {
let props;
let wrapper;
let createCheckBoxSectionSpy;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
createCheckBoxSectionSpy = jest.spyOn(wrapper.instance(), 'createCheckBoxSection');
});
it('calls createCheckBoxSection for each checkBoxSection', () => {
createCheckBoxSectionSpy.mockClear();
wrapper.instance().render();
props.checkBoxSections.forEach((section, index ) => {
expect(createCheckBoxSectionSpy).toHaveBeenCalledWith(section, `section:${index}`);
});
expect(createCheckBoxSectionSpy).toHaveBeenCalledTimes(props.checkBoxSections.length);
});
});
});
describe('mapStateToProps', () => {
// TODO
});
describe('mapDispatchToProps', () => {
// TODO
});
import * as React from 'react';
import './styles.scss';
const SearchPanel: React.SFC = ({ children }) => {
return (
<div className="search-control-panel">
{
React.Children.map(children, (child, index) => {
return (
<div key={`search-panel-child:${index}`} className="section">
{ child }
</div>
);
})
}
</div>
);
};
export default SearchPanel;
@import 'variables';
.search-control-panel {
border-right: 4px solid $stroke;
flex: 0 0 270px;
display: flex;
flex-direction: column;
.section {
padding: 32px 24px 32px 32px;
&:not(:first-child) {
border-top: 2px solid $stroke;
}
}
.radio {
margin-top: 16px;
margin-bottom: 16px;
.radio-label {
display: block;
}
}
}
import * as React from 'react';
import { shallow } from 'enzyme';
import SearchPanel from '../';
describe('SearchPanel', () => {
const resourceChild = (<div>I am a resource selector</div>);
const filterChild = (<div>I am a a set of filters</div>);
const setup = () => {
const wrapper = shallow(
<SearchPanel>
{ resourceChild }
{ filterChild }
</SearchPanel>
);
return { wrapper };
};
describe('render', () => {
let wrapper;
beforeAll(() => {
wrapper = setup().wrapper;
});
it('renders itself with correct class', () => {
expect(wrapper.hasClass('search-control-panel')).toBe(true);
})
it('renders its children with correct class', () => {
wrapper.children().forEach((child) => {
expect(child.props().className).toEqual('section');
});
})
it('renders expected children', () => {
const children = wrapper.children();
expect(children.at(0).contains(resourceChild)).toBe(true);
expect(children.at(1).contains(filterChild)).toBe(true);
});
});
});
......@@ -6,13 +6,10 @@ export const DOCUMENT_TITLE_SUFFIX = ' - Amundsen Search';
export const PAGE_INDEX_ERROR_MESSAGE = 'Page index out of bounds for available matches';
export const SEARCH_INFO_TEXT_BASE = `Ordered by the relevance of matches within a resource's metadata`;
export const SEARCH_INFO_TEXT_TABLE_SUFFIX = ', as well as overall usage';
export const SEARCH_SOURCE_NAME = 'search_results';
export const SEARCH_ERROR_MESSAGE_INFIX = ' - did not match any ';
export const SEARCH_ERROR_MESSAGE_PREFIX = 'Your search - ';
export const SEARCH_ERROR_MESSAGE_SUFFIX = ' result';
export const SEARCH_ERROR_MESSAGE_SUFFIX = ' results';
export const TABLE_RESOURCE_TITLE = 'Tables';
export const USER_RESOURCE_TITLE = 'Users';
export const TABLE_RESOURCE_TITLE = 'Datasets';
export const USER_RESOURCE_TITLE = 'People';
......@@ -5,12 +5,10 @@ import * as DocumentTitle from 'react-document-title';
import { RouteComponentProps } from 'react-router';
import { Search as UrlSearch } from 'history';
import AppConfig from 'config/config';
import LoadingSpinner from 'components/common/LoadingSpinner';
import InfoButton from 'components/common/InfoButton';
import ResourceList from 'components/common/ResourceList';
import TabsComponent from 'components/common/Tabs';
import SearchBar from './SearchBar';
import ResourceSelector from './ResourceSelector';
import SearchPanel from './SearchPanel';
import { GlobalState } from 'ducks/rootReducer';
import { setPageIndex, setResource, urlDidUpdate } from 'ducks/search/reducer';
......@@ -25,7 +23,6 @@ import {
} from 'ducks/search/types';
import { Resource, ResourceType } from 'interfaces';
// TODO: Use css-modules instead of 'import'
import './styles.scss';
......@@ -36,13 +33,12 @@ import {
SEARCH_ERROR_MESSAGE_INFIX,
SEARCH_ERROR_MESSAGE_PREFIX,
SEARCH_ERROR_MESSAGE_SUFFIX,
SEARCH_INFO_TEXT_BASE,
SEARCH_INFO_TEXT_TABLE_SUFFIX,
SEARCH_SOURCE_NAME,
TABLE_RESOURCE_TITLE,
USER_RESOURCE_TITLE,
} from './constants';
export interface StateFromProps {
searchTerm: string;
selectedTab: ResourceType;
......@@ -78,40 +74,15 @@ export class SearchPage extends React.Component<SearchPageProps> {
}
renderSearchResults = () => {
const tabConfig = [
{
title: `${TABLE_RESOURCE_TITLE} (${ this.props.tables.total_results })`,
key: ResourceType.table,
content: this.getTabContent(this.props.tables, ResourceType.table),
},
];
if (AppConfig.indexUsers.enabled) {
tabConfig.push({
title: `Users (${ this.props.users.total_results })`,
key: ResourceType.user,
content: this.getTabContent(this.props.users, ResourceType.user),
})
}
return (
<div>
<TabsComponent
tabs={ tabConfig }
defaultTab={ ResourceType.table }
activeKey={ this.props.selectedTab }
onSelect={ this.props.setResource }
/>
</div>
);
};
generateInfoText = (tab: ResourceType): string => {
switch (tab) {
switch(this.props.selectedTab) {
case ResourceType.table:
return `${SEARCH_INFO_TEXT_BASE}${SEARCH_INFO_TEXT_TABLE_SUFFIX}`;
default:
return SEARCH_INFO_TEXT_BASE;
return this.getTabContent(this.props.tables, ResourceType.table);
case ResourceType.user:
return this.getTabContent(this.props.users, ResourceType.user);
case ResourceType.dashboard:
return this.getTabContent(this.props.dashboards, ResourceType.dashboard);
}
return null;
};
generateTabLabel = (tab: ResourceType): string => {
......@@ -129,7 +100,6 @@ export class SearchPage extends React.Component<SearchPageProps> {
const { searchTerm } = this.props;
const { page_index, total_results } = results;
const startIndex = (RESULTS_PER_PAGE * page_index) + 1;
const endIndex = RESULTS_PER_PAGE * (page_index + 1);
const tabLabel = this.generateTabLabel(tab);
// TODO - Move error messages into Tab Component
......@@ -155,14 +125,8 @@ export class SearchPage extends React.Component<SearchPageProps> {
)
}
const title =`${startIndex}-${Math.min(endIndex, total_results)} of ${total_results} results`;
const infoText = this.generateInfoText(tab);
return (
<div className="search-list-container">
<div className="search-list-header">
<label>{ title }</label>
<InfoButton infoText={infoText}/>
</div>
<ResourceList
slicedItems={ results.results }
slicedItemsCount={ total_results }
......@@ -172,7 +136,7 @@ export class SearchPage extends React.Component<SearchPageProps> {
onPagination={ this.props.setPageIndex }
/>
</div>
);
);
};
renderContent = () => {
......@@ -185,12 +149,12 @@ export class SearchPage extends React.Component<SearchPageProps> {
render() {
const { searchTerm } = this.props;
const innerContent = (
<div className="container search-page">
<div className="row">
<div className="col-xs-12 col-md-offset-1 col-md-10">
<SearchBar />
{ this.renderContent() }
</div>
<div className="search-page">
<SearchPanel>
<ResourceSelector/>
</SearchPanel>
<div className="search-results">
{ this.renderContent() }
</div>
</div>
);
......
@import 'variables';
.search-page {
.tabs-component,
.search-list-container {
margin-top: 32px;
display: flex;
min-height: calc(100% - #{$nav-bar-height + $footer-height});
.list-group {
margin-top: 0px;
}
@media (max-width: $screen-sm-max) {
.tabs-component,
.search-list-container {
margin-top: 16px;
}
.search-results {
flex-grow: 1;
width: 0; // Temporary hack since list-items use width %
}
.tab-content {
.search-list-header {
display: flex;
flex-direction: row;
label {
color: $text-medium;
font-size: 18px;
}
@media (max-width: $screen-sm-max) {
.search-error {
margin-top: 16px;
}
}
.search-error {
text-align: center;
margin-top: 32px;
}
}
......@@ -4,7 +4,6 @@ import * as History from 'history';
import { shallow } from 'enzyme';
import AppConfig from 'config/config';
import { ResourceType } from 'interfaces';
import { mapDispatchToProps, mapStateToProps, SearchPage, SearchPageProps } from '../';
import {
......@@ -14,21 +13,15 @@ import {
SEARCH_ERROR_MESSAGE_INFIX,
SEARCH_ERROR_MESSAGE_PREFIX,
SEARCH_ERROR_MESSAGE_SUFFIX,
SEARCH_INFO_TEXT_BASE,
SEARCH_INFO_TEXT_TABLE_SUFFIX,
SEARCH_SOURCE_NAME,
TABLE_RESOURCE_TITLE,
USER_RESOURCE_TITLE,
} from '../constants';
import InfoButton from 'components/common/InfoButton';
import TabsComponent from 'components/common/Tabs';
import SearchBar from '../SearchBar';
import LoadingSpinner from 'components/common/LoadingSpinner';
import SearchPanel from 'components/SearchPage/SearchPanel';
import ResourceList from 'components/common/ResourceList';
import globalState from 'fixtures/globalState';
import { getMockRouterProps } from 'fixtures/mockRouter';
......@@ -106,24 +99,6 @@ describe('SearchPage', () => {
});
});
describe('generateInfoText', () => {
let wrapper;
beforeAll(() => {
wrapper = setup().wrapper;
});
it('returns correct text for ResourceType.table', () => {
const text = wrapper.instance().generateInfoText(ResourceType.table);
const expectedText = `${SEARCH_INFO_TEXT_BASE}${SEARCH_INFO_TEXT_TABLE_SUFFIX}`;
expect(text).toEqual(expectedText);
});
it('returns correct text for the default case', () => {
const text = wrapper.instance().generateInfoText(ResourceType.user);
expect(text).toEqual(SEARCH_INFO_TEXT_BASE);
});
});
describe('generateTabLabel', () => {
let wrapper;
beforeAll(() => {
......@@ -151,14 +126,16 @@ describe('SearchPage', () => {
describe('if searchTerm but no results', () => {
it('renders expected search error message', () => {
const { props, wrapper } = setup({ searchTerm: 'data' });
const searchTerm = 'data';
const { props, wrapper } = setup({ searchTerm });
const testResults = {
page_index: 0,
results: [],
total_results: 0,
};
content = shallow(wrapper.instance().getTabContent(testResults, ResourceType.table));
expect(content.children().at(0).text()).toEqual(`${SEARCH_ERROR_MESSAGE_PREFIX}data${SEARCH_ERROR_MESSAGE_INFIX}tables${SEARCH_ERROR_MESSAGE_SUFFIX}`);
const message = `${SEARCH_ERROR_MESSAGE_PREFIX}${searchTerm}${SEARCH_ERROR_MESSAGE_INFIX}${TABLE_RESOURCE_TITLE.toLowerCase()}${SEARCH_ERROR_MESSAGE_SUFFIX}`;
expect(content.children().at(0).text()).toEqual(message);
});
});
......@@ -184,20 +161,9 @@ describe('SearchPage', () => {
props = setupResult.props;
wrapper = setupResult.wrapper;
generateInfoTextMockResults = 'test info text';
jest.spyOn(wrapper.instance(), 'generateInfoText').mockImplementation(() => generateInfoTextMockResults);
content = shallow(wrapper.instance().getTabContent(props.tables, ResourceType.table));
});
it('renders correct label for content', () => {
expect(content.children().at(0).find('label').text()).toEqual(`1-1 of 1 results`);
});
it('renders InfoButton with correct props', () => {
expect(content.children().at(0).find(InfoButton).props()).toMatchObject({
infoText: generateInfoTextMockResults,
});
});
it('renders ResourceList with correct props', () => {
const { props, wrapper } = setup();
const testResults = {
......@@ -232,42 +198,41 @@ describe('SearchPage', () => {
});
describe('renderSearchResults', () => {
let props;
let wrapper;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
it('renders the correct content for table resources', () => {
const { props, wrapper } = setup({
selectedTab: ResourceType.table
});
const getTabContentSpy = jest.spyOn(wrapper.instance(), 'getTabContent');
shallow(wrapper.instance().renderSearchResults());
expect(getTabContentSpy).toHaveBeenCalledWith(props.tables, ResourceType.table);
});
it('renders TabsComponent with correct props', () => {
AppConfig.indexUsers.enabled = false;
const content = shallow(wrapper.instance().renderSearchResults());
const tabProps = content.find(TabsComponent).props();
expect(tabProps.activeKey).toEqual(props.selectedTab);
expect(tabProps.defaultTab).toEqual(ResourceType.table);
expect(tabProps.onSelect).toEqual(props.setResource);
const firstTab = tabProps.tabs[0];
expect(firstTab.key).toEqual(ResourceType.table);
expect(firstTab.title).toEqual(`${TABLE_RESOURCE_TITLE} (${props.tables.total_results})`);
expect(firstTab.content).toEqual(wrapper.instance().getTabContent(props.tables, firstTab.key));
it('renders the correct content for user resources', () => {
const { props, wrapper } = setup({
selectedTab: ResourceType.user
});
const getTabContentSpy = jest.spyOn(wrapper.instance(), 'getTabContent');
shallow(wrapper.instance().renderSearchResults());
expect(getTabContentSpy).toHaveBeenCalledWith(props.users, ResourceType.user);
});
it('renders only one tab if people is disabled', () => {
AppConfig.indexUsers.enabled = false;
const content = shallow(wrapper.instance().renderSearchResults());
const tabConfig = content.find(TabsComponent).props().tabs;
expect(tabConfig.length).toEqual(1)
it('renders the correct content for dashboard resources', () => {
const { props, wrapper } = setup({
selectedTab: ResourceType.dashboard
});
const getTabContentSpy = jest.spyOn(wrapper.instance(), 'getTabContent');
shallow(wrapper.instance().renderSearchResults());
expect(getTabContentSpy).toHaveBeenCalledWith(props.dashboards, ResourceType.dashboard);
});
it('renders two tabs if indexUsers is enabled', () => {
AppConfig.indexUsers.enabled = true;
const content = shallow(wrapper.instance().renderSearchResults());
const tabConfig = content.find(TabsComponent).props().tabs;
expect(tabConfig.length).toEqual(2)
it('renders null for an invalid selectedTab', () => {
const { props, wrapper } = setup({
selectedTab: null
});
const renderedSearchResults = wrapper.instance().renderSearchResults();
expect(renderedSearchResults).toBe(null);
});
});
describe('render', () => {
......@@ -285,11 +250,6 @@ describe('SearchPage', () => {
});
});
it('renders SearchBar with correct props', () => {
const { props, wrapper } = setup();
expect(wrapper.find(SearchBar).exists()).toBeTruthy();
});
it('calls renderSearchResults if searchTerm is not empty string', () => {
const { props, wrapper } = setup({ searchTerm: 'test search' });
const renderSearchResultsSpy = jest.spyOn(wrapper.instance(), 'renderSearchResults');
......@@ -297,6 +257,11 @@ describe('SearchPage', () => {
expect(renderSearchResultsSpy).toHaveBeenCalled();
});
});
it('renders a search panel', () => {
const {props, wrapper} = setup();
expect(wrapper.find(SearchPanel).exists()).toBe(true);
});
});
describe('mapDispatchToProps', () => {
......
......@@ -24,7 +24,7 @@
flex-grow: 1;
}
.grid-header.grid-cell {
background-color: $gray-lighter;
background-color: $body-bg-secondary;
height: 40px;
}
.grid-cell {
......@@ -38,7 +38,7 @@
height: 32px;
text-align: center;
white-space: nowrap;
border-bottom: 1px solid $gray-light;
border-bottom: 1px solid $stroke;
}
}
}
......
......@@ -6,8 +6,8 @@
flex-direction: column;
/* tmp fixes until we refactor/settle on styles */
cursor: default;
border-top-color: $gray-lighter !important;
border-bottom-color: $gray-lighter !important;
border-top-color: $stroke !important;
border-bottom-color: $stroke !important;
background-color: transparent !important;
padding: 10px 4px;
......@@ -61,8 +61,6 @@
cursor: pointer;
&:hover {
background-image: linear-gradient($gray-lightest, $gray-lightest, white);
.icon {
background-color: $brand-color-4;
}
......@@ -84,7 +82,7 @@
}
.stat-collection-info {
color: $text-medium;
color: $text-secondary;
font-style: italic;
margin-top: 4px;
}
......@@ -107,7 +105,7 @@
padding: 4px;
margin-right: 5px;
.icon {
background-color: $gray-light;
background-color: $stroke;
height: 14px;
-webkit-mask-size: 14px;
mask-size: 14px;
......@@ -116,9 +114,9 @@
}
&:hover,
&:focus {
background-color: $gray-lightest;
background-color: $body-bg-secondary;
.icon {
background-color: $gray-base;
background-color: $body-bg-dark;
}
}
}
......
......@@ -27,10 +27,10 @@
&:focus,
&:hover {
background-color: $gray-lightest;
background-color: $body-bg-secondary;
.btn.delete-button {
background-color: $gray-lightest;
background-color: $body-bg-secondary;
display: block
}
}
......@@ -75,7 +75,7 @@
margin-bottom: 16px;
}
input {
border: 1px solid $gray-lighter;
border: 1px solid $stroke;
border-radius: 4px;
outline: none;
padding: 4px;
......
......@@ -10,7 +10,7 @@
z-index: 6;
&.expanded {
background-color: white;
background-color: $white;
height: auto;
min-height: 450px;
padding: 32px;
......@@ -31,7 +31,7 @@
input,
textarea {
color: $text-medium;
color: $text-secondary;
}
input[type="checkbox"] {
margin-right: 8px;
......
@import 'variables';
.btn.tag-button {
background-color: $gray-lightest;
background-color: $tag-bg;
border: 0;
border-radius: 4px;
color: $text-dark;
color: $text-primary;
margin: 4px;
overflow: hidden;
padding: 8px;
......@@ -19,12 +19,12 @@
&:hover,
&:focus {
color: $text-dark;
background-color: $gray-lighter;
color: $text-primary;
background-color: $tag-bg-hover;
}
.tag-count {
color: $text-medium;
color: $text-secondary;
margin-left: 8px;
}
}
......@@ -17,12 +17,12 @@
}
.amundsen__multi-value {
background-color: $gray-lighter !important;
background-color: $tag-bg !important;
border-radius: 4px;
.amundsen__multi-value__label {
border-radius: 4px 0 0 4px;
color: $text-dark;
color: $text-primary;
line-height: 14px;
padding: 8px;
}
......@@ -33,8 +33,8 @@
&:hover,
&:focus {
background-color: $gray-light !important;
color: $text-dark;
background-color: $tag-bg-hover !important;
color: $text-primary;
}
}
}
......
......@@ -26,7 +26,7 @@
&:hover,
&:focus {
background-color: $gray-lighter;
background-color: $body-bg-secondary;
}
.icon {
......@@ -36,7 +36,7 @@
&,
&:hover,
&:focus {
background-color: $gray-light !important;
background-color: $stroke !important;
}
}
......
......@@ -3,7 +3,7 @@
.editable-container {
display: flex;
font-size: 16px;
color: $text-medium;
color: $text-secondary;
&:hover {
.edit-link {
......@@ -40,6 +40,6 @@
.editable-textarea:focus-within {
outline: none;
background: white;
background: $white;
}
}
......@@ -27,12 +27,12 @@ button.edit-button {
width: 24px;
border: none;
border-radius: 5px;
background-color: $gray-light;
background-color: $icon-bg;
-webkit-mask-image: url('/static/images/icons/Edit.svg');
mask-image: url('/static/images/icons/Edit.svg');
}
button.edit-button:hover {
background-color: $brand-color-4;
background-color: $icon-bg-brand;
}
button.active-edit-button {
height: 24px;
......
@import 'variables';
.flag {
height: 20px;
display: inline-block;
margin: 4px 4px 4px 8px;
margin: 0px 8px;
font-size: 14px;
border-radius: 5px;
}
.label {
font-weight: normal;
border-radius: 10px;
}
@import 'variables';
.flash-message {
background-color: #292936; // $gray100
background-color: $body-bg-dark;
border-radius: 6px;
display: flex;
color: white;
color: $white;
height: 56px;
padding: $spacer-2 $spacer-1;
......
......@@ -6,7 +6,7 @@ button.info-button {
margin: 0 0 0 8px;
padding: 0;
background-color: $gray-light;
background-color: $stroke;
border: none;
mask-image: url('/static/images/icons/Info.svg');
-webkit-mask-image: url('/static/images/icons/Info.svg');
......
import * as React from 'react';
import { connect } from 'react-redux';
import './styles.scss';
export interface CheckBoxItemProps {
checked: boolean;
disabled?: boolean
name: string;
onChange: (e: React.FormEvent<HTMLInputElement>) => any;
value: string;
}
const CheckBoxItem: React.SFC<CheckBoxItemProps> = ({ checked, disabled = false, name, onChange, value, children }) => {
return (
<div className="checkbox">
<label className="checkbox-label">
<input
type="checkbox"
checked={ checked }
disabled={ disabled }
name={ name }
onChange={ onChange }
value={ value }
/>
{ children }
</label>
</div>
);
};
export default CheckBoxItem;
.checkbox {
margin-top: 16px;
margin-bottom: 16px;
.checkbox-label {
display: block;
width: 100%;
input:not([disabled]) {
cursor: pointer;
}
}
}
import * as React from 'react';
import { shallow } from 'enzyme';
import CheckBoxItem, { CheckBoxItemProps } from '../';
describe('CheckBoxItem', () => {
const expectedChild = (<span>I am a child</span>);
const setup = (propOverrides?: Partial<CheckBoxItemProps>) => {
const props: CheckBoxItemProps = {
checked: true,
disabled: false,
name: 'test',
value: 'testMethod',
onChange: jest.fn(),
...propOverrides
};
const wrapper = shallow(
<CheckBoxItem {...props}>
{ expectedChild }
</CheckBoxItem>
);
return { props, wrapper };
};
describe('render', () => {
let props;
let wrapper;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
});
it('renders itself with correct class', () => {
expect(wrapper.hasClass('checkbox')).toBe(true);
});
it('renders itself with correct class', () => {
expect(wrapper.find('label').hasClass('checkbox-label')).toBe(true);
});
it('renders input with correct props', () => {
const input = wrapper.find('label').children().at(0);
expect(input.exists()).toBe(true);
expect(input.props()).toEqual({
type: 'checkbox',
checked: props.checked,
disabled: props.disabled,
name: props.name,
onChange: props.onChange,
value: props.value
});
});
it('renders input with default value for disabled if not provided', () => {
const wrapper = setup({ disabled: undefined }).wrapper;
expect(wrapper.find('input').props().disabled).toEqual(false);
});
it('renders expected children after input', () => {
const labelChildren = wrapper.find('label').children();
expect(labelChildren.at(1).contains(expectedChild)).toBe(true);
});
});
});
......@@ -7,6 +7,8 @@ import { TableResource } from 'interfaces';
import BookmarkIcon from 'components/common/Bookmark/BookmarkIcon';
import { getDatabaseDisplayName, getDatabaseIconClass } from 'config/config-utils';
export interface TableListItemProps {
table: TableResource;
logging: LoggingParams;
......@@ -29,6 +31,10 @@ class TableListItem extends React.Component<TableListItemProps, {}> {
+ `?index=${logging.index}&source=${logging.source}`;
};
generateResourceIconClass = (databaseId: string): string => {
return `icon resource-icon ${getDatabaseIconClass(databaseId)}`;
};
render() {
const { table } = this.props;
const hasLastUpdated = !!table.last_updated_epoch;
......@@ -36,9 +42,9 @@ class TableListItem extends React.Component<TableListItemProps, {}> {
return (
<li className="list-group-item">
<Link className="resource-list-item table-list-item" to={ this.getLink() }>
<img className="icon icon-database icon-color" />
<div className="content">
<div className="col-sm-6 col-md-8">
<div className="resource-info">
<img className={this.generateResourceIconClass(table.database)} />
<div className="resource-info-text">
<div className="resource-name title-2">
<div className="truncated">
{ `${table.schema_name}.${table.name}`}
......@@ -47,20 +53,22 @@ class TableListItem extends React.Component<TableListItemProps, {}> {
</div>
<div className="body-secondary-3 truncated">{ table.description }</div>
</div>
<div className="resource-type hidden-xs col-sm-3 col-md-2 text-center">
{ table.database }
</div>
</div>
<div className="resource-type">
{ getDatabaseDisplayName(table.database) }
</div>
<div className="resource-badges">
{
hasLastUpdated &&
<div className="hidden-xs col-sm-3 col-md-2">
<div>
<div className="title-3">Last Updated</div>
<div className="body-secondary-3 truncated">
<div className="body-secondary-3">
{ this.getDateLabel() }
</div>
</div>
}
<img className="icon icon-right" />
</div>
<img className="icon icon-right" />
</Link>
</li>
);
......
......@@ -7,6 +7,20 @@ import BookmarkIcon from 'components/common/Bookmark/BookmarkIcon';
import TableListItem, { TableListItemProps } from '../';
import { ResourceType } from 'interfaces';
import * as ConfigUtils from 'config/config-utils';
const MOCK_DISPLAY_NAME = 'displayName';
const MOCK_ICON_CLASS = 'test-class';
jest.mock('config/config-utils', () => (
{
getDatabaseDisplayName: () => { return MOCK_DISPLAY_NAME },
getDatabaseIconClass: () => { return MOCK_ICON_CLASS }
}
));
const getDBIconClassSpy = jest.spyOn(ConfigUtils, 'getDatabaseIconClass');
describe('TableListItem', () => {
const setup = (propOverrides?: Partial<TableListItemProps>) => {
const props: TableListItemProps = {
......@@ -27,6 +41,39 @@ describe('TableListItem', () => {
return { props, wrapper };
};
/* Note: Jest is configured to use UTC */
describe('getDateLabel', () => {
it('getDateLabel returns correct string', () => {
const { props, wrapper } = setup();
expect(wrapper.instance().getDateLabel()).toEqual('Mar 29, 2019');
});
});
describe('getLink', () => {
it('getLink returns correct string', () => {
const { props, wrapper } = setup();
const { table, logging } = props;
expect(wrapper.instance().getLink()).toEqual(`/table_detail/${table.cluster}/${table.database}/${table.schema_name}/${table.name}?index=${logging.index}&source=${logging.source}`);
});
});
describe('generateResourceIconClass', () => {
let wrapper;
beforeAll(() => {
wrapper = setup().wrapper;
});
it('calls getDatabaseIconClass with given database id', () => {
const testValue = 'noEffectOnTest';
const iconClass = wrapper.instance().generateResourceIconClass(testValue);
expect(getDBIconClassSpy).toHaveBeenCalledWith(testValue);
});
it('returns the default classes with the correct icon class appended', () => {
const iconClass = wrapper.instance().generateResourceIconClass('noEffectOnTest');
expect(iconClass).toEqual(`icon resource-icon test-class`);
});
});
describe('render', () => {
let props: TableListItemProps;
let wrapper;
......@@ -40,62 +87,82 @@ describe('TableListItem', () => {
expect(wrapper.find(Link).exists()).toBeTruthy();
});
it('renders table name', () => {
expect(wrapper.find('.resource-name').children().at(0).text()).toEqual('tableSchema.tableName');
});
describe('renders resource-info section', () => {
let resourceInfo;
beforeAll(() => {
resourceInfo = wrapper.find('.resource-info');
});
it('renders a bookmark icon', () => {
expect(wrapper.find(BookmarkIcon).exists()).toBe(true);
});
it('renders start correct icon', () => {
const startIcon = resourceInfo.find('img');
expect(startIcon.exists()).toBe(true);
expect(startIcon.props().className).toEqual(wrapper.instance().generateResourceIconClass(props.table.database));
});
it('renders table description', () => {
expect(wrapper.find('.content').children().at(0).children().at(1).text()).toEqual('I am the description');
});
it('renders table name', () => {
expect(resourceInfo.find('.resource-name').children().at(0).text()).toEqual('tableSchema.tableName');
});
it('renders a bookmark icon in the resource name', () => {
expect(resourceInfo.find('.resource-name').find(BookmarkIcon).exists()).toBe(true);
});
it('renders resource type', () => {
expect(wrapper.find('.content').children().at(1).text()).toEqual(props.table.database);
it('renders table description', () => {
expect(resourceInfo.children().at(1).children().at(1).text()).toEqual('I am the description');
});
});
describe('if props.table has last_updated_epoch', () => {
it('renders Last Update title', () => {
expect(wrapper.find('.content').children().at(2).children().at(0).text()).toEqual('Last Updated');
describe('renders resource-type section', () => {
let resourceType;
beforeAll(() => {
resourceType = wrapper.find('.resource-type');
});
it('renders getDateLabel value', () => {
expect(wrapper.find('.content').children().at(2).children().at(1).text()).toEqual(wrapper.instance().getDateLabel());
it('renders resource type', () => {
expect(resourceType.text()).toEqual(ConfigUtils.getDatabaseDisplayName(props.table.database));
});
});
describe('if props.table does not have last_updated_epoch', () => {
it('does not render Last Updated section', () => {
const { props, wrapper } = setup({ table: {
type: ResourceType.table,
cluster: '',
database: '',
description: 'I am the description',
key: '',
last_updated_epoch: null,
name: 'tableName',
schema_name: 'tableSchema',
}});
expect(wrapper.find('.content').children().at(2).exists()).toBeFalsy();
describe('renders resource-badges section', () => {
let resourceBadges;
beforeAll(() => {
resourceBadges = wrapper.find('.resource-badges');
});
});
});
/* Note: Jest is configured to use UTC */
describe('getDateLabel', () => {
it('getDateLabel returns correct string', () => {
const { props, wrapper } = setup();
expect(wrapper.instance().getDateLabel()).toEqual('Mar 29, 2019');
});
});
it('renders resource badges section', () => {
expect(resourceBadges.exists()).toBe(true);
});
describe('getLink', () => {
it('getLink returns correct string', () => {
const { props, wrapper } = setup();
const { table, logging } = props;
expect(wrapper.instance().getLink()).toEqual(`/table_detail/${table.cluster}/${table.database}/${table.schema_name}/${table.name}?index=${logging.index}&source=${logging.source}`);
describe('if props.table has last_updated_epoch', () => {
it('renders Last Updated title', () => {
expect(resourceBadges.children().at(0).children().at(0).text()).toEqual('Last Updated');
});
it('renders getDateLabel value', () => {
expect(resourceBadges.children().at(0).children().at(1).text()).toEqual(wrapper.instance().getDateLabel());
});
});
describe('if props.table does not have last_updated_epoch', () => {
it('does not render Last Updated section', () => {
const { props, wrapper } = setup({ table: {
type: ResourceType.table,
cluster: '',
database: '',
description: 'I am the description',
key: '',
last_updated_epoch: null,
name: 'tableName',
schema_name: 'tableSchema',
}});
expect(wrapper.find('.resource-badges').children()).toHaveLength(1);
});
});
it('renders correct end icon', () => {
const expectedClassName = 'icon icon-right'
expect(resourceBadges.find('img').props().className).toEqual(expectedClassName);
});
});
});
});
......@@ -22,34 +22,54 @@ class UserListItem extends React.Component<UserListItemProps, {}> {
return `/user/${user.user_id}?index=${logging.index}&source=${logging.source}`;
};
renderUserInfo = (user: UserResource) => {
const { role_name, team_name, user_id } = user;
if (!role_name && !team_name) {
return null;
}
const listItems = [];
if (!!role_name) {
listItems.push((<li key={`${user_id}:role_name`}>{role_name}</li>));
}
if (!!team_name) {
listItems.push((<li key={`${user_id}:team_name`}>{team_name}</li>));
}
return listItems;
};
render() {
const { user } = this.props;
const userInfo = this.renderUserInfo(user);
return (
<li className="list-group-item">
<Link className="resource-list-item user-list-item" to={ this.getLink() }>
<Avatar name={ user.display_name } size={ 24 } round={ true } />
<div className="content">
<div className="col-xs-12">
<div className="title-2">
<div className="resource-info">
<Avatar name={ user.display_name } size={ 24 } round={ true } />
<div className="resource-info-text">
<div className="resource-name title-2">
{ user.display_name }
{
!user.is_active &&
<Flag text="Alumni" labelStyle='danger' />
}
</div>
<div className="body-secondary-3">
{
!user.role_name && user.team_name &&
`${user.team_name}`
}
{
user.role_name && user.team_name &&
`${user.role_name} on ${user.team_name}`
}
</div>
{
userInfo &&
<div className="body-secondary-3 truncated">
<ul>
{ userInfo}
</ul>
</div>
}
</div>
</div>
<img className="icon icon-right" />
<div className="resource-type">
User
</div>
<div className="resource-badges">
{
!user.is_active &&
<Flag text="Alumni" labelStyle='danger' />
}
<img className="icon icon-right" />
</div>
</Link>
</li>
);
......
......@@ -36,10 +36,45 @@ describe('UserListItem', () => {
return { props, wrapper };
};
describe('render', () => {
describe('renderUserInfo', () => {
let props: UserListItemProps;
let wrapper;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
});
it('returns null if no role_name or team_name exists on', () => {
const testUser = {
type: ResourceType.user,
display_name: 'firstname lastname',
email: 'test@test.com',
employee_type: 'fulltime',
first_name: 'firstname',
full_name: 'firstname lastname',
github_username: 'githubName',
is_active: false,
last_name: 'lastname',
manager_fullname: 'Test Manager',
profile_url: 'www.test.com',
role_name: null,
slack_id: 'www.slack.com',
team_name: '',
user_id: 'test0',
};
expect(wrapper.instance().renderUserInfo(testUser)).toBe(null);
});
it('returns an array of list items for user description', () => {
const content = wrapper.instance().renderUserInfo(props.user);
expect(shallow(content[0]).find('li').text()).toEqual(props.user.role_name);
expect(shallow(content[1]).find('li').text()).toEqual(props.user.team_name);
});
});
describe('render', () => {
let props: UserListItemProps;
let wrapper;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
......@@ -50,47 +85,104 @@ describe('UserListItem', () => {
expect(wrapper.find(Link).exists()).toBeTruthy();
});
it('renders Avatar', () => {
expect(wrapper.find(Link).find(Avatar).props()).toMatchObject({
name: props.user.display_name,
size: 24,
round: true,
describe('renders resource-info section', () => {
let resourceInfo;
beforeAll(() => {
resourceInfo = wrapper.find('.resource-info');
});
});
it('renders user.name', () => {
expect(wrapper.find('.content').children().at(0).children().at(0).children().at(0).text()).toEqual(props.user.display_name);
});
it('renders Avatar', () => {
expect(resourceInfo.find(Avatar).props()).toMatchObject({
name: props.user.display_name,
size: 24,
round: true,
});
});
it('renders user.name', () => {
expect(resourceInfo.children().at(1).children().at(0).text()).toEqual(props.user.display_name);
});
it('does not render Alumni flag if user is active', () => {
expect(wrapper.find('.content').children().at(0).children().at(0).find(Flag).exists()).toBeFalsy();
it('calls renderUserInfo with correct props', () => {
const renderUserInfoSpy = jest.spyOn(wrapper.instance(), 'renderUserInfo');
wrapper.instance().forceUpdate();
expect(renderUserInfoSpy).toHaveBeenCalledWith(props.user);
});
it('renders ul with list item results of renderUserInfo', () => {
const renderUserInfoSpy = jest.spyOn(wrapper.instance(), 'renderUserInfo').mockImplementation(() => {
return (<div>Mock Info</div>);
});
wrapper.instance().forceUpdate();
expect(wrapper.find('.resource-info').children().at(1).children().at(1).find('ul').children().html()).toEqual('<div>Mock Info</div>');
});
it('does not render description if renderUserInfo returns null', () => {
const renderUserInfoSpy = jest.spyOn(wrapper.instance(), 'renderUserInfo').mockImplementation(() => {
return null;
});
wrapper.instance().forceUpdate();
expect(wrapper.find('.resource-info').children().at(1).children().at(1).exists()).toBe(false);
});
});
it('renders description', () => {
expect(wrapper.find('.content').children().at(0).children().at(1).text()).toEqual(`${props.user.role_name} on ${props.user.team_name}`);
describe('renders resource-type section', () => {
let resourceType;
beforeAll(() => {
resourceType = wrapper.find('.resource-type');
});
it('renders resource type', () => {
expect(resourceType.text()).toEqual('User');
});
});
it('renders Alumni flag if user not active', () => {
const wrapper = setup({
user: {
type: ResourceType.user,
display_name: 'firstname lastname',
email: 'test@test.com',
employee_type: 'fulltime',
first_name: 'firstname',
full_name: 'firstname lastname',
github_username: 'githubName',
is_active: false,
last_name: 'lastname',
manager_fullname: 'Test Manager',
profile_url: 'www.test.com',
role_name: 'Tester',
slack_id: 'www.slack.com',
team_name: 'QA',
user_id: 'test0',
}
}).wrapper;
expect(wrapper.find('.content').children().at(0).children().at(0).find(Flag).exists()).toBeTruthy();
describe('renders resource-badges section', () => {
let resourceBadges;
beforeAll(() => {
resourceBadges = wrapper.find('.resource-badges');
});
it('renders resource badges section', () => {
expect(resourceBadges.exists()).toBe(true);
});
it('does not render Alumni flag if user is active', () => {
expect(resourceBadges.find(Flag).exists()).toBe(false);
});
it('renders Alumni flag if user not active', () => {
const wrapper = setup({
user: {
type: ResourceType.user,
display_name: 'firstname lastname',
email: 'test@test.com',
employee_type: 'fulltime',
first_name: 'firstname',
full_name: 'firstname lastname',
github_username: 'githubName',
is_active: false,
last_name: 'lastname',
manager_fullname: 'Test Manager',
profile_url: 'www.test.com',
role_name: 'Tester',
slack_id: 'www.slack.com',
team_name: 'QA',
user_id: 'test0',
}
}).wrapper;
const flagComponent = wrapper.find('.resource-badges').find(Flag);
expect(flagComponent.exists()).toBe(true);
expect(flagComponent.props()).toMatchObject({
text: 'Alumni',
labelStyle: 'danger',
});
});
it('renders correct end icon', () => {
const expectedClassName = 'icon icon-right'
expect(resourceBadges.find('img').props().className).toEqual(expectedClassName);
});
});
});
......
@import 'variables';
.list-group-item .resource-list-item {
color: $text-dark;
color: $text-primary;
display: flex;
flex-direction: row;
height: 78px;
padding: 16px 8px;
height: 96px;
padding: 24px 24px;
text-decoration: none;
img.icon,
img.icon.resource-icon,
.sb-avatar {
margin: auto 0;
margin: auto 16px auto 0px;
}
&:hover img.icon {
background-color: $brand-color-4;
.icon-right {
margin: auto 0 auto auto;
opacity: 0;
}
.content {
width: 100%;
min-width: 0; /* Needed to support `white-space: nowrap` */
&:hover .icon-right,
&:focus .icon-right {
opacity: 1;
}
.resource-info {
display: flex;
flex: 7;
min-width: 0px;
.resource-info-text {
overflow: hidden;
}
.resource-name .truncated {
display: inline-block;
max-width: calc(100% - 32px);
.resource-name {
display: flex;
}
ul {
display: flex;
list-style-type: unset;
padding: 0;
}
li {
margin-right: 12px;
margin-left: 12px;
&:first-child {
list-style-type: none;
margin-left: 0;
}
}
}
}
.resource-type {
flex: 2;
margin: auto 24px;
}
.resource-badges {
display: flex;
flex: 3;
flex-wrap: wrap;
margin-top: auto;
margin-bottom: auto;
.flag {
margin: 4px;
}
}
.tab-content {
.list-group-item:first-child {
border-top: none;
.title-2 {
color: $resource-title-color;
}
}
......@@ -8,3 +8,7 @@ export const SUBTEXT_DEFAULT = `Search within a category using the pattern with
export const SYNTAX_ERROR_CATEGORY = `Advanced search syntax only supports searching one category. Please remove all extra ':'`;
export const SYNTAX_ERROR_PREFIX = 'Did you mean ';
export const SYNTAX_ERROR_SPACING_SUFFIX = ` ? Please remove the space around the ':'.`;
export const BUTTON_CLOSE_TEXT = 'Close';
export const SIZE_SMALL = 'small';
......@@ -6,8 +6,10 @@ import { connect } from 'react-redux';
import './styles.scss';
import {
BUTTON_CLOSE_TEXT,
ERROR_CLASSNAME,
PLACEHOLDER_DEFAULT,
SIZE_SMALL,
SUBTEXT_DEFAULT,
SYNTAX_ERROR_CATEGORY,
SYNTAX_ERROR_PREFIX,
......@@ -28,6 +30,7 @@ export interface DispatchFromProps {
export interface OwnProps {
placeholder?: string;
subText?: string;
size?: string;
}
export type SearchBarProps = StateFromProps & DispatchFromProps & OwnProps;
......@@ -42,6 +45,7 @@ export class SearchBar extends React.Component<SearchBarProps, SearchBarState> {
public static defaultProps: Partial<SearchBarProps> = {
placeholder: PLACEHOLDER_DEFAULT,
subText: SUBTEXT_DEFAULT,
size: '',
};
constructor(props) {
......@@ -59,6 +63,10 @@ export class SearchBar extends React.Component<SearchBarProps, SearchBarState> {
return { searchTerm };
}
clearSearchTerm = () : void => {
this.setState({ searchTerm: '' });
};
handleValueChange = (event: React.SyntheticEvent<HTMLInputElement>) : void => {
this.setState({ searchTerm: (event.target as HTMLInputElement).value.toLowerCase() });
};
......@@ -101,26 +109,36 @@ export class SearchBar extends React.Component<SearchBarProps, SearchBarState> {
};
render() {
const inputClass = `${this.props.size === SIZE_SMALL ? 'h3 small' : 'h2 large'} search-bar-input form-control`;
const searchButtonClass = `btn btn-flat-icon search-button ${this.props.size === SIZE_SMALL ? 'small' : 'large'}`;
const subTextClass = `subtext body-secondary-3 ${this.state.subTextClassName}`;
return (
<div id="search-bar">
<form className="search-bar-form" onSubmit={ this.handleValueSubmit }>
<input
id="search-input"
className="h2 search-bar-input form-control"
className={ inputClass }
value={ this.state.searchTerm }
onChange={ this.handleValueChange }
aria-label={ this.props.placeholder }
placeholder={ this.props.placeholder }
autoFocus={ true }
/>
<button className="btn btn-flat-icon search-bar-button" type="submit">
<button className={ searchButtonClass } type="submit">
<img className="icon icon-search" />
</button>
{
this.props.size === SIZE_SMALL &&
<button type="button" className="btn btn-close clear-button" aria-label={BUTTON_CLOSE_TEXT} onClick={this.clearSearchTerm} />
}
</form>
<div className={ subTextClass }>
{ this.state.subText }
</div>
{
this.props.size !== SIZE_SMALL &&
<div className={ subTextClass }>
{ this.state.subText }
</div>
}
</div>
);
}
......@@ -136,4 +154,4 @@ export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({ submitSearch }, dispatch);
};
export default connect<StateFromProps>(mapStateToProps, mapDispatchToProps)(SearchBar);
export default connect<StateFromProps, DispatchFromProps, OwnProps>(mapStateToProps, mapDispatchToProps)(SearchBar);
......@@ -4,35 +4,57 @@
.search-bar-form {
position: relative;
.search-bar-button {
.search-button {
position: absolute;
height: 24px;
width: 24px;
top: 28px;
left: 24px;
&.small {
top: 6px;
left: 8px;
}
&.large {
top: 28px;
left: 24px;
}
}
.clear-button {
position: absolute;
height: 24px;
width: 24px;
top: 2px;
right: 8px;
}
.h2.search-bar-input {
.search-bar-input {
border-radius: 4px;
border-color: $gray-light;
border-color: $stroke;
box-shadow: 0 3px 12px 0 rgba(17, 17, 31, 0.04);
height: 80px;
width: 100%;
padding: 16px 24px 16px 64px;
&.large {
height: 80px;
padding: 16px 24px 16px 64px;
}
&.small {
height: 36px;
padding: 6px 6px 6px 36px;
}
&:focus {
border-color: $brand-color-4;
border-color: $stroke-focus;
}
}
.search-bar-input:focus + .search-bar-button {
img.icon {
background-color: $brand-color-4;
&:focus + .search-button .icon-search:not(:hover) {
background-color: $stroke-focus;
}
}
@media (max-width: $screen-md-max) {
.search-bar-button {
.search-button {
left: 16px;
top: 18px;
}
......
......@@ -55,6 +55,17 @@ describe('SearchBar', () => {
});
});
describe('clearSearchTerm', () => {
it('sets the searchTerm to an empty string', () => {
setStateSpy.mockClear();
const initialSearchTerm = 'non empty search term';
const { wrapper } = setup({ searchTerm: initialSearchTerm});
expect(wrapper.state().searchTerm).toBe(initialSearchTerm);
wrapper.instance().clearSearchTerm();
expect(setStateSpy).toHaveBeenCalledWith({ searchTerm: '' });
});
});
describe('handleValueChange', () => {
it('calls setState on searchTerm with event.target.value.toLowerCase()', () => {
const { props, wrapper } = setup();
......@@ -178,7 +189,7 @@ describe('SearchBar', () => {
expect(wrapper.find('form').find('input').props()).toMatchObject({
'aria-label': SearchBar.defaultProps.placeholder,
autoFocus: true,
className: 'h2 search-bar-input form-control',
className: 'h2 large search-bar-input form-control',
id: 'search-input',
onChange: wrapper.instance().handleValueChange,
placeholder: SearchBar.defaultProps.placeholder,
......@@ -191,7 +202,7 @@ describe('SearchBar', () => {
expect(wrapper.find('form').find('input').props()).toMatchObject({
'aria-label': props.placeholder,
autoFocus: true,
className: 'h2 search-bar-input form-control',
className: 'h2 large search-bar-input form-control',
id: 'search-input',
onChange: wrapper.instance().handleValueChange,
placeholder: props.placeholder,
......@@ -202,7 +213,7 @@ describe('SearchBar', () => {
describe('submit button', () => {
it('renders button with correct props', () => {
expect(wrapper.find('form').find('button').props()).toMatchObject({
className: 'btn btn-flat-icon search-bar-button',
className: 'btn btn-flat-icon search-button large',
type: 'submit',
});
});
......@@ -226,6 +237,22 @@ describe('SearchBar', () => {
expect(wrapper.children().at(1).text()).toEqual(wrapper.state().subText);
});
});
describe('render with small mode', () => {
const { wrapper, props } = setup({ size: "small" });
it('does not render a subtext', () => {
const subtext = wrapper.find('subtext');
expect(subtext.exists()).toBe(false);
});
it('renders a close button', () => {
const closeButton = wrapper.find('button.clear-button');
expect(closeButton.exists()).toBe(true);
const buttonProps = closeButton.props();
expect(buttonProps.onClick).toEqual(wrapper.instance().clearSearchTerm);
});
});
});
});
......
......@@ -20,14 +20,14 @@
> a {
background: none;
border: none;
color: $text-medium;
color: $text-secondary;
font-size: $font-size-large;
line-height: $line-height-large;
margin: -4px 0 0 0;
padding: 4px 8px 12px;
&:hover {
color: $text-dark;
color: $text-primary;
}
// Active tab indicator
......
......@@ -18,6 +18,10 @@ const configDefault: AppConfig = {
enabled: false,
},
logoPath: null,
mailClientFeatures: {
feedbackEnabled: false,
notificationsEnabled: false,
},
navLinks: [
{
label: "Announcements",
......@@ -32,9 +36,25 @@ const configDefault: AppConfig = {
use_router: true,
}
],
mailClientFeatures: {
feedbackEnabled: false,
notificationsEnabled: false,
resourceConfig: {
datasets: {
'bigquery': {
displayName: 'BigQuery',
iconClass: 'icon-bigquery',
},
'hive': {
displayName: 'Hive',
iconClass: 'icon-hive',
},
'postgres': {
displayName: 'Postgres',
iconClass: 'icon-postgres',
},
'redshift': {
displayName: 'Redshift',
iconClass: 'icon-redshift',
},
},
},
tableLineage: {
iconPath: 'PATH_TO_ICON',
......
......@@ -12,6 +12,7 @@ export interface AppConfig {
logoPath: string | null;
mailClientFeatures: MailClientFeaturesConfig;
navLinks: Array<LinkConfig>;
resourceConfig: ResourceConfig;
tableLineage: TableLineageConfig;
tableProfile: TableProfileConfig;
}
......@@ -51,6 +52,39 @@ interface BrowseConfig {
showAllTags: boolean;
}
/** ResourceConfig - For customizing values related to how various resources
* are displayed in the UI.
*
* datasets - A map of each dataset id to an optional display name or icon class
*/
interface ResourceConfig {
datasets: { [id: string]: DatasetConfig }
}
/** DatasetConfig - For customizing values related to how each dataset resource
* is displayed in the UI.
*
* displayName - An optional display name for this dataset source
* iconClass - An option icon class to be used for this dataset source. This
* value should be defined in static/css/_icons.scss
*/
interface DatasetConfig {
displayName?: string;
iconClass?: string;
}
/**
* MailClientFeaturesConfig - Enable/disable UI features with a dependency on
* configuring a custom mail client.
*
* feedbackEnabled - Enables the feedback feature UI
* notificationsEnabled - Enables any UI related to sending notifications to users
*/
interface MailClientFeaturesConfig {
feedbackEnabled: boolean;
notificationsEnabled: boolean;
}
/**
* MailClientFeaturesConfig - Enable/disable UI features with a dependency on
* configuring a custom mail client.
......
import AppConfig from 'config/config';
export const DEFAULT_DATABASE_ICON_CLASS = 'icon-database icon-color';
/**
* Returns the database display name for a given database id.
* If a configuration or display name does not exist for the give id, the id
* is returned.
*/
export function getDatabaseDisplayName(databaseId: string): string {
const databaseConfig = AppConfig.resourceConfig.datasets[databaseId];
if (!databaseConfig || !databaseConfig.displayName) {
return databaseId;
}
return databaseConfig.displayName;
}
/**
* Returns an icon class for a given database id, which should be a value
* defined in `static/css/_icons.scss`.
* If a configuration or icon class does not exist for the give id, the default
* database icon class is returned.
*/
export function getDatabaseIconClass(databaseId: string): string {
const databaseConfig = AppConfig.resourceConfig.datasets[databaseId];
if (!databaseConfig || !databaseConfig.iconClass) {
return DEFAULT_DATABASE_ICON_CLASS;
}
return databaseConfig.iconClass;
}
/**
* Returns whether or not feedback features should be enabled
*/
export function feedbackEnabled(): boolean {
return AppConfig.mailClientFeatures.feedbackEnabled;
}
/**
* Returns whether or not notification features should be enabled
*/
export function notificationsEnabled(): boolean {
return AppConfig.mailClientFeatures.notificationsEnabled;
}
import AppConfig from 'config/config';
import * as ConfigUtils from 'config/config-utils';
describe('getDatabaseDisplayName', () => {
it('returns given id if no config for that id exists', () => {
const testId = 'fakeName';
expect(ConfigUtils.getDatabaseDisplayName(testId)).toBe(testId);
});
it('returns given id for a configured database id', () => {
const testId = 'hive';
const expectedName = AppConfig.resourceConfig.datasets[testId].displayName;
expect(ConfigUtils.getDatabaseDisplayName(testId)).toBe(expectedName);
})
});
describe('getDatabaseIconClass', () => {
it('returns default class no config for that id exists', () => {
const testId = 'fakeName';
expect(ConfigUtils.getDatabaseIconClass(testId)).toBe(ConfigUtils.DEFAULT_DATABASE_ICON_CLASS);
});
it('returns given icon class for a configured database id', () => {
const testId = 'hive';
const expectedClass = AppConfig.resourceConfig.datasets[testId].iconClass;
expect(ConfigUtils.getDatabaseIconClass(testId)).toBe(expectedClass);
})
});
describe('feedbackEnabled', () => {
it('returns whether or not the feaadback feature is enabled', () => {
expect(ConfigUtils.feedbackEnabled()).toBe(AppConfig.mailClientFeatures.feedbackEnabled);
......
......@@ -54,7 +54,6 @@ export function* searchAllWorker(action: SearchAllRequest): SagaIterator {
dashboards: dashboardResponse.dashboards || initialState.dashboards,
isLoading: false,
};
if (resource === undefined) {
resource = autoSelectResource(searchAllResponse);
searchAllResponse.selectedTab = resource;
......
......@@ -17,7 +17,6 @@ export const getPageIndex = (state: SearchReducerState, resource?: ResourceType)
return 0;
};
export const autoSelectResource = (state: SearchReducerState) => {
if (state.tables && state.tables.total_results > 0) {
return ResourceType.table;
......
......@@ -13,7 +13,6 @@ import { feedbackEnabled } from 'config/config-utils';
import AnnouncementPage from './components/AnnouncementPage';
import BrowsePage from './components/BrowsePage';
import Feedback from './components/Feedback';
import Footer from './components/Footer';
import HomePage from './components/HomePage'
import NavBar from './components/NavBar';
......@@ -39,7 +38,7 @@ ReactDOM.render(
<Router history={BrowserHistory}>
<div id="main">
<Preloader/>
<NavBar />
<Route component={NavBar} />
<Switch>
<Route path="/table_detail/:cluster/:db/:schema/:table" component={TableDetail} />
<Route path="/announcements" component={AnnouncementPage} />
......@@ -49,10 +48,6 @@ ReactDOM.render(
<Route path="/404" component={NotFoundPage} />
<Route path="/" component={HomePage} />
</Switch>
{
feedbackEnabled() &&
<Feedback />
}
<Footer />
</div>
</Router>
......
......@@ -18,7 +18,7 @@ _TODO: Please add doc_
_TODO: Please add doc_
## Mail Client Features
Amundsen has two features that leverage the custom mail client -- the feedback tool and notifications.
Amundsen has two features that leverage the custom mail client -- the feedback tool and notifications.
As these are optional features, our `MailClientFeaturesConfig` can be used to hide/display any UI related to these features:
1. Set `MailClientFeaturesConfig.feedbackEnabled` to `true` in order to display the `Feedback` component in the UI.
......@@ -31,6 +31,20 @@ client, please see this [entry](flask_config.md#mail-client-features) in our fla
_TODO: Please add doc_
## Resource Configurations
### Datasets
We provide a `datasets` option on our `ResourceConfig`. This can be used for the following customizations:
1. You can configure custom icons to be used throughout the UI when representing datasets from particular sources/databases. On the `ResourceConfig.datasets` object, add an entry with the `id` used to reference that database and set the `iconClass`. This `iconClass` should be defined in [icons.scss](https://github.com/lyft/amundsenfrontendlibrary/blob/master/amundsen_application/static/css/_icons.scss).
2. You can configure a specific display name to be used throughout the UI when representing datasets from particular sources/databases. On the `ResourceConfig.datasets` object, add an entry with the `id` used to reference that database and set the `displayName`.
#### Custom Icons
#### Display Names
_TODO: Please add doc_
## Table Lineage
_TODO: Please add doc_
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment