Commit c5b63658 by Dominik Prokop Committed by GitHub

grafana/toolkit: Add support for extensions styling (#17938)

* Add support for static files

* Use url-loader in single css setup

* Add postcss setup

* Expose emotion to plugins and externalise it in toolkit

* Add readme note about emotion
parent 17b5f48c
...@@ -80,8 +80,8 @@ Adidtionaly, you can also provide additional Jest config via package.json file. ...@@ -80,8 +80,8 @@ Adidtionaly, you can also provide additional Jest config via package.json file.
- [`snapshotSerializers`](https://jest-bot.github.io/jest/docs/configuration.html#snapshotserializers-array-string) - [`snapshotSerializers`](https://jest-bot.github.io/jest/docs/configuration.html#snapshotserializers-array-string)
## Working with CSS ## Working with CSS & static assets
We support pure css, SASS and CSS in JS approach (via Emotion). We support pure css, SASS and CSS in JS approach (via Emotion). All static assets referenced in your code (i.e. images) should be placed under `src/static` directory and referenced using relative paths.
1. Single css/sass file 1. Single css/sass file
Create your css/sass file and import it in your plugin entry point (typically module.ts): Create your css/sass file and import it in your plugin entry point (typically module.ts):
...@@ -91,13 +91,37 @@ import 'path/to/your/css_or_sass ...@@ -91,13 +91,37 @@ import 'path/to/your/css_or_sass
``` ```
The styles will be injected via `style` tag during runtime. The styles will be injected via `style` tag during runtime.
2. Theme css/sass files Note, that imported static assets will be inlined as base64 URIs. *This can be a subject of change in the future!*
If you want to provide different stylesheets for Dark/Light theme, create `dark.[css|scss]` and `light.[css|scss]` files in `src/styles` directory of your plugin. Based on that we will generate stylesheets that will end up in `dist/styles` directory.
2. Theme specific css/sass files
If you want to provide different stylesheets for dark/light theme, create `dark.[css|scss]` and `light.[css|scss]` files in `src/styles` directory of your plugin. Based on that we will generate stylesheets that will end up in `dist/styles` directory.
TODO: add note about loadPluginCss TODO: add note about loadPluginCss
3. Emotion 3. Emotion
TODO Starting from Grafana 6.2 our suggested way of styling plugins is by using [Emotion](https://emotion.sh). It's a css-in-js library that we use internaly at Grafana. The biggest advantage of using Emotion is that you will get access to Grafana Theme variables.
To use start using Emotion you first need to add it to your plugin dependencies:
```
yarn add "@emotion/core"@10.0.14
```
Then, import `css` function from emotion:
```import { css } from 'emotion'```
And start implementing your styles:
```tsx
const MyComponent = () => {
return <div className={css`background: red;`} />
}
```
Using themes: TODO, for now please refer to [internal guide](../../style_guides/themes.md)
> NOTE: We do not support Emotion's `css` prop. Use className instead!
## Prettier [todo] ## Prettier [todo]
......
...@@ -38,6 +38,7 @@ ...@@ -38,6 +38,7 @@
"copy-webpack-plugin": "5.0.3", "copy-webpack-plugin": "5.0.3",
"css-loader": "^3.0.0", "css-loader": "^3.0.0",
"execa": "^1.0.0", "execa": "^1.0.0",
"file-loader": "^4.0.0",
"glob": "^7.1.4", "glob": "^7.1.4",
"html-loader": "0.5.5", "html-loader": "0.5.5",
"inquirer": "^6.3.1", "inquirer": "^6.3.1",
...@@ -49,6 +50,9 @@ ...@@ -49,6 +50,9 @@
"node-sass": "^4.12.0", "node-sass": "^4.12.0",
"optimize-css-assets-webpack-plugin": "^5.0.3", "optimize-css-assets-webpack-plugin": "^5.0.3",
"ora": "^3.4.0", "ora": "^3.4.0",
"postcss-flexbugs-fixes": "4.1.0",
"postcss-loader": "3.0.0",
"postcss-preset-env": "6.6.0",
"prettier": "^1.17.1", "prettier": "^1.17.1",
"react-dev-utils": "^9.0.1", "react-dev-utils": "^9.0.1",
"replace-in-file": "^4.1.0", "replace-in-file": "^4.1.0",
...@@ -65,6 +69,7 @@ ...@@ -65,6 +69,7 @@
"tslint": "5.14.0", "tslint": "5.14.0",
"tslint-config-prettier": "^1.18.0", "tslint-config-prettier": "^1.18.0",
"typescript": "3.5.1", "typescript": "3.5.1",
"url-loader": "^2.0.1",
"webpack": "4.35.0" "webpack": "4.35.0"
}, },
"resolutions": { "resolutions": {
......
import { getPluginJson, validatePluginJson } from './pluginValidation';
describe('pluginValdation', () => {
describe('plugin.json', () => {
test('missing plugin.json file', () => {
expect(() => getPluginJson(`${__dirname}/mocks/missing-plugin-json`)).toThrow('plugin.json file is missing!');
});
});
describe('validatePluginJson', () => {
test('missing plugin.json file', () => {
expect(() => validatePluginJson({})).toThrow('Plugin id is missing in plugin.json');
});
});
});
import path = require('path');
interface PluginJSONSchema {
id: string;
}
export const validatePluginJson = (pluginJson: any) => {
if (!pluginJson.id) {
throw new Error('Plugin id is missing in plugin.json');
}
};
export const getPluginJson = (root: string = process.cwd()): PluginJSONSchema => {
let pluginJson;
try {
pluginJson = require(path.resolve(root, 'src/plugin.json'));
} catch (e) {
throw new Error('plugin.json file is missing!');
}
validatePluginJson(pluginJson);
return pluginJson as PluginJSONSchema;
};
...@@ -6,7 +6,7 @@ const TerserPlugin = require('terser-webpack-plugin'); ...@@ -6,7 +6,7 @@ const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
import * as webpack from 'webpack'; import * as webpack from 'webpack';
import { hasThemeStylesheets, getStyleLoaders, getStylesheetEntries } from './webpack/loaders'; import { hasThemeStylesheets, getStyleLoaders, getStylesheetEntries, getFileLoaders } from './webpack/loaders';
interface WebpackConfigurationOptions { interface WebpackConfigurationOptions {
watch?: boolean; watch?: boolean;
...@@ -82,8 +82,8 @@ const getCommonPlugins = (options: WebpackConfigurationOptions) => { ...@@ -82,8 +82,8 @@ const getCommonPlugins = (options: WebpackConfigurationOptions) => {
{ from: '../LICENSE', to: '.' }, { from: '../LICENSE', to: '.' },
{ from: 'img/*', to: '.' }, { from: 'img/*', to: '.' },
{ from: '**/*.json', to: '.' }, { from: '**/*.json', to: '.' },
{ from: '**/*.svg', to: '.' }, // { from: '**/*.svg', to: '.' },
{ from: '**/*.png', to: '.' }, // { from: '**/*.png', to: '.' },
{ from: '**/*.html', to: '.' }, { from: '**/*.html', to: '.' },
], ],
{ logLevel: options.watch ? 'silent' : 'warn' } { logLevel: options.watch ? 'silent' : 'warn' }
...@@ -131,6 +131,7 @@ export const getWebpackConfig: WebpackConfigurationGetter = options => { ...@@ -131,6 +131,7 @@ export const getWebpackConfig: WebpackConfigurationGetter = options => {
filename: '[name].js', filename: '[name].js',
path: path.join(process.cwd(), 'dist'), path: path.join(process.cwd(), 'dist'),
libraryTarget: 'amd', libraryTarget: 'amd',
publicPath: '/',
}, },
performance: { hints: false }, performance: { hints: false },
...@@ -139,6 +140,7 @@ export const getWebpackConfig: WebpackConfigurationGetter = options => { ...@@ -139,6 +140,7 @@ export const getWebpackConfig: WebpackConfigurationGetter = options => {
'jquery', 'jquery',
'moment', 'moment',
'slate', 'slate',
'emotion',
'prismjs', 'prismjs',
'slate-plain-serializer', 'slate-plain-serializer',
'slate-react', 'slate-react',
...@@ -146,6 +148,7 @@ export const getWebpackConfig: WebpackConfigurationGetter = options => { ...@@ -146,6 +148,7 @@ export const getWebpackConfig: WebpackConfigurationGetter = options => {
'react-dom', 'react-dom',
'rxjs', 'rxjs',
'd3', 'd3',
'angular',
'@grafana/ui', '@grafana/ui',
'@grafana/runtime', '@grafana/runtime',
'@grafana/data', '@grafana/data',
...@@ -190,6 +193,7 @@ export const getWebpackConfig: WebpackConfigurationGetter = options => { ...@@ -190,6 +193,7 @@ export const getWebpackConfig: WebpackConfigurationGetter = options => {
loader: 'html-loader', loader: 'html-loader',
}, },
}, },
...getFileLoaders(),
], ],
}, },
optimization, optimization,
......
import { getPluginJson } from '../utils/pluginValidation';
const path = require('path');
const fs = require('fs'); const fs = require('fs');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
...@@ -30,7 +33,7 @@ export const getStylesheetEntries = (root: string = process.cwd()) => { ...@@ -30,7 +33,7 @@ export const getStylesheetEntries = (root: string = process.cwd()) => {
}; };
export const hasThemeStylesheets = (root: string = process.cwd()) => { export const hasThemeStylesheets = (root: string = process.cwd()) => {
const stylesheetsPaths = [`${root}/src/styles/light`, `${root}/src/styles/dark`]; const stylesheetsPaths = getStylesheetPaths(root);
const stylesheetsSummary: boolean[] = []; const stylesheetsSummary: boolean[] = [];
const result = stylesheetsPaths.reduce((acc, current) => { const result = stylesheetsPaths.reduce((acc, current) => {
...@@ -68,25 +71,66 @@ export const getStyleLoaders = () => { ...@@ -68,25 +71,66 @@ export const getStyleLoaders = () => {
const executiveLoader = shouldExtractCss const executiveLoader = shouldExtractCss
? { ? {
loader: MiniCssExtractPlugin.loader, loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '../',
},
} }
: 'style-loader'; : 'style-loader';
const cssLoader = { const cssLoaders = [
{
loader: 'css-loader', loader: 'css-loader',
options: { options: {
importLoaders: 1, importLoaders: 1,
sourceMap: true, sourceMap: true,
}, },
}; },
{
loader: 'postcss-loader',
options: {
plugins: () => [
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({
autoprefixer: { flexbox: 'no-2009', grid: true },
}),
],
},
},
];
return [ return [
{ {
test: /\.css$/, test: /\.css$/,
use: [executiveLoader, cssLoader], use: [executiveLoader, ...cssLoaders],
}, },
{ {
test: /\.scss$/, test: /\.scss$/,
use: [executiveLoader, cssLoader, 'sass-loader'], use: [executiveLoader, ...cssLoaders, 'sass-loader'],
},
];
};
export const getFileLoaders = () => {
const shouldExtractCss = hasThemeStylesheets();
// const pluginJson = getPluginJson();
return [
{
test: /\.(png|jpe?g|gif|svg)$/,
use: [
shouldExtractCss
? {
loader: 'file-loader',
options: {
outputPath: 'static',
name: '[name].[hash:8].[ext]',
},
}
: // When using single css import images are inlined as base64 URIs in the result bundle
{
loader: 'url-loader',
},
],
}, },
]; ];
}; };
...@@ -27,6 +27,7 @@ import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv'; ...@@ -27,6 +27,7 @@ import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv';
import impressionSrv from 'app/core/services/impression_srv'; import impressionSrv from 'app/core/services/impression_srv';
import builtInPlugins from './built_in_plugins'; import builtInPlugins from './built_in_plugins';
import * as d3 from 'd3'; import * as d3 from 'd3';
import * as emotion from 'emotion';
import * as grafanaData from '@grafana/data'; import * as grafanaData from '@grafana/data';
import * as grafanaUI from '@grafana/ui'; import * as grafanaUI from '@grafana/ui';
import * as grafanaRuntime from '@grafana/runtime'; import * as grafanaRuntime from '@grafana/runtime';
...@@ -90,6 +91,7 @@ exposeToPlugin('slate-react', slateReact); ...@@ -90,6 +91,7 @@ exposeToPlugin('slate-react', slateReact);
exposeToPlugin('slate-plain-serializer', slatePlain); exposeToPlugin('slate-plain-serializer', slatePlain);
exposeToPlugin('react', react); exposeToPlugin('react', react);
exposeToPlugin('react-dom', reactDom); exposeToPlugin('react-dom', reactDom);
exposeToPlugin('emotion', emotion);
exposeToPlugin('app/features/dashboard/impression_store', { exposeToPlugin('app/features/dashboard/impression_store', {
impressions: impressionSrv, impressions: impressionSrv,
......
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