diff --git a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx index 519e755b474..7f43936481f 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx +++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx @@ -8,6 +8,7 @@ interface Props { autoHideDuration?: number; autoMaxHeight?: string; hideTracksWhenNotNeeded?: boolean; + autoHeightMin?: number | string; } /** @@ -21,6 +22,7 @@ export class CustomScrollbar extends PureComponent { autoHideDuration: 200, autoMaxHeight: '100%', hideTracksWhenNotNeeded: false, + autoHeightMin: '0' }; render() { @@ -32,7 +34,6 @@ export class CustomScrollbar extends PureComponent { autoHeight={true} // These autoHeightMin & autoHeightMax options affect firefox and chrome differently. // Before these where set to inhert but that caused problems with cut of legends in firefox - autoHeightMin={'0'} autoHeightMax={autoMaxHeight} renderTrackHorizontal={props =>
} renderTrackVertical={props =>
} diff --git a/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap b/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap index 60b4a2e0aa5..aabe3dd98c5 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap +++ b/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap @@ -7,7 +7,7 @@ exports[`CustomScrollbar renders correctly 1`] = ` Object { "height": "auto", "maxHeight": "100%", - "minHeight": "0", + "minHeight": 0, "overflow": "hidden", "position": "relative", "width": "100%", @@ -24,7 +24,7 @@ exports[`CustomScrollbar renders correctly 1`] = ` "marginBottom": 0, "marginRight": 0, "maxHeight": "calc(100% + 0px)", - "minHeight": "calc(0 + 0px)", + "minHeight": 0, "overflow": "scroll", "position": "relative", "right": undefined, diff --git a/public/app/core/components/Animations/FadeIn.tsx b/public/app/core/components/Animations/FadeIn.tsx index e12f22486f1..ea9a92d5f0f 100644 --- a/public/app/core/components/Animations/FadeIn.tsx +++ b/public/app/core/components/Animations/FadeIn.tsx @@ -1,4 +1,4 @@ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; import Transition from 'react-transition-group/Transition'; interface Props { @@ -8,7 +8,7 @@ interface Props { unmountOnExit?: boolean; } -export const FadeIn: SFC = props => { +export const FadeIn: FC = props => { const defaultStyle = { transition: `opacity ${props.duration}ms linear`, opacity: 0, diff --git a/public/app/core/components/Footer/Footer.tsx b/public/app/core/components/Footer/Footer.tsx new file mode 100644 index 00000000000..101168beb66 --- /dev/null +++ b/public/app/core/components/Footer/Footer.tsx @@ -0,0 +1,50 @@ +import React, { FC } from 'react'; +import { Tooltip } from '@grafana/ui'; + +interface Props { + appName: string; + buildVersion: string; + buildCommit: string; + newGrafanaVersionExists: boolean; + newGrafanaVersion: string; +} + +export const Footer: FC = React.memo(({appName, buildVersion, buildCommit, newGrafanaVersionExists, newGrafanaVersion}) => { + return ( + + ); +}); + +export default Footer; diff --git a/public/app/core/components/LayoutSelector/LayoutSelector.tsx b/public/app/core/components/LayoutSelector/LayoutSelector.tsx index d9e00102438..3afa1f931f2 100644 --- a/public/app/core/components/LayoutSelector/LayoutSelector.tsx +++ b/public/app/core/components/LayoutSelector/LayoutSelector.tsx @@ -1,4 +1,4 @@ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; export type LayoutMode = LayoutModes.Grid | LayoutModes.List; @@ -12,7 +12,7 @@ interface Props { onLayoutModeChanged: (mode: LayoutMode) => {}; } -const LayoutSelector: SFC = props => { +const LayoutSelector: FC = props => { const { mode, onLayoutModeChanged } = props; return (
diff --git a/public/app/core/components/Page/Page.tsx b/public/app/core/components/Page/Page.tsx new file mode 100644 index 00000000000..8c9a5595cb7 --- /dev/null +++ b/public/app/core/components/Page/Page.tsx @@ -0,0 +1,75 @@ +// Libraries +import React, { Component } from 'react'; +import config from 'app/core/config'; +import { NavModel } from 'app/types'; +import { getTitleFromNavModel } from 'app/core/selectors/navModel'; + +// Components +import PageHeader from '../PageHeader/PageHeader'; +import Footer from '../Footer/Footer'; +import PageContents from './PageContents'; +import { CustomScrollbar } from '@grafana/ui'; + +interface Props { + title?: string; + children: JSX.Element[] | JSX.Element; + navModel: NavModel; +} + +class Page extends Component { + private bodyClass = 'is-react'; + private body = document.body; + static Header = PageHeader; + static Contents = PageContents; + + componentDidMount() { + this.body.classList.add(this.bodyClass); + this.updateTitle(); + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.title !== this.props.title) { + this.updateTitle(); + } + } + + componentWillUnmount() { + this.body.classList.remove(this.bodyClass); + } + + updateTitle = () => { + const title = this.getPageTitle; + document.title = title ? title + ' - Grafana' : 'Grafana'; + } + + get getPageTitle () { + const { navModel } = this.props; + if (navModel) { + return getTitleFromNavModel(navModel) || undefined; + } + return undefined; + } + + render() { + const { navModel } = this.props; + const { buildInfo } = config; + return ( +
+ +
+ + {this.props.children} +
+
+
+
+ ); + } +} + +export default Page; diff --git a/public/app/core/components/Page/PageContents.tsx b/public/app/core/components/Page/PageContents.tsx new file mode 100644 index 00000000000..6970857d383 --- /dev/null +++ b/public/app/core/components/Page/PageContents.tsx @@ -0,0 +1,26 @@ +// Libraries +import React, { Component } from 'react'; + +// Components +import PageLoader from '../PageLoader/PageLoader'; + +interface Props { + isLoading?: boolean; + children: JSX.Element[] | JSX.Element; +} + +class PageContents extends Component { + + render() { + const { isLoading } = this.props; + + return ( +
+ {isLoading && } + {this.props.children} +
+ ); + } +} + +export default PageContents; diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx index c176095afa4..83066054f88 100644 --- a/public/app/core/components/PageHeader/PageHeader.tsx +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { FormEvent } from 'react'; import { NavModel, NavModelItem } from 'app/types'; import classNames from 'classnames'; import appEvents from 'app/core/app_events'; @@ -12,8 +12,8 @@ const SelectNav = ({ main, customCss }: { main: NavModelItem; customCss: string return navItem.active === true; }); - const gotoUrl = evt => { - const element = evt.target; + const gotoUrl = (evt: FormEvent) => { + const element = evt.target as HTMLSelectElement; const url = element.options[element.selectedIndex].value; appEvents.emit('location-change', { href: url }); }; diff --git a/public/app/core/components/PageLoader/PageLoader.tsx b/public/app/core/components/PageLoader/PageLoader.tsx index dcb67dde220..3182695e5e5 100644 --- a/public/app/core/components/PageLoader/PageLoader.tsx +++ b/public/app/core/components/PageLoader/PageLoader.tsx @@ -1,10 +1,10 @@ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; interface Props { - pageName: string; + pageName?: string; } -const PageLoader: SFC = ({ pageName }) => { +const PageLoader: FC = ({ pageName }) => { const loadingText = `Loading ${pageName}...`; return (
diff --git a/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx b/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx index 86e15923bda..a2c06eef9f5 100644 --- a/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx +++ b/public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx @@ -1,4 +1,4 @@ -import React, { SFC, ReactNode, PureComponent } from 'react'; +import React, { FC, ReactNode, PureComponent } from 'react'; import { Tooltip } from '@grafana/ui'; interface ToggleButtonGroupProps { @@ -29,7 +29,7 @@ interface ToggleButtonProps { tooltip?: string; } -export const ToggleButton: SFC = ({ +export const ToggleButton: FC = ({ children, selected, className = '', diff --git a/public/app/core/components/sidemenu/DropDownChild.tsx b/public/app/core/components/sidemenu/DropDownChild.tsx index 1a577d185e5..41aa794999e 100644 --- a/public/app/core/components/sidemenu/DropDownChild.tsx +++ b/public/app/core/components/sidemenu/DropDownChild.tsx @@ -1,10 +1,10 @@ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; export interface Props { child: any; } -const DropDownChild: SFC = props => { +const DropDownChild: FC = props => { const { child } = props; const listItemClassName = child.divider ? 'divider' : ''; diff --git a/public/app/core/components/sidemenu/SideMenuDropDown.tsx b/public/app/core/components/sidemenu/SideMenuDropDown.tsx index 7cd7554f82c..4231e992b19 100644 --- a/public/app/core/components/sidemenu/SideMenuDropDown.tsx +++ b/public/app/core/components/sidemenu/SideMenuDropDown.tsx @@ -1,11 +1,11 @@ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; import DropDownChild from './DropDownChild'; interface Props { link: any; } -const SideMenuDropDown: SFC = props => { +const SideMenuDropDown: FC = props => { const { link } = props; return (
    diff --git a/public/app/core/components/sidemenu/SignIn.tsx b/public/app/core/components/sidemenu/SignIn.tsx index 17dd913823a..50b3aef2d9b 100644 --- a/public/app/core/components/sidemenu/SignIn.tsx +++ b/public/app/core/components/sidemenu/SignIn.tsx @@ -1,6 +1,6 @@ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; -const SignIn: SFC = () => { +const SignIn: FC = () => { const loginUrl = `login?redirect=${encodeURIComponent(window.location.pathname)}`; return (
    diff --git a/public/app/core/components/sidemenu/TopSection.tsx b/public/app/core/components/sidemenu/TopSection.tsx index c6bf5df8242..827b868ea67 100644 --- a/public/app/core/components/sidemenu/TopSection.tsx +++ b/public/app/core/components/sidemenu/TopSection.tsx @@ -1,9 +1,9 @@ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; import _ from 'lodash'; import TopSectionItem from './TopSectionItem'; import config from '../../config'; -const TopSection: SFC = () => { +const TopSection: FC = () => { const navTree = _.cloneDeep(config.bootData.navTree); const mainLinks = _.filter(navTree, item => !item.hideFromMenu); diff --git a/public/app/core/components/sidemenu/TopSectionItem.tsx b/public/app/core/components/sidemenu/TopSectionItem.tsx index 7b3bf96dce8..0aca32c3ba3 100644 --- a/public/app/core/components/sidemenu/TopSectionItem.tsx +++ b/public/app/core/components/sidemenu/TopSectionItem.tsx @@ -1,11 +1,11 @@ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; import SideMenuDropDown from './SideMenuDropDown'; export interface Props { link: any; } -const TopSectionItem: SFC = props => { +const TopSectionItem: FC = props => { const { link } = props; return (
    diff --git a/public/app/core/config.ts b/public/app/core/config.ts index 13d84772ecf..0aa159af84d 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -6,6 +6,8 @@ export interface BuildInfo { commit: string; isEnterprise: boolean; env: string; + latestVersion: string; + hasUpdate: boolean; } export class Settings { diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index aa508616962..7d745b58002 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -41,3 +41,7 @@ export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel) return getNotFoundModel(); } + +export const getTitleFromNavModel = (navModel: NavModel) => { + return `${navModel.main.text}${navModel.node.text ? ': ' + navModel.node.text : '' }`; +}; diff --git a/public/app/features/api-keys/ApiKeysPage.test.tsx b/public/app/features/api-keys/ApiKeysPage.test.tsx index 54200234ddc..cd640b5a357 100644 --- a/public/app/features/api-keys/ApiKeysPage.test.tsx +++ b/public/app/features/api-keys/ApiKeysPage.test.tsx @@ -6,7 +6,14 @@ import { getMultipleMockKeys, getMockKey } from './__mocks__/apiKeysMock'; const setup = (propOverrides?: object) => { const props: Props = { - navModel: {} as NavModel, + navModel: { + main: { + text: 'Configuration' + }, + node: { + text: 'Api Keys' + } + } as NavModel, apiKeys: [] as ApiKey[], searchQuery: '', hasFetched: false, diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index e14873fa9f6..41b9b0c8a55 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -6,8 +6,7 @@ import { NavModel, ApiKey, NewApiKey, OrgRole } from 'app/types'; import { getNavModel } from 'app/core/selectors/navModel'; import { getApiKeys, getApiKeysCount } from './state/selectors'; import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions'; -import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import PageLoader from 'app/core/components/PageLoader/PageLoader'; +import Page from 'app/core/components/Page/Page'; import SlideDown from 'app/core/components/Animations/SlideDown'; import ApiKeysAddedModal from './ApiKeysAddedModal'; import config from 'app/core/config'; @@ -240,18 +239,17 @@ export class ApiKeysPage extends PureComponent { const { hasFetched, navModel, apiKeysCount } = this.props; return ( -
    - - {hasFetched ? ( - apiKeysCount > 0 ? ( - this.renderApiKeyList() - ) : ( - this.renderEmptyList() - ) - ) : ( - - )} -
    + + + {hasFetched && ( + apiKeysCount > 0 ? ( + this.renderApiKeyList() + ) : ( + this.renderEmptyList() + ) + )} + + ); } } diff --git a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap index 7ede9618250..f40894426ae 100644 --- a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap +++ b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap @@ -1,132 +1,152 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Render should render API keys table if there are any keys 1`] = ` -
    - + - -
    + `; exports[`Render should render CTA if there are no API keys 1`] = ` -
    - -
    + - - -
    + - -
    - Add API Key -
    -
    -
    + + +
    + Add API Key +
    +
    - - Key name - - -
    -
    - - Role - - - +
    +
    + + Role + + + - -
    -
    -
    +
    - Add - + +
    -
    - -
    - -
    -
    + +
    + +
+ + `; diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx index d42b48fe1d6..66a942f0afc 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx @@ -1,11 +1,11 @@ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; import { PanelMenuItem } from '@grafana/ui'; interface Props { children: any; } -export const PanelHeaderMenuItem: SFC = props => { +export const PanelHeaderMenuItem: FC = props => { const isSubMenu = props.type === 'submenu'; const isDivider = props.type === 'divider'; return isDivider ? ( diff --git a/public/app/features/dashboard/panel_editor/DataSourceOption.tsx b/public/app/features/dashboard/panel_editor/DataSourceOption.tsx index 9a3ce527510..e4bbcfffe1d 100644 --- a/public/app/features/dashboard/panel_editor/DataSourceOption.tsx +++ b/public/app/features/dashboard/panel_editor/DataSourceOption.tsx @@ -1,4 +1,4 @@ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; import { Tooltip } from '@grafana/ui'; interface Props { @@ -10,7 +10,7 @@ interface Props { tooltipInfo?: any; } -export const DataSourceOptions: SFC = ({ label, placeholder, name, value, onChange, tooltipInfo }) => { +export const DataSourceOptions: FC = ({ label, placeholder, name, value, onChange, tooltipInfo }) => { const dsOption = (
diff --git a/public/app/features/datasources/DashboardsTable.tsx b/public/app/features/datasources/DashboardsTable.tsx index b732782c23b..077dc1dba63 100644 --- a/public/app/features/datasources/DashboardsTable.tsx +++ b/public/app/features/datasources/DashboardsTable.tsx @@ -1,4 +1,4 @@ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; import { PluginDashboard } from '../../types'; export interface Props { @@ -7,7 +7,7 @@ export interface Props { onRemove: (dashboard) => void; } -const DashboardsTable: SFC = ({ dashboards, onImport, onRemove }) => { +const DashboardsTable: FC = ({ dashboards, onImport, onRemove }) => { function buttonText(dashboard: PluginDashboard) { return dashboard.revision !== dashboard.importedRevision ? 'Update' : 'Re-import'; } diff --git a/public/app/features/datasources/DataSourcesListPage.test.tsx b/public/app/features/datasources/DataSourcesListPage.test.tsx index 0ea716d62c9..33f5790978d 100644 --- a/public/app/features/datasources/DataSourcesListPage.test.tsx +++ b/public/app/features/datasources/DataSourcesListPage.test.tsx @@ -10,7 +10,14 @@ const setup = (propOverrides?: object) => { dataSources: [] as DataSource[], layoutMode: LayoutModes.Grid, loadDataSources: jest.fn(), - navModel: {} as NavModel, + navModel: { + main: { + text: 'Configuration' + }, + node: { + text: 'Data Sources' + } + } as NavModel, dataSourcesCount: 0, searchQuery: '', setDataSourcesSearchQuery: jest.fn(), diff --git a/public/app/features/datasources/DataSourcesListPage.tsx b/public/app/features/datasources/DataSourcesListPage.tsx index 6a292d63e53..884df929319 100644 --- a/public/app/features/datasources/DataSourcesListPage.tsx +++ b/public/app/features/datasources/DataSourcesListPage.tsx @@ -1,15 +1,15 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; -import PageHeader from '../../core/components/PageHeader/PageHeader'; -import PageLoader from 'app/core/components/PageLoader/PageLoader'; -import OrgActionBar from '../../core/components/OrgActionBar/OrgActionBar'; -import EmptyListCTA from '../../core/components/EmptyListCTA/EmptyListCTA'; +import Page from 'app/core/components/Page/Page'; +import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar'; +import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import DataSourcesList from './DataSourcesList'; -import { DataSource, NavModel } from 'app/types'; -import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector'; +import { DataSource, NavModel, StoreState } from 'app/types'; +import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector'; import { loadDataSources, setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/actions'; -import { getNavModel } from '../../core/selectors/navModel'; +import { getNavModel } from 'app/core/selectors/navModel'; + import { getDataSources, getDataSourcesCount, @@ -67,30 +67,30 @@ export class DataSourcesListPage extends PureComponent { }; return ( -
- -
- {!hasFetched && } - {hasFetched && dataSourcesCount === 0 && } - {hasFetched && - dataSourcesCount > 0 && [ - setDataSourcesLayoutMode(mode)} - setSearchQuery={query => setDataSourcesSearchQuery(query)} - linkButton={linkButton} - key="action-bar" - />, - , - ]} -
-
+ + + <> + {hasFetched && dataSourcesCount === 0 && } + {hasFetched && + dataSourcesCount > 0 && [ + setDataSourcesLayoutMode(mode)} + setSearchQuery={query => setDataSourcesSearchQuery(query)} + linkButton={linkButton} + key="action-bar" + />, + , + ]} + + + ); } } -function mapStateToProps(state) { +function mapStateToProps(state: StoreState) { return { navModel: getNavModel(state.navIndex, 'datasources'), dataSources: getDataSources(state.dataSources), diff --git a/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap b/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap index c26ac50fed8..63998d43870 100644 --- a/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap +++ b/public/app/features/datasources/__snapshots__/DataSourcesListPage.test.tsx.snap @@ -1,12 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Render should render action bar and datasources 1`] = ` -
- -
+ -
-
+ + `; exports[`Render should render component 1`] = ` -
- + -
- -
-
+ `; diff --git a/public/app/features/datasources/settings/BasicSettings.tsx b/public/app/features/datasources/settings/BasicSettings.tsx index 56d4570e3a3..77c4f3d13c6 100644 --- a/public/app/features/datasources/settings/BasicSettings.tsx +++ b/public/app/features/datasources/settings/BasicSettings.tsx @@ -1,4 +1,4 @@ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; import { FormLabel } from '@grafana/ui'; import { Switch } from '../../../core/components/Switch/Switch'; @@ -9,7 +9,7 @@ export interface Props { onDefaultChange: (value: boolean) => void; } -const BasicSettings: SFC = ({ dataSourceName, isDefault, onDefaultChange, onNameChange }) => { +const BasicSettings: FC = ({ dataSourceName, isDefault, onDefaultChange, onNameChange }) => { return (
diff --git a/public/app/features/datasources/settings/ButtonRow.tsx b/public/app/features/datasources/settings/ButtonRow.tsx index cb70b29c706..6b85e21405c 100644 --- a/public/app/features/datasources/settings/ButtonRow.tsx +++ b/public/app/features/datasources/settings/ButtonRow.tsx @@ -1,4 +1,4 @@ -import React, { SFC } from 'react'; +import React, { FC } from 'react'; export interface Props { isReadOnly: boolean; @@ -6,7 +6,7 @@ export interface Props { onSubmit: (event) => void; } -const ButtonRow: SFC = ({ isReadOnly, onDelete, onSubmit }) => { +const ButtonRow: FC = ({ isReadOnly, onDelete, onSubmit }) => { return (