diff --git a/.gitignore b/.gitignore index bb293595e316e..832681095db09 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ selenium *.swp *.swo *.out +src/ui_framework/doc_site/build/*.js* diff --git a/package.json b/package.json index 1380678cc5d74..05650e6f26268 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,8 @@ "makelogs": "makelogs", "mocha": "mocha", "mocha:debug": "mocha --debug-brk", - "sterilize": "grunt sterilize" + "sterilize": "grunt sterilize", + "uiFramework:start": "webpack-dev-server --config src/ui_framework/doc_site/webpack.config.js --hot --inline --content-base src/ui_framework/doc_site/build" }, "repository": { "type": "git", @@ -171,6 +172,8 @@ "cheerio": "0.22.0", "chokidar": "1.6.0", "chromedriver": "2.24.1", + "classnames": "2.2.5", + "del": "1.2.1", "elasticdump": "2.1.1", "eslint": "1.10.3", "eslint-plugin-mocha": "1.1.0", @@ -190,6 +193,9 @@ "gruntify-eslint": "1.0.1", "gulp-sourcemaps": "1.7.3", "handlebars": "4.0.5", + "highlight.js": "9.0.0", + "history": "2.1.1", + "html-loader": "0.4.3", "husky": "0.8.1", "image-diff": "1.6.0", "intern": "3.2.3", @@ -201,6 +207,7 @@ "karma-ie-launcher": "0.2.0", "karma-mocha": "0.2.0", "karma-safari-launcher": "0.1.1", + "keymirror": "0.1.1", "license-checker": "5.1.2", "load-grunt-config": "0.19.2", "makelogs": "3.0.2", @@ -209,15 +216,26 @@ "murmurhash3js": "3.0.1", "ncp": "2.0.0", "nock": "8.0.0", + "node-sass": "3.8.0", "npm": "3.10.8", "portscanner": "1.0.0", "proxyquire": "1.7.10", + "react": "15.2.0", + "react-addons-test-utils": "15.2.0", + "react-dom": "15.2.0", + "react-redux": "4.4.5", + "react-router": "2.0.0", + "react-router-redux": "4.0.4", + "redux": "3.0.0", + "redux-thunk": "0.1.0", + "sass-loader": "4.0.0", "simple-git": "1.37.0", "sinon": "1.17.2", "source-map": "0.5.6", "source-map-support": "0.2.10", "supertest": "1.2.0", - "supertest-as-promised": "2.0.2" + "supertest-as-promised": "2.0.2", + "webpack-dev-server": "1.14.1" }, "engines": { "node": "6.9.0", diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 5e0e79d3d72a2..8c0ac81b1361e 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -108,6 +108,13 @@ class BaseOptimizer { `css${mapQ}!autoprefixer${mapQPre}{ "browsers": ["last 2 versions","> 5%"] }!less${mapQPre}dumpLineNumbers=comments` ) }, + { + test: /\.scss$/, + loader: ExtractTextPlugin.extract( + 'style', + `css${mapQ}!autoprefixer${mapQPre}{ "browsers": ["last 2 versions","> 5%"] }!sass${mapQPre}` + ) + }, { test: /\.css$/, loader: ExtractTextPlugin.extract('style', `css${mapQ}`) }, { test: /\.jade$/, loader: 'jade' }, { test: /\.json$/, loader: 'json' }, diff --git a/src/ui/public/autoload/styles.js b/src/ui/public/autoload/styles.js index 040094a9ce921..e1329e49ffb99 100644 --- a/src/ui/public/autoload/styles.js +++ b/src/ui/public/autoload/styles.js @@ -1,2 +1,6 @@ +// Kibana UI Framework +require('../../../ui_framework/components/index.scss'); + +// All Kibana styles inside of the /styles dir const context = require.context('../styles', false, /[\/\\](?!mixins|variables|_|\.)[^\/\\]+\.less/); context.keys().forEach(key => context(key)); diff --git a/src/ui/public/styles/base.less b/src/ui/public/styles/base.less index 9c3ac595de46b..9a78036496a19 100644 --- a/src/ui/public/styles/base.less +++ b/src/ui/public/styles/base.less @@ -3,9 +3,6 @@ @import (reference) "./variables"; @import (reference) "~ui/styles/bootstrap/bootstrap"; -// Kibana UI Framework -@import (less) "~@elastic/kibana-ui-framework/dist/framework.css"; - html, body { .flex-parent(); diff --git a/src/ui_framework/README.md b/src/ui_framework/README.md new file mode 100644 index 0000000000000..7e14e71c45ce3 --- /dev/null +++ b/src/ui_framework/README.md @@ -0,0 +1,57 @@ +# Kibana UI Framework + +## Development + +* Start development server `npm run uiFramework:start`. +* View docs on `http://localhost:8080/`. + +## What is this? + +The UI Framework provides you with UI components you can quickly use to build UIs, as well as interactive examples which document how they're supposed to be used. These UI components are currently only implemented in CSS and markup, but eventually they'll grow to involve JS as well. + +When you build a UI using this framework (e.g. a plugin's UI), you can rest assured it will fit into the overall Kibana UI. + +## Benefits + +### Dynamic, interactive documentation + +By having a "living style guide", we relieve our designers of the burden of creating and maintaining static style guides. This also makes it easier for our engineers to translate mockups, prototypes, and wireframes into products. + +### Copy-pasteable UI + +Engineers can copy and paste sample code into their projects to quickly get reliable, consistent results. + +### Remove CSS from the day-to-day + +The CSS portion of this framework means engineers don't need to spend mental cycles. These cycles can be spent on the things critical to the identity of the specific project they're working on, like architecture and business logic. + +Once this framework also provides JS components, engineers won't even need to _see_ CSS -- it will be encapsulated behind the JS components' interfaces. + +### More UI tests === fewer UI bugs + +By covering our UI components with great unit tests and having those tests live within the framework itself, we can rest assured that our UI layer is tested and remove some of that burden from out integration/end-to-end tests. + +## Why not just use Bootstrap? + +In short: we've outgrown it! Third-party CSS frameworks like Bootstrap and Foundation are designed +for a general audience, so they offer things we don't need and _don't_ offer things we _do_ need. +As a result, we've been forced to override their styles until the original framework is no longer +recognizable. When the CSS reaches that point, it's time to take ownership over it and build +your own framework. + +We also gain the ability to fix some of the common issues with third-party CSS frameworks: + +* They have non-semantic markup. +* They deeply nest their selectors. + +For a more in-depth analysis of the problems with Bootstrap (and similar frameworks), check out this article and the links it has at the bottom: ["Bootstrap Bankruptcy"](http://www.matthewcopeland.me/blog/2013/11/04/bootstrap-bankruptcy/). + +## Examples of other in-house UI frameworks + +* [Ubiquiti CSS Framework](http://ubnt-css.herokuapp.com/#/app/popover) +* [Smaato React UI Framework](http://smaato.github.io/ui-framework/#/modal) +* [Lonely Planet Style Guide](http://rizzo.lonelyplanet.com/styleguide/design-elements/colours) +* [MailChimp Patterns Library](http://ux.mailchimp.com/patterns) +* [Salesforce Lightning Design System](https://www.lightningdesignsystem.com/) +* [Refills](http://refills.bourbon.io/) +* [Formstone](https://formstone.it/) \ No newline at end of file diff --git a/src/ui_framework/components/index.scss b/src/ui_framework/components/index.scss new file mode 100644 index 0000000000000..8eaa3ef6bea93 --- /dev/null +++ b/src/ui_framework/components/index.scss @@ -0,0 +1,27 @@ +// Normal colors +$textColor: #2d2d2d; +$buttonTextColor: #ffffff; +$buttonBackgroundColor: #9c9c9c; +$linkColor: #328CAA; +$linkColor-isHover: #105A73; +$inputTextColor: $textColor; +$inputBackgroundColor: #ffffff; +$inputBorderColor: $inputBackgroundColor; + +// Dark theme colors +$textColor--darkTheme: #cecece; +$buttonTextColor--darkTheme: #ffffff; +$buttonBackgroundColor--darkTheme: #777777; +$linkColor--darkTheme: #b7e2ea; +$linkColor-isHover--darkTheme: #def2f6; +$inputTextColor--darkTheme: $textColor--darkTheme; +$inputBackgroundColor--darkTheme: #444444; +$inputBorderColor--darkTheme: $inputBackgroundColor--darkTheme; + +@mixin darkTheme() { + .theme-dark & { + @content; + } +} + +@import "local_nav/index"; diff --git a/src/ui_framework/components/local_nav/_index.scss b/src/ui_framework/components/local_nav/_index.scss new file mode 100644 index 0000000000000..b04b1b3cb9b4e --- /dev/null +++ b/src/ui_framework/components/local_nav/_index.scss @@ -0,0 +1,48 @@ +// Normal colors +$localNavTextColor: $textColor; +$localNavBackgroundColor: #e4e4e4; +$localNavButtonTextColor: #5a5a5a; +$localNavButtonTextColor-isHover: #000000; +$localNavButtonBackgroundColor: transparent; +$localNavButtonBackgroundColor-isHover: rgba(#000000, 0.1); +$localNavButtonBackgroundColor-isSelected: #f6f6f6; +$localNavBreadcrumbDelimiterColor: #5a5a5a; +$localSearchBackgroundColor: #ffffff; +$localSearchBorderColor-isInvalid: #e74C3c; +$localDropdownBackgroundColor: $localNavButtonBackgroundColor-isSelected; +$localDropdownFormNoteTextColor: #737373; +$localTabTextColor: $localNavButtonTextColor; +$localTabTextColor-isHover: $localNavButtonTextColor-isHover; +$localTabTextColor-isSelected: $localNavButtonTextColor-isHover; + +// Dark theme colors +$localNavTextColor--darkTheme: $textColor--darkTheme; +$localNavBackgroundColor--darkTheme: #333333; +$localNavButtonTextColor--darkTheme: #dedede; +$localNavButtonTextColor-isHover--darkTheme: #ffffff; +$localNavButtonBackgroundColor-isHover--darkTheme: #000000; +$localNavButtonBackgroundColor-isSelected--darkTheme: #525252; +$localNavBreadcrumbDelimiterColor--darkTheme: #a5a5a5; +$localSearchBackgroundColor--darkTheme: #4e4e4e; +$localSearchBorderColor-isInvalid--darkTheme: #ff6758; +$localDropdownBackgroundColor--darkTheme: $localNavButtonBackgroundColor-isSelected--darkTheme; +$localDropdownFormNoteTextColor--darkTheme: #a2a2a2; +$localDropdownWarningTextColor--darkTheme: $textColor--darkTheme; +$localDropdownWarningBackgroundColor--darkTheme: #636363; +$localTabTextColor--darkTheme: $localNavButtonTextColor--darkTheme; +$localTabTextColor-isHover--darkTheme: $localNavButtonTextColor-isHover--darkTheme; +$localTabTextColor-isSelected--darkTheme: $localNavButtonTextColor-isHover--darkTheme; + +// Spacing +$localNavSideSpacing: 10px; + +// Font size +$localNavFontSizeNormal: 14px; + +@import "local_breadcrumbs"; +@import "local_dropdown"; +@import "local_menu"; +@import "local_nav"; +@import "local_search"; +@import "local_tabs"; +@import "local_title"; diff --git a/src/ui_framework/components/local_nav/_local_breadcrumbs.scss b/src/ui_framework/components/local_nav/_local_breadcrumbs.scss new file mode 100644 index 0000000000000..16d8bd834f5da --- /dev/null +++ b/src/ui_framework/components/local_nav/_local_breadcrumbs.scss @@ -0,0 +1,62 @@ + +/** + * 1. Breadcrumbs are placed in the top-left corner and need to be bumped over + * a bit. + */ +.localBreadcrumbs { + display: flex; + align-items: center; + height: 100%; + padding-left: $localNavSideSpacing; /* 1 */ +} + + .localBreadcrumb { + & + & { + margin-left: 6px; + + &:before { + content: '/'; + user-select: none; + margin-right: 4px; + color: $localNavBreadcrumbDelimiterColor; + + @include darkTheme { + color: $localNavBreadcrumbDelimiterColor--darkTheme; + } + } + } + + &:last-child { + .localBreadcrumb__link { + color: $localNavTextColor; + cursor: default; + + &:hover { + text-decoration: none; + } + + @include darkTheme { + color: $localNavTextColor--darkTheme; + } + } + } + + } + + .localBreadcrumb__link { + font-size: $localNavFontSizeNormal; + color: #5a5a5a; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + + @include darkTheme { + color: $localNavButtonTextColor--darkTheme; + } + } + + .localBreadcrumb__emphasis { + font-weight: 700; + } diff --git a/src/ui_framework/components/local_nav/_local_dropdown.scss b/src/ui_framework/components/local_nav/_local_dropdown.scss new file mode 100644 index 0000000000000..53728b4e8f502 --- /dev/null +++ b/src/ui_framework/components/local_nav/_local_dropdown.scss @@ -0,0 +1,163 @@ + +.localDropdown { + position: relative; + padding: 10px $localNavSideSpacing 14px; + background-color: $localDropdownBackgroundColor; + line-height: 20px; + + @include darkTheme { + background-color: $localDropdownBackgroundColor--darkTheme; + } +} + +.localDropdownCloseButton { + appearance: none; + background-color: transparent; + padding: 4px; + border: none; + position: absolute; + top: 1px; + right: 5px; + font-size: 16px; + color: $localNavTextColor; + cursor: pointer; + opacity: 0.35; + + &:hover { + opacity: 1; + } + + @include darkTheme { + color: $localNavTextColor--darkTheme; + } +} + +.localDropdownPanels { + display: flex; +} + +.localDropdownPanel { + flex: 1 1 0%; +} + +.localDropdownPanel--left { + margin-right: 30px; +} + +.localDropdownPanel--right { + margin-left: 30px; +} + +.localDropdownTitle { + margin-bottom: 12px; + font-size: 18px; + color: $localNavTextColor; + + @include darkTheme { + color: $localNavTextColor--darkTheme; + } +} + +.localDropdownSection { + margin-bottom: 16px; + + &:last-child { + margin-bottom: 0; + } +} + +.localDropdownHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 6px; +} + + .localDropdownHeader__label { + font-size: 14px; + font-weight: 700; + color: $localNavTextColor; + + @include darkTheme { + color: $localNavTextColor--darkTheme; + } + } + + .localDropdownHeader__actions { + display: flex; + } + + .localDropdownHeader__action { + color: $linkColor; + font-size: 12px; + text-decoration: none; + cursor: pointer; + + & + & { + margin-left: 10px; + } + + &:hover, + &:active { + color: $linkColor-isHover; + } + + @include darkTheme { + color: $linkColor--darkTheme; + + &:hover, + &:active { + color: $linkColor-isHover--darkTheme; + } + } + } + +.localDropdownInput { + display: block; + width: 100%; + margin-bottom: 12px; + padding: 5px 15px; + font-size: 14px; + color: $inputTextColor; + background-color: $inputBackgroundColor; + border: 2px solid $inputBorderColor; + border-radius: 4px; + + @include darkTheme { + color: $inputTextColor--darkTheme; + background-color: $inputBackgroundColor--darkTheme; + border-color: $inputBorderColor--darkTheme; + } +} + +.localDropdownFormNote { + font-size: 14px; + color: $localDropdownFormNoteTextColor; + + @include darkTheme { + color: $localDropdownFormNoteTextColor--darkTheme; + } +} + +.localDropdownWarning { + margin-bottom: 16px; + padding: 6px 10px; + font-size: 14px; + color: $textColor; + background-color: $localNavBackgroundColor; + + @include darkTheme { + color: $localDropdownWarningTextColor--darkTheme; + background-color: $localDropdownWarningBackgroundColor--darkTheme; + } +} + +.localDropdownHelpText { + margin-bottom: 16px; + font-size: 14px; + color: #2D2D2D; + + @include darkTheme { + color: #9e9e9e; + } +} diff --git a/src/ui_framework/components/local_nav/_local_menu.scss b/src/ui_framework/components/local_nav/_local_menu.scss new file mode 100644 index 0000000000000..45c9c0a6e728a --- /dev/null +++ b/src/ui_framework/components/local_nav/_local_menu.scss @@ -0,0 +1,62 @@ + +.localMenu { + display: flex; + align-items: center; + height: 100%; +} + + .localMenuItem { + display: flex; + align-items: center; + height: 100%; + padding: 0 $localNavSideSpacing; + font-size: $localNavFontSizeNormal; + background-color: $localNavButtonBackgroundColor; + color: $localNavButtonTextColor; + border: 0; + cursor: pointer; + + &:hover { + background-color: $localNavButtonBackgroundColor-isHover; + color: $localNavButtonTextColor-isHover; + } + + &.localMenuItem-isSelected { + background-color: $localNavButtonBackgroundColor-isSelected; + } + + &.localMenuItem-isDisabled { + opacity: 0.5; + cursor: default; + + &:hover { + background-color: $localNavButtonBackgroundColor; + color: $localNavButtonTextColor; + } + } + + @include darkTheme { + color: $localNavButtonTextColor--darkTheme; + + &:hover { + background-color: $localNavButtonBackgroundColor-isHover--darkTheme; + color: $localNavButtonTextColor-isHover--darkTheme; + } + + &.localMenuItem-isSelected { + background-color: $localNavButtonBackgroundColor-isSelected--darkTheme; + } + + &.localMenuItem-isDisabled { + &:hover { + background-color: transparent; + color: $localNavButtonTextColor--darkTheme; + } + } + } + } + + .localMenuItem__icon { + margin-right: 5px; + margin-bottom: -1px; + } diff --git a/src/ui_framework/components/local_nav/_local_nav.scss b/src/ui_framework/components/local_nav/_local_nav.scss new file mode 100644 index 0000000000000..2e5b6f48a876d --- /dev/null +++ b/src/ui_framework/components/local_nav/_local_nav.scss @@ -0,0 +1,34 @@ + +/** + * 1. Match height of logo in side bar, but allow it to expand to accommodate + * dropdown. + */ +.localNav { + display: flex; + flex-direction: column; + justify-content: space-between; + min-height: 70px; /* 1 */ + color: $localNavTextColor; + background-color: $localNavBackgroundColor; + + @include darkTheme { + color: $localNavTextColor--darkTheme; + background-color: $localNavBackgroundColor--darkTheme; + } +} + +.localNavRow { + display: flex; + align-items: center; + justify-content: space-between; + height: 32px; +} + + .localNavRow__section { + height: 100%; + } + +.localNavRow--secondary { + height: 38px; + padding: 0 $localNavSideSpacing; +} diff --git a/src/ui_framework/components/local_nav/_local_search.scss b/src/ui_framework/components/local_nav/_local_search.scss new file mode 100644 index 0000000000000..103e2c889aacf --- /dev/null +++ b/src/ui_framework/components/local_nav/_local_search.scss @@ -0,0 +1,53 @@ + +$localSearchHeight: 30px; + +.localSearch { + display: flex; + width: 100%; + height: $localSearchHeight; +} + +.localSearchInput { + flex: 1 1 100%; + padding: 5px 15px; + font-size: $localNavFontSizeNormal; + color: $localNavTextColor; + background-color: $localSearchBackgroundColor; + border: 2px solid $localSearchBackgroundColor; + border-bottom-left-radius: 4px; + border-top-left-radius: 4px; + border-bottom-right-radius: 0; + border-top-right-radius: 0; + + &.localSearchInput-isInvalid { + border-color: $localSearchBorderColor-isInvalid; + } + + @include darkTheme { + color: $localNavTextColor--darkTheme; + background-color: $localSearchBackgroundColor--darkTheme; + border-color: $localSearchBackgroundColor--darkTheme; + + &.localSearchInput-isInvalid { + border-color: $localSearchBorderColor-isInvalid--darkTheme; + } + } +} + +.localSearchButton { + width: 43px; + height: $localSearchHeight; + font-size: $localNavFontSizeNormal; + color: $buttonTextColor; + background-color: $buttonBackgroundColor; + border: 0; + border-bottom-left-radius: 0; + border-top-left-radius: 0; + border-bottom-right-radius: 4px; + border-top-right-radius: 4px; + + @include darkTheme { + color: $buttonTextColor--darkTheme; + background-color: $buttonBackgroundColor--darkTheme; + } +} diff --git a/src/ui_framework/components/local_nav/_local_tabs.scss b/src/ui_framework/components/local_nav/_local_tabs.scss new file mode 100644 index 0000000000000..ce83b304cc9a2 --- /dev/null +++ b/src/ui_framework/components/local_nav/_local_tabs.scss @@ -0,0 +1,49 @@ +/** + * 1. We want the bottom border on selected tabs to be flush with the bottom of the container. + */ +.localTabs { + display: flex; + align-items: flex-end; // 1 + height: 100%; +} + + /** + * 1. Make sure the bottom border is flush with the bottom of the LocalNav. + */ + .localTab { + padding: 5px 0 6px 0; + font-size: 18px; + line-height: 22px; /* 1 */ + color: $localTabTextColor; + border-bottom: 2px solid transparent; + text-decoration: none; + cursor: pointer; + + &:hover, + &:active { + color: $localTabTextColor-isHover; + + @include darkTheme { + color: $localTabTextColor-isHover--darkTheme; + } + } + + &.localTab-isSelected { + color: $localTabTextColor-isSelected; + border-bottom-color: $localTabTextColor-isSelected; + cursor: default; + + @include darkTheme { + color: $localTabTextColor-isSelected--darkTheme; + border-bottom-color: $localTabTextColor-isSelected--darkTheme; + } + } + + & + & { + margin-left: 15px; + } + + @include darkTheme { + color: $localTabTextColor--darkTheme; + } + } diff --git a/src/ui_framework/components/local_nav/_local_title.scss b/src/ui_framework/components/local_nav/_local_title.scss new file mode 100644 index 0000000000000..34d62b00c5a6a --- /dev/null +++ b/src/ui_framework/components/local_nav/_local_title.scss @@ -0,0 +1,8 @@ +.localTitle { + display: flex; + align-items: center; + height: 100%; + padding-left: $localNavSideSpacing; + font-size: $localNavFontSizeNormal; + font-weight: bold; +} diff --git a/src/ui_framework/doc_site/build/index.html b/src/ui_framework/doc_site/build/index.html new file mode 100644 index 0000000000000..8bf6acd476348 --- /dev/null +++ b/src/ui_framework/doc_site/build/index.html @@ -0,0 +1,13 @@ + + + + + + + + + +
+ + + diff --git a/src/ui_framework/doc_site/src/actions/action_types.js b/src/ui_framework/doc_site/src/actions/action_types.js new file mode 100644 index 0000000000000..c92f35ceccb5a --- /dev/null +++ b/src/ui_framework/doc_site/src/actions/action_types.js @@ -0,0 +1,13 @@ + +import keyMirror from 'keymirror'; + +export default keyMirror({ + + // Source code viewer actions + OPEN_CODE_VIEWER: null, + UPDATE_CODE_VIEWER: null, + CLOSE_CODE_VIEWER: null, + REGISTER_CODE: null, + UNREGISTER_CODE: null, + +}); diff --git a/src/ui_framework/doc_site/src/actions/code_viewer/code_viewer_actions.js b/src/ui_framework/doc_site/src/actions/code_viewer/code_viewer_actions.js new file mode 100644 index 0000000000000..56b5a52e0399e --- /dev/null +++ b/src/ui_framework/doc_site/src/actions/code_viewer/code_viewer_actions.js @@ -0,0 +1,30 @@ + +import ActionTypes from '../action_types'; + +export default { + + openCodeViewer: slug => ({ + type: ActionTypes.OPEN_CODE_VIEWER, + slug, + }), + + updateCodeViewer: slug => ({ + type: ActionTypes.UPDATE_CODE_VIEWER, + slug, + }), + + closeCodeViewer: () => ({ + type: ActionTypes.CLOSE_CODE_VIEWER, + }), + + registerCode: code => ({ + type: ActionTypes.REGISTER_CODE, + code, + }), + + unregisterCode: code => ({ + type: ActionTypes.UNREGISTER_CODE, + code + }), + +}; diff --git a/src/ui_framework/doc_site/src/actions/index.js b/src/ui_framework/doc_site/src/actions/index.js new file mode 100644 index 0000000000000..22f997b682227 --- /dev/null +++ b/src/ui_framework/doc_site/src/actions/index.js @@ -0,0 +1,4 @@ + +export { + default as CodeViewerActions, +} from './code_viewer/code_viewer_actions'; diff --git a/src/ui_framework/doc_site/src/components/guide_code_viewer/_guide_code_viewer.scss b/src/ui_framework/doc_site/src/components/guide_code_viewer/_guide_code_viewer.scss new file mode 100644 index 0000000000000..80ababb96841d --- /dev/null +++ b/src/ui_framework/doc_site/src/components/guide_code_viewer/_guide_code_viewer.scss @@ -0,0 +1,114 @@ + +.guideCodeViewer { + position: fixed; + top: $guideNavHeight; + right: 0; + bottom: 0; + width: $guideCodeViewerWidth; + padding: 40px 20px 40px 0; + background-color: white; + transform: translateX($guideCodeViewerWidth); + transition: transform $guideCodeViewerTransition; + overflow: auto; + + @include scrollbar; + + @include whenNarrowerThan($normalBreakpoint) { + width: $guideCodeViewerSmallWidth; + } + + &.is-code-viewer-open { + transform: translateX(0); + } +} + + .guideCodeViewer__header { + padding-bottom: 10px; + line-height: $guideLineHeight; + border-bottom: 1px solid #d6d6d6; + font-size: 18px; + font-weight: 700; + margin-bottom: 10px; + } + + .guideCodeViewer__closeButton { + position: absolute; + top: 5px; + right: 5px; + cursor: pointer; + padding: 10px; + border-radius: 3px; + color: #6b7490; + + &:hover { + color: #2b52cc; + } + } + + + .guideCodeViewer__title { + padding-bottom: 6px; + border-bottom: 1px solid #d6d6d6; + line-height: $guideLineHeight; + font-size: 14px; + } + + .guideCodeViewer__content { + margin: 0 0 16px; + } + +// HLJS + +.hljs { + display: block; + padding: 15px 20px; + color: #637c84; + font-size: 14px; + line-height: 1.3; + font-family: 'Ubuntu Mono', monospace; +} + +.hljs-keyword { + color: #b58900; +} + +.hljs-function { + .hljs-keyword { + color: #268bd2; + } + + .hljs-title { + color: #7441c6; + } +} + +.hljs-built_in { + color: #268bd2; +} + +.hljs-string { + color: #36958e; +} + +.hljs-comment { + color: #9d9d9d; +} + +.hljs-number, +.hljs-literal { + color: #d84a7e; +} + +.hljs-tag { + .hljs-name { + color: #63a35c; + } + + .hljs-attr { + color: #795da3; + } + + .hljs-string { + color: #df5000; + } +} diff --git a/src/ui_framework/doc_site/src/components/guide_code_viewer/guide_code_viewer.jsx b/src/ui_framework/doc_site/src/components/guide_code_viewer/guide_code_viewer.jsx new file mode 100644 index 0000000000000..8001ef89166f8 --- /dev/null +++ b/src/ui_framework/doc_site/src/components/guide_code_viewer/guide_code_viewer.jsx @@ -0,0 +1,76 @@ + +import React, { + Component, + PropTypes, +} from 'react'; + +import classNames from 'classnames'; +import hljs from 'highlight.js'; + +export default class GuideCodeViewer extends Component { + + constructor(props) { + super(props); + } + + componentDidUpdate() { + if (this.refs.html) { + hljs.highlightBlock(this.refs.html); + } + + if (this.refs.javascript) { + hljs.highlightBlock(this.refs.javascript); + } + } + + renderSection(title, content, codeClass) { + if (content) { + return ( +
+
+ {title} +
+
+            
+              {content}
+            
+          
+
+ ); + } + } + + render() { + const classes = classNames('guideCodeViewer', { + 'is-code-viewer-open': this.props.isOpen, + }); + + return ( +
+
+ {this.props.title} +
+ +
+ + {this.renderSection('HTML', this.props.html, 'html')} + {this.renderSection('JavaScript', this.props.js, 'javascript')} +
+ ); + } + +} + +GuideCodeViewer.propTypes = { + isOpen: PropTypes.bool, + onClose: PropTypes.func, + title: PropTypes.string, + html: PropTypes.string, + js: PropTypes.string, +}; diff --git a/src/ui_framework/doc_site/src/components/guide_example/_guide_example.scss b/src/ui_framework/doc_site/src/components/guide_example/_guide_example.scss new file mode 100644 index 0000000000000..ec52942938a92 --- /dev/null +++ b/src/ui_framework/doc_site/src/components/guide_example/_guide_example.scss @@ -0,0 +1,3 @@ +.guideExample { + +} diff --git a/src/ui_framework/doc_site/src/components/guide_example/guide_example.jsx b/src/ui_framework/doc_site/src/components/guide_example/guide_example.jsx new file mode 100644 index 0000000000000..72508a5493a54 --- /dev/null +++ b/src/ui_framework/doc_site/src/components/guide_example/guide_example.jsx @@ -0,0 +1,77 @@ + +import React, { + Component, + PropTypes, +} from 'react'; + +import { + Slugify, +} from '../../services'; + +import { + GuidePage, + GuidePageSection, +} from '../'; + +export default class GuideExample extends Component { + + constructor(props, sections) { + super(props); + + this.sections = sections.map(section => Object.assign({}, section, { + slug: Slugify.one(section.title), + })); + } + + componentWillMount() { + this.sections.forEach(section => { + this.context.registerCode(section); + }); + } + + componentWillUnmount() { + this.sections.forEach(section => { + this.context.unregisterCode(section); + }); + } + + renderSections() { + return this.sections.map((section, index) => ( + + {section.description} + + )); + } + + render() { + return ( + + {this.renderSections()} + + ); + } + +} + +GuideExample.contextTypes = { + registerCode: PropTypes.func, + unregisterCode: PropTypes.func, +}; + +GuideExample.propTypes = { + route: PropTypes.object.isRequired, + sections: PropTypes.arrayOf(React.PropTypes.shape({ + title: React.PropTypes.string.isRequired, + description: React.PropTypes.any, + html: React.PropTypes.string.isRequired, + js: React.PropTypes.string, + })), +}; diff --git a/src/ui_framework/doc_site/src/components/guide_nav/_guide_nav.scss b/src/ui_framework/doc_site/src/components/guide_nav/_guide_nav.scss new file mode 100644 index 0000000000000..adb168888663a --- /dev/null +++ b/src/ui_framework/doc_site/src/components/guide_nav/_guide_nav.scss @@ -0,0 +1,67 @@ +.guideNav { + position: fixed; + z-index: 1; + top: 0; + left: 0; + right: 0; + height: $guideNavHeight; + padding: 0 20px; + background-color: #e8488b; + color: #ffffff; + box-shadow: + inset 0 -20px 18px rgba(#5a1029, 0.2), + inset 0 -5px 4px rgba(#5a1029, 0.3); + transition: height 0.3s ease; + overflow: hidden; + + &.is-guide-nav-open { + height: 100%; + } +} + + .guideNav__header { + display: flex; + align-items: center; + height: 60px; + } + + .guideNav__menu { + cursor: pointer; + margin-right: 10px; + padding: 10px; + border-radius: 3px; + + &.is-menu-button-pinned, + &:hover { + background-color: rgba(black, 0.15); + } + + &:active { + background-color: rgba(black, 0.2); + box-shadow: inset 0 2px 8px rgba(black, 0.2); + } + } + + .guideNav__title { + color: white; + text-decoration: none; + font-size: 18px; + } + + .guideNav__version { + margin-left: 10px; + font-weight: 300; + font-size: 14px; + } + + .guideNavItem { + color: white; + text-decoration: none; + font-size: 20px; + padding: 10px; + border-radius: 3px; + + &:hover { + background-color: rgba(black, 0.15); + } + } diff --git a/src/ui_framework/doc_site/src/components/guide_nav/guide_nav.jsx b/src/ui_framework/doc_site/src/components/guide_nav/guide_nav.jsx new file mode 100644 index 0000000000000..f6d7aacf9f1b1 --- /dev/null +++ b/src/ui_framework/doc_site/src/components/guide_nav/guide_nav.jsx @@ -0,0 +1,65 @@ + +import React, { + PropTypes, +} from 'react'; + +import { + Link, +} from 'react-router'; + +import classNames from 'classnames'; + +const GuideNav = props => { + const classes = classNames('guideNav', { + 'is-guide-nav-open': props.isNavOpen, + }); + + const buttonClasses = classNames('guideNav__menu fa fa-bars', { + 'is-menu-button-pinned': props.isNavOpen, + }); + + const navItems = props.items.map((item, index) => { + return ( + + {item.name} + + ); + }); + + return ( +
+
+
+ + Kibana UI Framework {props.version} + +
+ +
+ {navItems} +
+
+ ); +}; + +GuideNav.propTypes = { + isNavOpen: PropTypes.bool, + onToggleNav: PropTypes.func, + onClickNavItem: PropTypes.func, + version: PropTypes.string, + items: PropTypes.array, +}; + +export default GuideNav; diff --git a/src/ui_framework/doc_site/src/components/guide_page/_guide_page.scss b/src/ui_framework/doc_site/src/components/guide_page/_guide_page.scss new file mode 100644 index 0000000000000..21ae022cb3056 --- /dev/null +++ b/src/ui_framework/doc_site/src/components/guide_page/_guide_page.scss @@ -0,0 +1,15 @@ + +@import "../../variables"; + +.guidePage { + display: flex; +} + +.guidePageBody { + flex: 1 1 auto; + padding: 0 80px 0 80px + $guideSideNavWidth; + + @include whenNarrowerThan($normalBreakpoint) { + padding: 0 20px 0 $guideSideNavSmallWidth; + } +} diff --git a/src/ui_framework/doc_site/src/components/guide_page/guide_page.jsx b/src/ui_framework/doc_site/src/components/guide_page/guide_page.jsx new file mode 100644 index 0000000000000..dac8bc5120f5e --- /dev/null +++ b/src/ui_framework/doc_site/src/components/guide_page/guide_page.jsx @@ -0,0 +1,72 @@ + +import React, { + Component, + PropTypes, +} from 'react'; + +import { + Slugify, +} from '../../services'; + +import { + GuidePageSideNav, + GuidePageSideNavItem, +} from '../'; + +export default class GuidePage extends Component { + + constructor(props) { + super(props); + + this.onClickLink = this.onClickLink.bind(this); + } + + onClickLink(slug) { + // Scroll to element. + $('html, body').animate({ + scrollTop: $(`#${slug}`).offset().top - 100 + }, 250); + + // Load in code viewer. + this.context.updateCodeViewer(slug); + } + + renderSideNavMenu() { + // Traverse children and build side nav from it. + return this.props.children.map((section, index) => { + return ( + + {section.props.title} + + ); + }); + } + + render() { + return ( +
+ + {this.renderSideNavMenu()} + + +
+ {this.props.children} +
+
+ ); + } + +} + +GuidePage.contextTypes = { + updateCodeViewer: PropTypes.func, +}; + +GuidePage.propTypes = { + children: PropTypes.any, + title: PropTypes.string, +}; diff --git a/src/ui_framework/doc_site/src/components/guide_page_section/_guide_page_section.scss b/src/ui_framework/doc_site/src/components/guide_page_section/_guide_page_section.scss new file mode 100644 index 0000000000000..85a578f407ea8 --- /dev/null +++ b/src/ui_framework/doc_site/src/components/guide_page_section/_guide_page_section.scss @@ -0,0 +1,56 @@ + +@import "../../variables"; + +.guidePageSection { + margin-bottom: 40px; +} + + .guidePageSection__header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 10px; + line-height: $guideLineHeight; + border-bottom: 1px solid #d6d6d6; + } + + .guidePageSection__title { + font-size: 18px; + font-weight: 700; + } + + .guidePageSection__sourceButton { + line-height: 10px; + padding: 4px 10px; + background-color: #19a8e0; + color: white; + border-radius: 3px; + cursor: pointer; + + &:hover { + box-shadow: + inset 0 1px 0 rgba(#95e1ff, 1), + 0 2px 4px rgba(black, 0.2); + } + + &:active { + box-shadow: + inset 0 20px 20px rgba(black, 0.1), + inset 0 2px 8px rgba(black, 0.2); + } + } + + .guidePageSection__description { + font-size: 14px; + line-height: 21px; + } + + .guidePageSection__example { + & + & { + margin-top: 20px; + } + } + + .guidePageSection__example--standalone { + margin-top: 10px; + } diff --git a/src/ui_framework/doc_site/src/components/guide_page_section/guide_page_section.jsx b/src/ui_framework/doc_site/src/components/guide_page_section/guide_page_section.jsx new file mode 100644 index 0000000000000..aa3ea3e3dc423 --- /dev/null +++ b/src/ui_framework/doc_site/src/components/guide_page_section/guide_page_section.jsx @@ -0,0 +1,123 @@ + +import React, { + Component, + PropTypes, +} from 'react'; + +import classNames from 'classnames'; + +import { + JsInjector, +} from '../../services'; + +export default class GuidePageSection extends Component { + + constructor(props) { + super(props); + + this.onClickSource = this.onClickSource.bind(this); + } + + componentDidMount() { + // NOTE: This will cause a race condition if a GuidePage adds and removes + // GuidePageSection instances during its lifetime (e.g. if a user is allowed + // to click "add" and "delete" buttons to add and remove GuidePageSections). + // + // In such a race condition, we could end up with GuidePageSections with + // identical id values. + // + // As long as all GuidePageSection instances are added when a GuidePage + // is instantiated, and then they're all removed when a GuidePage is + // removed, we won't encounter this race condition. + if (this.props.js) { + this.scriptId = `${GuidePageSection.SCRIPT_ID}${GuidePageSection.count}`; + GuidePageSection.count++; + // JS injection must occur _after_ the component has been mounted, so + // the component DOM is available for the JS to manipulate. + JsInjector.inject(this.props.js, this.scriptId); + } + + function trimChildren(node) { + if (node.children.length > 0) { + [...node.children].forEach(trimChildren); + return; + } + node.textContent = node.textContent.trim(); + } + + trimChildren(this.refs.html); + trimChildren(this.refs.htmlDarkTheme); + } + + componentWillUnmount() { + JsInjector.remove(this.scriptId); + GuidePageSection.count--; + } + + onClickSource() { + this.context.openCodeViewer(this.props.slug); + } + + render() { + let description; + + if (this.props.children) { + description = ( +
+ {this.props.children} +
+ ); + } + + const exampleClasses = classNames('guidePageSection__example', { + 'guidePageSection__example--standalone': !this.props.children, + }); + + return ( +
+
+
+ {this.props.title} +
+
+
+ + {description} + +
+ +
+
+ ); + } + +} + +GuidePageSection.count = 0; +GuidePageSection.SCRIPT_ID = 'EXAMPLE_SCRIPT'; + +GuidePageSection.contextTypes = { + openCodeViewer: PropTypes.func, +}; + +GuidePageSection.propTypes = { + title: PropTypes.string, + slug: PropTypes.string, + html: PropTypes.string, + js: PropTypes.string, + children: PropTypes.any, +}; diff --git a/src/ui_framework/doc_site/src/components/guide_page_side_nav/_guide_page_side_nav.scss b/src/ui_framework/doc_site/src/components/guide_page_side_nav/_guide_page_side_nav.scss new file mode 100644 index 0000000000000..9a8a706544c81 --- /dev/null +++ b/src/ui_framework/doc_site/src/components/guide_page_side_nav/_guide_page_side_nav.scss @@ -0,0 +1,46 @@ + +.guidePageSideNav { + position: fixed; + top: 100px; + left: 0; + bottom: 0; + width: $guideSideNavWidth; + padding: 0 20px 30px 80px; + overflow: auto; + + @include scrollbar; + + @include whenNarrowerThan($normalBreakpoint) { + padding: 0 20px 30px 20px; + width: $guideSideNavSmallWidth; + } +} + + .guidePageSideNav__title { + padding-bottom: 10px; + margin-bottom: 10px; + font-size: 22px; + line-height: $guideLineHeight; + border-bottom: 1px solid #d6d6d6; + opacity: 0.8; + } + + .guidePageSideNavMenu { + line-height: $guideLineHeight; + } + + .guidePageSideNavMenu__item { + & + & { + margin-top: 6px; + } + } + + .guidePageSideNavMenu__itemLink { + cursor: pointer; + color: #6b7490; + text-decoration: none; + + &:hover { + color: #2b52cc; + } + } diff --git a/src/ui_framework/doc_site/src/components/guide_page_side_nav/guide_page_side_nav.jsx b/src/ui_framework/doc_site/src/components/guide_page_side_nav/guide_page_side_nav.jsx new file mode 100644 index 0000000000000..0d01d8de2ab20 --- /dev/null +++ b/src/ui_framework/doc_site/src/components/guide_page_side_nav/guide_page_side_nav.jsx @@ -0,0 +1,32 @@ + +import React, { + Component, + PropTypes, +} from 'react'; + +export default class GuidePageSideNav extends Component { + + constructor(props) { + super(props); + } + + render() { + return ( +
+
+ {this.props.title} +
+ +
+ {this.props.children} +
+
+ ); + } + +} + +GuidePageSideNav.propTypes = { + title: PropTypes.string, + children: PropTypes.any, +}; diff --git a/src/ui_framework/doc_site/src/components/guide_page_side_nav/guide_page_side_nav_item.jsx b/src/ui_framework/doc_site/src/components/guide_page_side_nav/guide_page_side_nav_item.jsx new file mode 100644 index 0000000000000..2d439ba5da0e1 --- /dev/null +++ b/src/ui_framework/doc_site/src/components/guide_page_side_nav/guide_page_side_nav_item.jsx @@ -0,0 +1,38 @@ + +import React, { + Component, + PropTypes, +} from 'react'; + +export default class GuidePageSideNavItem extends Component { + + constructor(props) { + super(props); + + this.onClick = this.onClick.bind(this); + } + + onClick() { + this.props.onClick(this.props.slug); + } + + render() { + return ( +
+
+ {this.props.children} +
+
+ ); + } + +} + +GuidePageSideNavItem.propTypes = { + slug: PropTypes.string, + children: PropTypes.any, + onClick: PropTypes.func, +}; diff --git a/src/ui_framework/doc_site/src/components/index.js b/src/ui_framework/doc_site/src/components/index.js new file mode 100644 index 0000000000000..0785980c1dcf8 --- /dev/null +++ b/src/ui_framework/doc_site/src/components/index.js @@ -0,0 +1,21 @@ + +export * from './guide_code_viewer/guide_code_viewer.jsx'; +export { default as GuideCodeViewer } from './guide_code_viewer/guide_code_viewer.jsx'; + +export * from './guide_example/guide_example.jsx'; +export { default as GuideExample } from './guide_example/guide_example.jsx'; + +export * from './guide_nav/guide_nav.jsx'; +export { default as GuideNav } from './guide_nav/guide_nav.jsx'; + +export * from './guide_page/guide_page.jsx'; +export { default as GuidePage } from './guide_page/guide_page.jsx'; + +export * from './guide_page_section/guide_page_section.jsx'; +export { default as GuidePageSection } from './guide_page_section/guide_page_section.jsx'; + +export * from './guide_page_side_nav/guide_page_side_nav.jsx'; +export { default as GuidePageSideNav } from './guide_page_side_nav/guide_page_side_nav.jsx'; + +export * from './guide_page_side_nav/guide_page_side_nav_item.jsx'; +export { default as GuidePageSideNavItem } from './guide_page_side_nav/guide_page_side_nav_item.jsx'; diff --git a/src/ui_framework/doc_site/src/index.js b/src/ui_framework/doc_site/src/index.js new file mode 100644 index 0000000000000..a68210b67c3d0 --- /dev/null +++ b/src/ui_framework/doc_site/src/index.js @@ -0,0 +1,79 @@ +require('./main.scss'); + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; +import { + Router, + useRouterHistory, +} from 'react-router'; +import { syncHistoryWithStore } from 'react-router-redux'; +import createHashHistory from 'history/lib/createHashHistory'; + +// Store. +import configureStore from './store/configure_store'; + +// Guide views. +import AppContainer from './views/app_container'; +import HomeView from './views/home/home_view.jsx'; +import NotFoundView from './views/not_found/not_found_view.jsx'; + +import { + Routes, +} from './services'; + +const store = configureStore(); +const browserHistory = useRouterHistory(createHashHistory)({ + queryKey: false, +}); +const history = syncHistoryWithStore(browserHistory, store); + +const childRoutes = Routes.getAppRoutes(); +childRoutes.push({ + path: '*', + component: NotFoundView, + name: 'Page Not Found', +}); + +const routes = [{ + path: '/', + component: AppContainer, + indexRoute: { + component: HomeView, + source: 'views/home/HomeView.jsx', + }, + childRoutes, +}]; + +// Update document title with route name. +const onRouteEnter = route => { + const leafRoute = route.routes[route.routes.length - 1]; + document.title = leafRoute.name ? + `Kibana UI Framework - ${leafRoute.name}` : + 'Kibana UI Framework'; +}; + +const syncTitleWithRoutes = routesList => { + if (!routesList) return; + routesList.forEach(route => { + route.onEnter = onRouteEnter; // eslint-disable-line no-param-reassign + if (route.indexRoute) { + // Index routes have a weird relationship with their "parent" routes, + // so it seems we need to give their own onEnter hooks. + route.indexRoute.onEnter = onRouteEnter; // eslint-disable-line no-param-reassign + } + syncTitleWithRoutes(route.childRoutes); + }); +}; + +syncTitleWithRoutes(routes); + +ReactDOM.render( + + + , + document.getElementById('guide') +); diff --git a/src/ui_framework/doc_site/src/main.scss b/src/ui_framework/doc_site/src/main.scss new file mode 100644 index 0000000000000..c20b7457ae37c --- /dev/null +++ b/src/ui_framework/doc_site/src/main.scss @@ -0,0 +1,28 @@ + +@import "../../components/index"; +@import "./views/app"; +@import "./components/guide_code_viewer/guide_code_viewer"; +@import "./components/guide_nav/guide_nav"; +@import "./components/guide_page/guide_page"; +@import "./components/guide_page_section/guide_page_section"; +@import "./components/guide_page_side_nav/guide_page_side_nav"; + +* { + box-sizing: border-box; +} + +html, +body { + height: 100%; +} + +/** + * 1. Insane line-height makes it easier to notice when components are relying + * on styles inherited from body. + */ +body { + font-family: 'Lato', 'Helvetica Neue', sans-serif; + background: #ffffff; + line-height: 40px; /* 1 */ + margin: 0; +} diff --git a/src/ui_framework/doc_site/src/services/example/createExample.js b/src/ui_framework/doc_site/src/services/example/createExample.js new file mode 100644 index 0000000000000..185b0ec908162 --- /dev/null +++ b/src/ui_framework/doc_site/src/services/example/createExample.js @@ -0,0 +1,16 @@ + +import { + GuideExample, +} from '../../components'; + +export default function createExample(examples) { + class Example extends GuideExample { + constructor(props) { + super(props, examples); + } + } + + Example.propTypes = Object.assign({}, GuideExample.propTypes); + + return Example; +} diff --git a/src/ui_framework/doc_site/src/services/index.js b/src/ui_framework/doc_site/src/services/index.js new file mode 100644 index 0000000000000..f5e87594f507a --- /dev/null +++ b/src/ui_framework/doc_site/src/services/index.js @@ -0,0 +1,12 @@ + +export * from './example/createExample'; +export { default as createExample } from './example/createExample'; + +export * from './js_injector/js_injector'; +export { default as JsInjector } from './js_injector/js_injector'; + +export * from './routes/routes'; +export { default as Routes } from './routes/routes'; + +export * from './string/slugify'; +export { default as Slugify } from './string/slugify'; diff --git a/src/ui_framework/doc_site/src/services/js_injector/js_injector.js b/src/ui_framework/doc_site/src/services/js_injector/js_injector.js new file mode 100644 index 0000000000000..f77986272390c --- /dev/null +++ b/src/ui_framework/doc_site/src/services/js_injector/js_injector.js @@ -0,0 +1,21 @@ + +import $ from 'jquery'; + +const ID_ATTRIBUTE = 'injected-js-tag-id'; + +export default { + + inject(js, id) { + if (id) { + $(`[${ID_ATTRIBUTE}=${id}]`).remove(); + } + + const script = $(``); + $('body').append(script); + }, + + remove(id) { + $(`[${ID_ATTRIBUTE}=${id}]`).remove(); + }, + +}; diff --git a/src/ui_framework/doc_site/src/services/routes/Routes.js b/src/ui_framework/doc_site/src/services/routes/Routes.js new file mode 100644 index 0000000000000..2fe47ff9a9319 --- /dev/null +++ b/src/ui_framework/doc_site/src/services/routes/Routes.js @@ -0,0 +1,19 @@ + +import Slugify from '../string/slugify'; + +import LocalNavExample + from '../../views/local_nav/local_nav_example.jsx'; + +// Component route names should match the component name exactly. +const components = [{ + name: 'LocalNav', + component: LocalNavExample, +}]; + +export default { + components: Slugify.each(components, 'name', 'path'), + getAppRoutes: function getAppRoutes() { + const list = this.components; + return list.slice(0); + }, +}; diff --git a/src/ui_framework/doc_site/src/services/string/slugify.js b/src/ui_framework/doc_site/src/services/string/slugify.js new file mode 100644 index 0000000000000..829b37640f16b --- /dev/null +++ b/src/ui_framework/doc_site/src/services/string/slugify.js @@ -0,0 +1,27 @@ + + +/** + * Lowercases input and replaces spaces with hyphens: + * e.g. 'GridView Example' -> 'gridview-example' + */ +function one(str) { + const parts = str + .toLowerCase() + .replace(/[-]+/g, ' ') + .replace(/[^\w^\s]+/g, '') + .replace(/ +/g, ' ').split(' '); + return parts.join('-'); +} + +function each(items, src, dest) { + return items.map(item => { + const _item = item; + _item[dest] = one(_item[src]); + return _item; + }); +} + +export default { + one, + each, +}; diff --git a/src/ui_framework/doc_site/src/store/configure_store.js b/src/ui_framework/doc_site/src/store/configure_store.js new file mode 100644 index 0000000000000..08f186671abdd --- /dev/null +++ b/src/ui_framework/doc_site/src/store/configure_store.js @@ -0,0 +1,35 @@ +import { + applyMiddleware, + createStore, + compose, +} from 'redux'; +import thunk from 'redux-thunk'; +import { browserHistory } from 'react-router'; +import { + routerMiddleware, + routerReducer, +} from 'react-router-redux'; + +import codeViewerReducer from './reducers/code_viewer_reducer'; + +/** + * @param {Object} initialState An object defining the application's initial + * state. + */ +export default function configureStore(initialState) { + function rootReducer(state = {}, action) { + return { + routing: routerReducer(state.routing, action), + codeViewer: codeViewerReducer(state.codeViewer, action), + }; + } + + const finalStore = compose( + applyMiddleware( + thunk, + routerMiddleware(browserHistory) + ) + )(createStore)(rootReducer, initialState); + + return finalStore; +} diff --git a/src/ui_framework/doc_site/src/store/reducers/code_viewer_reducer.js b/src/ui_framework/doc_site/src/store/reducers/code_viewer_reducer.js new file mode 100644 index 0000000000000..e4d27ad181375 --- /dev/null +++ b/src/ui_framework/doc_site/src/store/reducers/code_viewer_reducer.js @@ -0,0 +1,69 @@ + +import ActionTypes from '../../actions/action_types'; + +const defaultState = { + isOpen: false, + codesBySlug: {}, + code: undefined, +}; + +export default function codeViewerReducer(state = defaultState, action) { + switch (action.type) { + case ActionTypes.OPEN_CODE_VIEWER: { + const newCode = state.codesBySlug[action.slug]; + + if (state.code === newCode) { + // If we are opening the existing code, then close the viewer. + return Object.assign({}, state, { + isOpen: false, + code: undefined, + }); + } + + return Object.assign({}, state, { + isOpen: true, + code: newCode, + }); + } + + case ActionTypes.UPDATE_CODE_VIEWER: { + if (state.isOpen) { + return Object.assign({}, state, { + code: state.codesBySlug[action.slug], + }); + } + return state; + } + + case ActionTypes.CLOSE_CODE_VIEWER: { + return Object.assign({}, state, { + isOpen: false, + code: undefined, + }); + } + + case ActionTypes.REGISTER_CODE: { + const codesBySlug = Object.assign({}, state.codesBySlug, { + [action.code.slug]: action.code, + }); + + return Object.assign({}, state, { + codesBySlug + }); + } + + case ActionTypes.UNREGISTER_CODE: { + const codesBySlug = Object.assign({}, state.codesBySlug); + delete codesBySlug[action.code.slug]; + + return Object.assign({}, state, { + codesBySlug + }); + } + + default: + break; + } + + return state; +} diff --git a/src/ui_framework/doc_site/src/variables.scss b/src/ui_framework/doc_site/src/variables.scss new file mode 100644 index 0000000000000..8376a7ef8d59b --- /dev/null +++ b/src/ui_framework/doc_site/src/variables.scss @@ -0,0 +1,39 @@ + +$guideLineHeight: 24px; +$guideNavHeight: 60px; +$guideSideNavWidth: 400px; +$guideSideNavSmallWidth: 220px; +$guideCodeViewerWidth: 700px; +$guideCodeViewerSmallWidth: 580px; +$guideCodeViewerTransition: 0.2s ease; + +$normalBreakpoint: 1900px; + +@mixin whenNarrowerThan($browserWidth) { + @media only screen and (max-width: #{$browserWidth}) { + @content; + } +} + +@mixin whenWiderThan($browserWidth) { + @media only screen and (min-width: #{$browserWidth}) { + @content; + } +} + +@mixin scrollbar($color: rgba(#454D58, 0.4)) { + &::-webkit-scrollbar { + width: 16px; + height: 16px; + } + + &::-webkit-scrollbar-thumb { + background-color: $color; + border: 6px solid transparent; + background-clip: content-box; + } + + &::-webkit-scrollbar-track { + background-color: transparent; + } +} diff --git a/src/ui_framework/doc_site/src/views/_app.scss b/src/ui_framework/doc_site/src/views/_app.scss new file mode 100644 index 0000000000000..47e0554a94572 --- /dev/null +++ b/src/ui_framework/doc_site/src/views/_app.scss @@ -0,0 +1,22 @@ + +@import "../variables"; +@import "./home/home_view"; + +.guide { + display: flex; + height: 100%; +} + +.guideContent { + flex: 1 1 auto; + padding-top: 100px; + transition: padding-right $guideCodeViewerTransition; + + &.is-code-viewer-open { + padding-right: $guideCodeViewerWidth; + + @include whenNarrowerThan($normalBreakpoint) { + padding-right: $guideCodeViewerSmallWidth; + } + } +} diff --git a/src/ui_framework/doc_site/src/views/app_container.js b/src/ui_framework/doc_site/src/views/app_container.js new file mode 100644 index 0000000000000..bc134de2e60ef --- /dev/null +++ b/src/ui_framework/doc_site/src/views/app_container.js @@ -0,0 +1,30 @@ + +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import AppView from './app_view.jsx'; + +import { + CodeViewerActions, +} from '../actions'; + +function mapStateToProps(state, ownProps) { + return { + routes: ownProps.routes, + isCodeViewerOpen: state.codeViewer.isOpen, + code: state.codeViewer.code, + }; +} + +function mapDispatchToProps(dispatch) { + const actions = { + openCodeViewer: CodeViewerActions.openCodeViewer, + updateCodeViewer: CodeViewerActions.updateCodeViewer, + closeCodeViewer: CodeViewerActions.closeCodeViewer, + registerCode: CodeViewerActions.registerCode, + unregisterCode: CodeViewerActions.unregisterCode, + }; + + return bindActionCreators(actions, dispatch); +} + +export default connect(mapStateToProps, mapDispatchToProps)(AppView); diff --git a/src/ui_framework/doc_site/src/views/app_view.jsx b/src/ui_framework/doc_site/src/views/app_view.jsx new file mode 100644 index 0000000000000..8a71f12667545 --- /dev/null +++ b/src/ui_framework/doc_site/src/views/app_view.jsx @@ -0,0 +1,113 @@ + +import React, { + Component, + PropTypes, +} from 'react'; + +import classNames from 'classnames'; + +import { + Routes, +} from '../services'; + +import { + GuideCodeViewer, + GuideNav, +} from '../components'; + +// Inject version into header. +const pkg = require('json!../../../../../package.json'); + +export default class AppView extends Component { + + constructor(props) { + super(props); + + this.state = { + isNavOpen: false, + }; + + this.onClickNavItem = this.onClickNavItem.bind(this); + this.onToggleNav = this.onToggleNav.bind(this); + this.onCloseCodeViewer = this.onCloseCodeViewer.bind(this); + } + + getChildContext() { + return { + openCodeViewer: this.props.openCodeViewer, + updateCodeViewer: this.props.updateCodeViewer, + registerCode: this.props.registerCode, + unregisterCode: this.props.unregisterCode, + }; + } + + onClickNavItem() { + this.setState({ + isNavOpen: false, + }); + } + + onCloseCodeViewer() { + this.props.closeCodeViewer(); + } + + onToggleNav() { + this.setState({ + isNavOpen: !this.state.isNavOpen, + }) + } + + render() { + const contentClasses = classNames('guideContent', { + 'is-code-viewer-open': this.props.isCodeViewerOpen, + }); + + return ( +
+ + +
+ {this.props.children} +
+ + +
+ ); + } + +} + +AppView.childContextTypes = { + openCodeViewer: PropTypes.func, + updateCodeViewer: PropTypes.func, + registerCode: PropTypes.func, + unregisterCode: PropTypes.func, +}; + +AppView.propTypes = { + children: PropTypes.any, + routes: PropTypes.array.isRequired, + openCodeViewer: PropTypes.func, + updateCodeViewer: PropTypes.func, + closeCodeViewer: PropTypes.func, + registerCode: PropTypes.func, + unregisterCode: PropTypes.func, + isCodeViewerOpen: PropTypes.bool, + code: PropTypes.object, +}; + +AppView.defaultProps = { + code: {}, +}; diff --git a/src/ui_framework/doc_site/src/views/home/_home_view.scss b/src/ui_framework/doc_site/src/views/home/_home_view.scss new file mode 100644 index 0000000000000..66dc7fcba01e6 --- /dev/null +++ b/src/ui_framework/doc_site/src/views/home/_home_view.scss @@ -0,0 +1,28 @@ + +@import "../../variables"; + +.guideHome { + display: flex; + justify-content: center; +} + + .guideHome__panel { + width: 100%; + max-width: 600px; + max-height: 500px; + padding: 60px; + margin-bottom: 20px; + border-radius: 3px; + background-color: #e8e8e8; + line-height: $guideLineHeight; + } + + .guideHome__panelTitle { + font-weight: 700; + font-size: 22px; + margin-bottom: 20px; + } + + .guideHome__panelText { + font-size: 18px; + } diff --git a/src/ui_framework/doc_site/src/views/home/home_view.jsx b/src/ui_framework/doc_site/src/views/home/home_view.jsx new file mode 100644 index 0000000000000..715fbc36c0aee --- /dev/null +++ b/src/ui_framework/doc_site/src/views/home/home_view.jsx @@ -0,0 +1,28 @@ + +import React, { + Component, +} from 'react'; + +export default class HomeView extends Component { + + constructor(props) { + super(props); + } + + render() { + return ( +
+
+
+ Welcome to the Kibana UI Framework +
+ +
+ Get started by clicking the menu button in the top left corner of the screen. +
+
+
+ ); + } + +} diff --git a/src/ui_framework/doc_site/src/views/local_nav/local_nav_breadcrumbs/local_nav_breadcrumbs.html b/src/ui_framework/doc_site/src/views/local_nav/local_nav_breadcrumbs/local_nav_breadcrumbs.html new file mode 100644 index 0000000000000..2078dea3b9f08 --- /dev/null +++ b/src/ui_framework/doc_site/src/views/local_nav/local_nav_breadcrumbs/local_nav_breadcrumbs.html @@ -0,0 +1,41 @@ + +
+
+
+ +
+ +
+
+
+ New +
+ +
+ Save +
+ +
+ Open +
+ + +
+
+
+
diff --git a/src/ui_framework/doc_site/src/views/local_nav/local_nav_dropdown/local_nav_dropdown.html b/src/ui_framework/doc_site/src/views/local_nav/local_nav_dropdown/local_nav_dropdown.html new file mode 100644 index 0000000000000..ec39fbe1fd214 --- /dev/null +++ b/src/ui_framework/doc_site/src/views/local_nav/local_nav_dropdown/local_nav_dropdown.html @@ -0,0 +1,127 @@ + +
+
+
+ +
+ +
+
+
+ New +
+ +
+ Save +
+ +
+ Open +
+ + +
+
+
+ +
+ + + + +
Dropdown title
+ + +
+ Here's some help text to explain the purpose of the dropdown. +
+ + +
+ Here's some warning text in case the user has something misconfigured. +
+ +
+ +
+
+ Header for a section of content +
+
+ + + +
+ +
+ +
+
+ Header for another section of content +
+ +
+ + + + + +
+ Here are some notes to explain the purpose of this section of the dropdown. +
+
+
+ +
+
+ + +
+
+
diff --git a/src/ui_framework/doc_site/src/views/local_nav/local_nav_dropdown_panels/local_nav_dropdown_panels.html b/src/ui_framework/doc_site/src/views/local_nav/local_nav_dropdown_panels/local_nav_dropdown_panels.html new file mode 100644 index 0000000000000..abc628ad178b3 --- /dev/null +++ b/src/ui_framework/doc_site/src/views/local_nav/local_nav_dropdown_panels/local_nav_dropdown_panels.html @@ -0,0 +1,86 @@ + +
+
+
+ +
+ +
+
+
+ New +
+ +
+ Save +
+ +
+ Open +
+ + +
+
+
+ +
+ + + +
+ +
+ +
Left panel
+ + +
+ Here's some help text to explain the purpose of the dropdown. +
+
+ + +
+ +
Right panel
+ + +
+ Here's some help text to explain the purpose of the dropdown. +
+
+
+
+ +
+
+ + +
+
+
diff --git a/src/ui_framework/doc_site/src/views/local_nav/local_nav_example.jsx b/src/ui_framework/doc_site/src/views/local_nav/local_nav_example.jsx new file mode 100644 index 0000000000000..79d01c68eadf3 --- /dev/null +++ b/src/ui_framework/doc_site/src/views/local_nav/local_nav_example.jsx @@ -0,0 +1,56 @@ + +import React from 'react'; + +import { + createExample, +} from '../../services'; + +export default createExample([{ + title: 'Simple', + description: ( +

Here's a simple LocalNav with a Title in the top left corner and Menu in the top right.

+ ), + html: require('./local_nav_simple/local_nav_simple.html'), +}, { + title: 'Breadcrumbs', + description: ( +

You can replace the Title with Breadcrumbs.

+ ), + html: require('./local_nav_breadcrumbs/local_nav_breadcrumbs.html'), +}, { + title: 'Search', + description: ( +

You can add a Search component for filtering results.

+ ), + html: require('./local_nav_search/local_nav_search.html'), +}, { + title: 'Invalid Search', + html: require('./local_nav_search_error/local_nav_search_error.html'), +}, { + title: 'Selected and disabled Menu Item states', + description: ( +
+

When the user selects a Menu Item, additional content can be displayed inside of a Dropdown.

+

Menu Items can also be disabled, in which case they become non-interactive.

+
+ ), + html: require('./local_nav_menu_item_states/local_nav_menu_item_states.html'), +}, { + title: 'Dropdown', + description: ( +

Selecting a Menu Item will commonly result in an open Dropdown.

+ ), + html: require('./local_nav_dropdown/local_nav_dropdown.html'), +}, { + title: 'Dropdown panels', + description: ( +

You can split the Dropdown into side-by-side Panels.

+ ), + html: require('./local_nav_dropdown_panels/local_nav_dropdown_panels.html'), +}, { + title: 'Tabs', + description: ( +

You can display Tabs for navigating local content.

+ ), + html: require('./local_nav_tabs/local_nav_tabs.html'), +}]); diff --git a/src/ui_framework/doc_site/src/views/local_nav/local_nav_menu_item_states/local_nav_menu_item_states.html b/src/ui_framework/doc_site/src/views/local_nav/local_nav_menu_item_states/local_nav_menu_item_states.html new file mode 100644 index 0000000000000..25211acd98469 --- /dev/null +++ b/src/ui_framework/doc_site/src/views/local_nav/local_nav_menu_item_states/local_nav_menu_item_states.html @@ -0,0 +1,55 @@ + +
+
+
+ +
+ +
+
+
+ New +
+ +
+ Save +
+ +
+ Open +
+ + +
+
+
+ +
+
+ + +
+
+
diff --git a/src/ui_framework/doc_site/src/views/local_nav/local_nav_search/local_nav_search.html b/src/ui_framework/doc_site/src/views/local_nav/local_nav_search/local_nav_search.html new file mode 100644 index 0000000000000..01ab1e7017da3 --- /dev/null +++ b/src/ui_framework/doc_site/src/views/local_nav/local_nav_search/local_nav_search.html @@ -0,0 +1,55 @@ + +
+
+
+ +
+ +
+
+ + + + + + + +
+
+
+ +
+
+ + +
+
+
diff --git a/src/ui_framework/doc_site/src/views/local_nav/local_nav_search_error/local_nav_search_error.html b/src/ui_framework/doc_site/src/views/local_nav/local_nav_search_error/local_nav_search_error.html new file mode 100644 index 0000000000000..9f73b820990b1 --- /dev/null +++ b/src/ui_framework/doc_site/src/views/local_nav/local_nav_search_error/local_nav_search_error.html @@ -0,0 +1,55 @@ + +
+
+
+ +
+ +
+
+ + + + + + + +
+
+
+ +
+
+ + +
+
+
diff --git a/src/ui_framework/doc_site/src/views/local_nav/local_nav_simple/local_nav_simple.html b/src/ui_framework/doc_site/src/views/local_nav/local_nav_simple/local_nav_simple.html new file mode 100644 index 0000000000000..d35d5da8adbb7 --- /dev/null +++ b/src/ui_framework/doc_site/src/views/local_nav/local_nav_simple/local_nav_simple.html @@ -0,0 +1,31 @@ + +
+
+
+
+ Untitled Document +
+
+ +
+
+
+ New +
+ +
+ Save +
+ +
+ Open +
+ + +
+
+
+
diff --git a/src/ui_framework/doc_site/src/views/local_nav/local_nav_tabs/local_nav_tabs.html b/src/ui_framework/doc_site/src/views/local_nav/local_nav_tabs/local_nav_tabs.html new file mode 100644 index 0000000000000..8b0b277b5d2b7 --- /dev/null +++ b/src/ui_framework/doc_site/src/views/local_nav/local_nav_tabs/local_nav_tabs.html @@ -0,0 +1,57 @@ + +
+
+
+ +
+ +
+
+ + + + + + + +
+
+
+ + +
diff --git a/src/ui_framework/doc_site/src/views/not_found/not_found_view.jsx b/src/ui_framework/doc_site/src/views/not_found/not_found_view.jsx new file mode 100644 index 0000000000000..417a9fcec4b4b --- /dev/null +++ b/src/ui_framework/doc_site/src/views/not_found/not_found_view.jsx @@ -0,0 +1,20 @@ + +import React, { + Component, +} from 'react'; + +export default class NotFoundView extends Component { + + constructor(props) { + super(props); + } + + render() { + return ( +
+

Page not found.

+
+ ); + } + +} diff --git a/src/ui_framework/doc_site/webpack.config.js b/src/ui_framework/doc_site/webpack.config.js new file mode 100644 index 0000000000000..b5d7fcb3018ab --- /dev/null +++ b/src/ui_framework/doc_site/webpack.config.js @@ -0,0 +1,39 @@ +var path = require('path'); + +module.exports = { + devtool: 'source-map', + + entry: { + guide: './src/ui_framework/doc_site/src/index.js' + }, + + output: { + path: path.resolve(__dirname, 'src/ui_framework/doc_site/build'), + filename: 'bundle.js' + }, + + resolve: { + root: [ + path.resolve(__dirname, 'src/ui_framework/doc_site') + ] + }, + + module: { + loaders: [{ + test: /\.jsx?$/, + loader: 'babel', + exclude: /node_modules/ + }, { + test: /\.scss$/, + loaders: ['style', 'css', 'sass'], + exclude: /node_modules/ + }, { + test: /\.html$/, + loader: 'html', + exclude: /node_modules/ + }, { + test: require.resolve('jquery'), + loader: 'expose?jQuery!expose?$' + }] + } +};