diff --git a/.drone.yml b/.drone.yml index e027b23aa5f..16caa00cb3a 100644 --- a/.drone.yml +++ b/.drone.yml @@ -506,15 +506,13 @@ steps: - test-backend - test-frontend -- name: release-next-npm-packages +- name: release-canary-npm-packages image: grafana/build-container:1.3.0 commands: - - ./node_modules/.bin/lerna bootstrap - - echo "//registry.npmjs.org/:_authToken=$${NPM_TOKEN}" >> ~/.npmrc - - ./scripts/circle-release-next-packages.sh + - ./scripts/circle-release-canary-packages.sh environment: - NPM_TOKEN: - from_secret: npm_token + GITHUB_PACKAGE_TOKEN: + from_secret: github_package_token depends_on: - end-to-end-tests diff --git a/Dockerfile b/Dockerfile index 3e9a4e96a3b..da5d72a9567 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,6 +64,7 @@ RUN export GF_GID_NAME=$(getent group $GF_GID | cut -d':' -f1) && \ mkdir -p "$GF_PATHS_PROVISIONING/datasources" \ "$GF_PATHS_PROVISIONING/dashboards" \ "$GF_PATHS_PROVISIONING/notifiers" \ + "$GF_PATHS_PROVISIONING/plugins" \ "$GF_PATHS_LOGS" \ "$GF_PATHS_PLUGINS" \ "$GF_PATHS_DATA" && \ diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index 04c5132f94a..00ed09edb09 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -58,6 +58,7 @@ RUN mkdir -p "$GF_PATHS_HOME/.aws" && \ mkdir -p "$GF_PATHS_PROVISIONING/datasources" \ "$GF_PATHS_PROVISIONING/dashboards" \ "$GF_PATHS_PROVISIONING/notifiers" \ + "$GF_PATHS_PROVISIONING/plugins" \ "$GF_PATHS_LOGS" \ "$GF_PATHS_PLUGINS" \ "$GF_PATHS_DATA" && \ diff --git a/contribute/style-guides/documentation-style-guide.md b/contribute/style-guides/documentation-style-guide.md index 3e224196276..bb21b47485a 100644 --- a/contribute/style-guides/documentation-style-guide.md +++ b/contribute/style-guides/documentation-style-guide.md @@ -115,7 +115,6 @@ If that is the case, then use the following format for headings: - Grafana, Loki, and Prometheus are always capitalized unless part of a code block. - API names are always Title Case, followed by "API"—for example, "Dashboard Permissions API" -- Git is always capitalized, unless part of a code block. - Abbreviations are always capitalized (such as API, HTTP, ID, JSON, SQL, or URL) unless they are part of a code block. - Menu and submenu titles always use sentence case: capitalize the first word, and lowercase the rest. - "Dashboards" when referring to the submenu title. @@ -125,6 +124,22 @@ If that is the case, then use the following format for headings: - Lowercase "dashboards" when referring to multiple dashboards. - **Exceptions:** If a term is lowercased in the Grafana UI, then match the UI. +#### Git, GitHub + +Git is always capitalized, unless part of a code block. GitHub is the correct spelling and capitalization. + +#### Integrations + +In general, "integration" is not capitalized. Only capitalize it if it is capitalized in the UI or part of a proper noun, like the name of a specific integration. + +The first letter of the name of an integration is always capitalized, even if the original named source is lowercase. + +**Examples:** +- MySQL Integration +- CockroachDB Integration +- Etcd Integration +- I installed an integration on my local Grafana. + ### Links and references When referencing another document, use "Refer to" rather than alternatives such as "See" or "Check out." diff --git a/devenv/docker/blocks/influxdb/docker-compose.yaml b/devenv/docker/blocks/influxdb/docker-compose.yaml index 0a8c7c2d7de..a83e1553cce 100644 --- a/devenv/docker/blocks/influxdb/docker-compose.yaml +++ b/devenv/docker/blocks/influxdb/docker-compose.yaml @@ -8,6 +8,7 @@ environment: INFLUXDB_ADMIN_USER: grafana INFLUXDB_ADMIN_PASSWORD: grafana + INFLUXDB_DB: site volumes: - ./docker/blocks/influxdb/influxdb.conf:/etc/influxdb/influxdb.conf diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 98913aa9f60..1ce057d93ca 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -147,19 +147,20 @@ Since not all datasources have the same configuration settings we only have the | tlsAuthWithCACert | boolean | _All_ | Enable TLS authentication using CA cert | | tlsSkipVerify | boolean | _All_ | Controls whether a client verifies the server's certificate chain and host name. | | graphiteVersion | string | Graphite | Graphite version | -| timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL and MSSQL | Lowest interval/step value that should be used for this data source | -| httpMode | string | Influxdb | HTTP Method. 'GET', 'POST', defaults to GET | -| httpMethod | string | Prometheus | HTTP Method. 'GET', 'POST', defaults to GET | +| timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL and MSSQL | Lowest interval/step value that should be used for this data source. | +| httpMode | string | Influxdb | HTTP Method. 'GET', 'POST', defaults to GET | +| httpMethod | string | Prometheus | HTTP Method. 'GET', 'POST', defaults to GET | | esVersion | number | Elasticsearch | Elasticsearch version as a number (2/5/56/60/70) | | timeField | string | Elasticsearch | Which field that should be used as timestamp | | interval | string | Elasticsearch | Index date time format. nil(No Pattern), 'Hourly', 'Daily', 'Weekly', 'Monthly' or 'Yearly' | | logMessageField | string | Elasticsearch | Which field should be used as the log message | | logLevelField | string | Elasticsearch | Which field should be used to indicate the priority of the log message | -| sigV4AuthType | string | Elasticsearch | SigV4 auth provider. default/credentials/keys | -| sigV4ExternalId | string | Elasticsearch | Optional SigV4 External ID | -| sigV4AssumeRoleArn | string | Elasticsearch | Optional SigV4 ARN role to assume | -| sigV4Region | string | Elasticsearch | SigV4 AWS region | -| sigV4Profile | string | Elasticsearch | Optional SigV4 credentials profile | +| sigV4Auth | boolean | Elasticsearch | Enable usage of SigV4 | +| sigV4AuthType | string | Elasticsearch | SigV4 auth provider. default/credentials/keys | +| sigV4ExternalId | string | Elasticsearch | Optional SigV4 External ID | +| sigV4AssumeRoleArn | string | Elasticsearch | Optional SigV4 ARN role to assume | +| sigV4Region | string | Elasticsearch | SigV4 AWS region | +| sigV4Profile | string | Elasticsearch | Optional SigV4 credentials profile | | authType | string | Cloudwatch | Auth provider. default/credentials/keys | | externalId | string | Cloudwatch | Optional External ID | | assumeRoleArn | string | Cloudwatch | Optional ARN role to assume | @@ -194,7 +195,7 @@ Secure json data is a map of settings that will be encrypted with [secret key]({ | basicAuthPassword | string | _All_ | password for basic authentication | | accessKey | string | Cloudwatch | Access key for connecting to Cloudwatch | | secretKey | string | Cloudwatch | Secret key for connecting to Cloudwatch | -| sigV4AccessKey | string | Elasticsearch | SigV4 access key. Required when using keys auth provider | +| sigV4AccessKey | string | Elasticsearch | SigV4 access key. Required when using keys auth provider | | sigV4SecretKey | string | Elasticsearch | SigV4 secret key. Required when using keys auth provider | #### Custom HTTP headers for datasources diff --git a/package.json b/package.json index 0b486f88d25..b70519ef834 100644 --- a/package.json +++ b/package.json @@ -185,9 +185,9 @@ "style-loader": "1.1.3", "terser-webpack-plugin": "2.3.5", "ts-jest": "26.4.4", - "ts-node": "8.8.1", - "tslib": "2.0.1", - "typescript": "4.0.2", + "ts-node": "9.0.0", + "tslib": "2.0.3", + "typescript": "4.1.2", "webpack": "4.41.5", "webpack-bundle-analyzer": "3.6.0", "webpack-cleanup-plugin": "0.5.1", diff --git a/packages/README.md b/packages/README.md index bbd8dfb6ec6..480716c4149 100644 --- a/packages/README.md +++ b/packages/README.md @@ -27,7 +27,29 @@ Every commit to master that has changes within the `packages` directory is a sub - ``` -Automatic prereleases are published under the `canary` dist tag. +Automatic prereleases are published under the `canary` dist tag to the [github package registry](https://docs.github.com/en/free-pro-team@latest/packages/publishing-and-managing-packages/about-github-packages). + +#### Consuming prereleases + +As mentioned above the `canary` releases are published to the Github package registry rather than the NPM registry. If you wish to make use of these prereleases please follow these steps: + +1. You must use a personal access token to install packages from Github. To create an access token [click here](https://github.com/settings/tokens) and create a token with the `read:packages` scope. Make a copy of the token. +2. Create / modify your `~/.npmrc` file with the following: + +``` +@grafana:registry=https://npm.pkg.github.com +//npm.pkg.github.com/:_authToken={INSERT_GH_TOKEN_HERE} +``` + +3. Update the package.json of your project to use either the `canary` channel or a version of the `canary` channel + +```json +// plugin's package.json +{ + ... + "@grafana/data": "canary" +} +``` ### Manual release diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 9077511ba08..0583d7281e6 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -53,6 +53,6 @@ "rollup-plugin-typescript2": "0.26.0", "rollup-plugin-visualizer": "3.3.1", "sinon": "8.1.1", - "typescript": "4.0.2" + "typescript": "4.1.2" } } diff --git a/packages/grafana-data/src/field/fieldDisplay.ts b/packages/grafana-data/src/field/fieldDisplay.ts index 5d24df79091..ec207cfd289 100644 --- a/packages/grafana-data/src/field/fieldDisplay.ts +++ b/packages/grafana-data/src/field/fieldDisplay.ts @@ -160,7 +160,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi }); values.push({ - name, + name: '', field: config, display: displayValue, view, diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index fe57a9a3799..5f2c017127a 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -36,14 +36,14 @@ "rollup-plugin-typescript2": "0.26.0", "rollup-plugin-visualizer": "3.3.1", "ts-loader": "6.2.1", - "ts-node": "8.8.1" + "ts-node": "9.0.0" }, "types": "src/index.ts", "dependencies": { "@grafana/tsconfig": "^1.0.0-rc1", "commander": "5.0.0", "execa": "4.0.0", - "typescript": "4.0.2", + "typescript": "4.1.2", "yaml": "^1.8.3" } } diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index 5fe5fc38a2c..60468634930 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -54,7 +54,7 @@ "execa": "4.0.0", "resolve-as-bin": "2.1.0", "ts-loader": "6.2.1", - "typescript": "4.0.2", + "typescript": "4.1.2", "yaml": "^1.8.3" } } diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 4082933318f..6c276ba0f37 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -41,7 +41,7 @@ "rollup-plugin-terser": "5.3.0", "rollup-plugin-typescript2": "0.26.0", "rollup-plugin-visualizer": "3.3.1", - "typescript": "4.0.2" + "typescript": "4.1.2" }, "types": "src/index.ts" } diff --git a/packages/grafana-toolkit/package.json b/packages/grafana-toolkit/package.json index d6aeff9d55e..e8557dd9136 100644 --- a/packages/grafana-toolkit/package.json +++ b/packages/grafana-toolkit/package.json @@ -103,9 +103,9 @@ "terser-webpack-plugin": "2.3.5", "ts-jest": "26.4.4", "ts-loader": "6.2.1", - "ts-node": "8.8.1", - "tslib": "2.0.1", - "typescript": "4.0.2", + "ts-node": "9.0.0", + "tslib": "2.0.3", + "typescript": "4.1.2", "url-loader": "^2.0.1", "webpack": "4.41.5" }, diff --git a/packages/grafana-toolkit/src/cli/tasks/toolkit.build.ts b/packages/grafana-toolkit/src/cli/tasks/toolkit.build.ts index aac41cf1392..4bfd09e6fe4 100644 --- a/packages/grafana-toolkit/src/cli/tasks/toolkit.build.ts +++ b/packages/grafana-toolkit/src/cli/tasks/toolkit.build.ts @@ -22,7 +22,7 @@ const compile = () => const savePackage = ({ path, pkg }: { path: string; pkg: {} }) => useSpinner('Updating package.json', async () => { - new Promise((resolve, reject) => { + new Promise((resolve, reject) => { fs.writeFile(path, JSON.stringify(pkg, null, 2), err => { if (err) { reject(err); @@ -61,7 +61,7 @@ const copyFiles = () => { return useSpinner(`Moving ${files.join(', ')} files`, async () => { const promises = files.map(file => { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const basedir = path.dirname(`${distDir}/${file}`); if (!fs.existsSync(basedir)) { fs.mkdirSync(basedir, { recursive: true }); @@ -85,7 +85,7 @@ const copySassFiles = () => { return useSpinner(`Copy scss files ${files.join(', ')} files`, async () => { const sassDir = path.resolve(cwd, '../../public/sass/'); const promises = files.map(file => { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const name = file.replace('.generated', ''); fs.copyFile(`${sassDir}/${file}`, `${distDir}/sass/${name}`, err => { if (err) { diff --git a/packages/grafana-ui/.storybook/main.ts b/packages/grafana-ui/.storybook/main.ts index e32a86bfa26..6a46779233e 100644 --- a/packages/grafana-ui/.storybook/main.ts +++ b/packages/grafana-ui/.storybook/main.ts @@ -26,7 +26,9 @@ module.exports = { check: true, reactDocgen: 'react-docgen-typescript', reactDocgenTypescriptOptions: { + tsconfigPath: path.resolve(__dirname, 'tsconfig.json'), shouldExtractLiteralValuesFromEnum: true, + shouldRemoveUndefinedFromOptional: true, propFilter: (prop: any) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true), }, }, @@ -44,21 +46,6 @@ module.exports = { configFile: path.resolve(__dirname, 'tsconfig.json'), }, }, - { - loader: require.resolve('react-docgen-typescript-loader'), - options: { - tsconfigPath: path.resolve(__dirname, 'tsconfig.json'), - // https://github.com/styleguidist/react-docgen-typescript#parseroptions - // @ts-ignore - propFilter: prop => { - if (prop.parent) { - return !prop.parent.fileName.includes('node_modules/@types/react/'); - } - - return true; - }, - }, - }, ], }, { diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 5f9febd2444..7f42046639f 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -77,12 +77,12 @@ "@rollup/plugin-commonjs": "16.0.0", "@rollup/plugin-image": "2.0.5", "@rollup/plugin-node-resolve": "10.0.0", - "@storybook/addon-essentials": "6.1.2", - "@storybook/addon-controls": "6.1.2", - "@storybook/addon-knobs": "6.1.2", - "@storybook/addon-storysource": "6.1.2", - "@storybook/react": "6.1.2", - "@storybook/theming": "6.1.2", + "@storybook/addon-essentials": "6.1.9", + "@storybook/addon-controls": "6.1.9", + "@storybook/addon-knobs": "6.1.9", + "@storybook/addon-storysource": "6.1.9", + "@storybook/react": "6.1.9", + "@storybook/theming": "6.1.9", "@types/classnames": "2.2.7", "@types/common-tags": "^1.8.0", "@types/d3": "5.7.2", @@ -111,7 +111,7 @@ "rollup-plugin-visualizer": "4.2.0", "storybook-dark-mode": "1.0.3", "ts-loader": "8.0.11", - "typescript": "4.0.2", + "typescript": "4.1.2", "webpack-filter-warnings-plugin": "1.2.1" }, "types": "src/index.ts" diff --git a/packages/grafana-ui/src/components/Alert/Alert.tsx b/packages/grafana-ui/src/components/Alert/Alert.tsx index b009b01d5fc..451197091d9 100644 --- a/packages/grafana-ui/src/components/Alert/Alert.tsx +++ b/packages/grafana-ui/src/components/Alert/Alert.tsx @@ -9,7 +9,7 @@ import { getColorsFromSeverity } from '../../utils/colors'; export type AlertVariant = 'success' | 'warning' | 'error' | 'info'; -export interface Props extends HTMLAttributes { +export interface Props extends HTMLAttributes { title: string; /** On click handler for alert button, mostly used for dismissing the alert */ onRemove?: (event: React.MouseEvent) => void; @@ -39,13 +39,13 @@ function getIconFromSeverity(severity: AlertVariant): string { } } -export const Alert: FC = React.forwardRef( - ({ title, buttonText, onButtonClick, onRemove, children, buttonContent, severity = 'error', ...restProps }) => { +export const Alert: FC = React.forwardRef( + ({ title, buttonText, onButtonClick, onRemove, children, buttonContent, severity = 'error', ...restProps }, ref) => { const theme = useTheme(); const styles = getStyles(theme, severity, !!buttonContent); return ( -
+
diff --git a/packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap b/packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap index 049d927b50e..0d5632f1989 100644 --- a/packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap +++ b/packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap @@ -4,11 +4,11 @@ exports[`FormField should render component with custom inputEl 1`] = `
- Test - + Input @@ -22,11 +22,11 @@ exports[`FormField should render component with default inputEl 1`] = `
- Test - + { const validateAsync = (shouldPass: boolean) => async () => { try { - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { setTimeout(() => { if (shouldPass) { resolve(); diff --git a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx index 25a9a65b752..fa57563d812 100755 --- a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx +++ b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx @@ -114,14 +114,15 @@ export const GraphNG: React.FC = ({ } const fmt = field.display ?? defaultFormatter; - const scale = config.unit || '__fixed'; - const isNewScale = !builder.hasScale(scale); + const scaleKey = config.unit || '__fixed'; - if (isNewScale && customConfig.axisPlacement !== AxisPlacement.Hidden) { - builder.addScale({ scaleKey: scale, min: field.config.min, max: field.config.max }); + if (customConfig.axisPlacement !== AxisPlacement.Hidden) { + // The builder will manage unique scaleKeys and combine where appropriate + builder.addScale({ scaleKey, min: field.config.min, max: field.config.max }); builder.addAxis({ - scaleKey: scale, + scaleKey, label: customConfig.axisLabel, + size: customConfig.axisWidth, placement: customConfig.axisPlacement ?? AxisPlacement.Auto, formatValue: v => formattedValueToString(fmt(v)), theme, @@ -136,7 +137,7 @@ export const GraphNG: React.FC = ({ const pointsMode = customConfig.mode === GraphMode.Points ? PointMode.Always : customConfig.points; builder.addSeries({ - scaleKey: scale, + scaleKey, mode: customConfig.mode!, lineColor: seriesColor, lineWidth: customConfig.lineWidth, @@ -149,7 +150,7 @@ export const GraphNG: React.FC = ({ }); if (hasLegend.current) { - const axisPlacement = builder.getAxisPlacement(scale); + const axisPlacement = builder.getAxisPlacement(scaleKey); legendItems.push({ color: seriesColor, diff --git a/packages/grafana-ui/src/components/Select/utils.ts b/packages/grafana-ui/src/components/Select/utils.ts index 39bb33baf71..daa255722ec 100644 --- a/packages/grafana-ui/src/components/Select/utils.ts +++ b/packages/grafana-ui/src/components/Select/utils.ts @@ -1,10 +1,13 @@ import { SelectableValue } from '@grafana/data'; -import { SelectOptions } from './types'; +import { SelectableOptGroup } from './types'; /** * Normalize the value format to SelectableValue[] | []. Only used for single select */ -export const cleanValue = (value: any, options: SelectOptions): SelectableValue[] | [] => { +export const cleanValue = ( + value: any, + options: Array +): SelectableValue[] | [] => { if (Array.isArray(value)) { return value.filter(Boolean); } @@ -23,7 +26,10 @@ export const cleanValue = (value: any, options: SelectOptions): SelectableValue[ /** * Find the label for a string|number value inside array of options or optgroups */ -export const findSelectedValue = (value: string | number, options: SelectOptions): SelectableValue | null => { +export const findSelectedValue = ( + value: string | number, + options: Array +): SelectableValue | null => { for (const option of options) { if ('options' in option) { let found = findSelectedValue(value, option.options); diff --git a/packages/grafana-ui/src/components/ThemeColors/ThemeColors.tsx b/packages/grafana-ui/src/components/ThemeColors/ThemeColors.tsx index 19ccfe239a6..6f8f332c7f9 100644 --- a/packages/grafana-ui/src/components/ThemeColors/ThemeColors.tsx +++ b/packages/grafana-ui/src/components/ThemeColors/ThemeColors.tsx @@ -13,7 +13,7 @@ interface DemoBoxProps { const DemoBox: FC = ({ bg, border, children }) => { const style = cx( css` - padding: 16px 32px; + padding: 32px 32px 16px 32px; background: ${bg}; width: 100%; `, @@ -24,18 +24,7 @@ const DemoBox: FC = ({ bg, border, children }) => { : null ); - return ( -
-
- {name} -
- {children} -
- ); + return
{children}
; }; const DemoText: FC<{ color?: string; bold?: boolean; size?: number }> = ({ color, bold, size, children }) => { diff --git a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/__snapshots__/TimePickerContent.test.tsx.snap b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/__snapshots__/TimePickerContent.test.tsx.snap index db1ee65bf98..7201deedbfd 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/__snapshots__/TimePickerContent.test.tsx.snap +++ b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/__snapshots__/TimePickerContent.test.tsx.snap @@ -57,7 +57,7 @@ exports[`TimePickerContent renders correctly in full screen 1`] = ` } visible={false} /> - -
- @@ -158,7 +158,7 @@ exports[`TimePickerContent renders correctly in narrow screen 1`] = ` } visible={true} /> - - - -
- diff --git a/packages/grafana-ui/src/components/uPlot/Plot.tsx b/packages/grafana-ui/src/components/uPlot/Plot.tsx index 8b202e18bd6..e030ef4bc0b 100755 --- a/packages/grafana-ui/src/components/uPlot/Plot.tsx +++ b/packages/grafana-ui/src/components/uPlot/Plot.tsx @@ -23,10 +23,6 @@ export const UPlotChart: React.FC = props => { props.config ); const getPlotInstance = useCallback(() => { - if (!plotInstance.current) { - throw new Error("Plot hasn't initialised yet"); - } - return plotInstance.current; }, []); @@ -72,13 +68,15 @@ export const UPlotChart: React.FC = props => { // Memoize plot context const plotCtx = useMemo(() => { - return buildPlotContext(Boolean(plotInstance.current), canvasRef, props.data, registerPlugin, getPlotInstance); + return buildPlotContext(canvasRef, props.data, registerPlugin, getPlotInstance); }, [plotInstance, canvasRef, props.data, registerPlugin, getPlotInstance]); return ( -
- {props.children} +
+
+ {props.children} +
); }; diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts index 8792d474d0d..4c33ae7546c 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts @@ -3,13 +3,14 @@ import uPlot, { Axis } from 'uplot'; import { PlotConfigBuilder } from '../types'; import { measureText } from '../../../utils/measureText'; import { AxisPlacement } from '../config'; +import { optMinMax } from './UPlotScaleBuilder'; export interface AxisProps { scaleKey: string; theme: GrafanaTheme; label?: string; show?: boolean; - size?: number; + size?: number | null; placement?: AxisPlacement; grid?: boolean; formatValue?: (v: any) => string; @@ -19,6 +20,16 @@ export interface AxisProps { } export class UPlotAxisBuilder extends PlotConfigBuilder { + merge(props: AxisProps) { + this.props.size = optMinMax('max', this.props.size, props.size); + if (!this.props.label) { + this.props.label = props.label; + } + if (this.props.placement === AxisPlacement.Auto) { + this.props.placement = props.placement; + } + } + getConfig(): Axis { const { scaleKey, @@ -42,7 +53,7 @@ export class UPlotAxisBuilder extends PlotConfigBuilder { side: getUPlotSideFromAxis(placement), font: `12px 'Roboto'`, labelFont: `12px 'Roboto'`, - size: calculateAxisSize, + size: this.props.size ?? calculateAxisSize, grid: { show: grid, stroke: gridColor, @@ -107,8 +118,7 @@ function calculateAxisSize(self: uPlot, values: string[], axisIdx: number) { } } - let axisWidth = measureText(maxLength, 12).width + 18; - return axisWidth; + return measureText(maxLength, 12).width + 18; } /** Format time axis ticks */ diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts index 18a3ae243e9..3973719f175 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts @@ -61,7 +61,6 @@ describe('UPlotConfigBuilder', () => { formatValue: () => 'test value', grid: false, show: true, - size: 1, theme: { isDark: true, palette: { gray25: '#ffffff' }, colors: { text: 'gray' } } as GrafanaTheme, values: [], }); diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts index e12232bfc5f..3896fa97bce 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts @@ -8,13 +8,17 @@ export class UPlotConfigBuilder { private series: UPlotSeriesBuilder[] = []; private axes: Record = {}; private scales: UPlotScaleBuilder[] = []; - private registeredScales: string[] = []; hasLeftAxis = false; addAxis(props: AxisProps) { props.placement = props.placement ?? AxisPlacement.Auto; + if (this.axes[props.scaleKey]) { + this.axes[props.scaleKey].merge(props); + return; + } + // Handle auto placement logic if (props.placement === AxisPlacement.Auto) { props.placement = this.hasLeftAxis ? AxisPlacement.Right : AxisPlacement.Left; @@ -36,15 +40,16 @@ export class UPlotConfigBuilder { this.series.push(new UPlotSeriesBuilder(props)); } + /** Add or update the scale with the scale key */ addScale(props: ScaleProps) { - this.registeredScales.push(props.scaleKey); + const current = this.scales.find(v => v.props.scaleKey === props.scaleKey); + if (current) { + current.merge(props); + return; + } this.scales.push(new UPlotScaleBuilder(props)); } - hasScale(scaleKey: string) { - return this.registeredScales.indexOf(scaleKey) > -1; - } - getConfig() { const config: PlotSeriesConfig = { series: [{}] }; config.axes = Object.values(this.axes).map(a => a.getConfig()); diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.test.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.test.ts new file mode 100644 index 00000000000..82576389c82 --- /dev/null +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.test.ts @@ -0,0 +1,21 @@ +import { optMinMax } from './UPlotScaleBuilder'; + +describe('UPlotScaleBuilder', () => { + it('opt min max', () => { + expect(7).toEqual(optMinMax('min', null, 7)); + expect(7).toEqual(optMinMax('min', undefined, 7)); + expect(7).toEqual(optMinMax('min', 20, 7)); + + expect(7).toEqual(optMinMax('min', 7, null)); + expect(7).toEqual(optMinMax('min', 7, undefined)); + expect(7).toEqual(optMinMax('min', 7, 20)); + + expect(7).toEqual(optMinMax('max', null, 7)); + expect(7).toEqual(optMinMax('max', undefined, 7)); + expect(7).toEqual(optMinMax('max', 5, 7)); + + expect(7).toEqual(optMinMax('max', 7, null)); + expect(7).toEqual(optMinMax('max', 7, undefined)); + expect(7).toEqual(optMinMax('max', 7, 5)); + }); +}); diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.ts index e3f822b422b..16f2f2caef4 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.ts @@ -9,6 +9,11 @@ export interface ScaleProps { } export class UPlotScaleBuilder extends PlotConfigBuilder { + merge(props: ScaleProps) { + this.props.min = optMinMax('min', this.props.min, props.min); + this.props.max = optMinMax('max', this.props.max, props.max); + } + getConfig() { const { isTime, scaleKey } = this.props; if (isTime) { @@ -29,3 +34,18 @@ export class UPlotScaleBuilder extends PlotConfigBuilder { }; } } + +export function optMinMax(minmax: 'min' | 'max', a?: number | null, b?: number | null): undefined | number | null { + const hasA = !(a === undefined || a === null); + const hasB = !(b === undefined || b === null); + if (hasA) { + if (!hasB) { + return a; + } + if (minmax === 'min') { + return a! < b! ? a : b; + } + return a! > b! ? a : b; + } + return b; +} diff --git a/packages/grafana-ui/src/components/uPlot/context.ts b/packages/grafana-ui/src/components/uPlot/context.ts index cc6d4806b4f..558e88c2104 100644 --- a/packages/grafana-ui/src/components/uPlot/context.ts +++ b/packages/grafana-ui/src/components/uPlot/context.ts @@ -21,8 +21,7 @@ interface PlotPluginsContextType { } interface PlotContextType extends PlotPluginsContextType { - isPlotReady: boolean; - getPlotInstance: () => uPlot; + getPlotInstance: () => uPlot | undefined; getSeries: () => Series[]; getCanvas: () => PlotCanvasContextType; canvasRef: any; @@ -127,28 +126,31 @@ export const usePlotData = (): PlotDataAPI => { }; export const buildPlotContext = ( - isPlotReady: boolean, canvasRef: any, data: AlignedFrameWithGapTest, registerPlugin: any, - getPlotInstance: () => uPlot + getPlotInstance: () => uPlot | undefined ): PlotContextType => { return { - isPlotReady, canvasRef, data, registerPlugin, getPlotInstance, - getSeries: () => getPlotInstance().series, - getCanvas: () => ({ - width: getPlotInstance().width, - height: getPlotInstance().height, - plot: { - width: getPlotInstance().bbox.width / window.devicePixelRatio, - height: getPlotInstance().bbox.height / window.devicePixelRatio, - top: getPlotInstance().bbox.top / window.devicePixelRatio, - left: getPlotInstance().bbox.left / window.devicePixelRatio, - }, - }), + getSeries: () => getPlotInstance()!.series, + getCanvas: () => { + const plotInstance = getPlotInstance()!; + const bbox = plotInstance.bbox; + const pxRatio = window.devicePixelRatio; + return { + width: plotInstance.width, + height: plotInstance.height, + plot: { + width: bbox.width / pxRatio, + height: bbox.height / pxRatio, + top: bbox.top / pxRatio, + left: bbox.left / pxRatio, + }, + }; + }, }; }; diff --git a/packages/grafana-ui/src/components/uPlot/geometries/EventsCanvas.tsx b/packages/grafana-ui/src/components/uPlot/geometries/EventsCanvas.tsx index 0c5c8ac6262..cc247362a5a 100644 --- a/packages/grafana-ui/src/components/uPlot/geometries/EventsCanvas.tsx +++ b/packages/grafana-ui/src/components/uPlot/geometries/EventsCanvas.tsx @@ -18,8 +18,7 @@ export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoo const eventMarkers = useMemo(() => { const markers: React.ReactNode[] = []; - - if (!plotCtx.isPlotReady || events.length === 0) { + if (!plotCtx.getPlotInstance() || events.length === 0) { return markers; } @@ -41,9 +40,9 @@ export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoo } return <>{markers}; - }, [events, renderEventMarker, renderToken, plotCtx.isPlotReady]); + }, [events, renderEventMarker, renderToken, plotCtx]); - if (!plotCtx.isPlotReady) { + if (!plotCtx.getPlotInstance()) { return null; } diff --git a/packages/grafana-ui/src/components/uPlot/geometries/XYCanvas.tsx b/packages/grafana-ui/src/components/uPlot/geometries/XYCanvas.tsx index 0ded2517c74..352d34cb0df 100644 --- a/packages/grafana-ui/src/components/uPlot/geometries/XYCanvas.tsx +++ b/packages/grafana-ui/src/components/uPlot/geometries/XYCanvas.tsx @@ -10,8 +10,9 @@ interface XYCanvasProps {} */ export const XYCanvas: React.FC = ({ children }) => { const plotContext = usePlotContext(); + const plotInstance = plotContext.getPlotInstance(); - if (!plotContext.isPlotReady) { + if (!plotInstance) { return null; } @@ -20,8 +21,8 @@ export const XYCanvas: React.FC = ({ children }) => { className={css` position: absolute; overflow: visible; - left: ${plotContext.getPlotInstance().bbox.left / window.devicePixelRatio}px; - top: ${plotContext.getPlotInstance().bbox.top / window.devicePixelRatio}px; + left: ${plotInstance.bbox.left / window.devicePixelRatio}px; + top: ${plotInstance.bbox.top / window.devicePixelRatio}px; `} > {children} diff --git a/packages/grafana-ui/src/components/uPlot/plugins/SelectionPlugin.tsx b/packages/grafana-ui/src/components/uPlot/plugins/SelectionPlugin.tsx index af351aa18a2..eff8b96a341 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/SelectionPlugin.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/SelectionPlugin.tsx @@ -75,7 +75,7 @@ export const SelectionPlugin: React.FC = ({ onSelect, onDi }; }, []); - if (!plotCtx.isPlotReady || !children || !selection) { + if (!plotCtx.getPlotInstance() || !children || !selection) { return null; } diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx index be3cbb9f0d2..feed5e7fd4d 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx @@ -25,7 +25,7 @@ export const TooltipPlugin: React.FC = ({ mode = 'single', t return ( {({ focusedSeriesIdx, focusedPointIdx, coords }) => { - if (!plotContext.isPlotReady) { + if (!plotContext.getPlotInstance()) { return null; } diff --git a/packages/grafana-ui/src/types/icon.ts b/packages/grafana-ui/src/types/icon.ts index 56e205f44a6..801a8b13705 100644 --- a/packages/grafana-ui/src/types/icon.ts +++ b/packages/grafana-ui/src/types/icon.ts @@ -80,6 +80,7 @@ export type IconName = | 'compass' | 'sliders-v-alt' | 'bell' + | 'bell-slash' | 'database' | 'user' | 'camera' @@ -198,6 +199,7 @@ export const getAvailableIcons = (): IconName[] => [ 'compass', 'sliders-v-alt', 'bell', + 'bell-slash', 'database', 'user', 'camera', diff --git a/packages/jaeger-ui-components/package.json b/packages/jaeger-ui-components/package.json index 473837ee699..e2da7dc5f28 100644 --- a/packages/jaeger-ui-components/package.json +++ b/packages/jaeger-ui-components/package.json @@ -11,7 +11,7 @@ "devDependencies": { "enzyme": "^3.8.0", "enzyme-adapter-react-16": "^1.2.0", - "typescript": "4.0.2" + "typescript": "4.1.2" }, "dependencies": { "@grafana/data": "7.4.0-pre.0", diff --git a/pkg/api/avatar/avatar.go b/pkg/api/avatar/avatar.go index 9eb99db8b3b..95b6cea3efc 100644 --- a/pkg/api/avatar/avatar.go +++ b/pkg/api/avatar/avatar.go @@ -132,6 +132,9 @@ func newNotFound() *Avatar { avatar := &Avatar{notFound: true} // load user_profile png into buffer + // It's safe to ignore gosec warning G304 since the variable part of the file path comes from a configuration + // variable. + // nolint:gosec path := filepath.Join(setting.StaticRootPath, "img", "user_profile.png") if data, err := ioutil.ReadFile(path); err != nil { diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 8a9028aaa63..51d38bb0a77 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/dashdiffs" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/guardian" @@ -334,7 +333,7 @@ func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) Response { dashRedirect := dtos.DashboardRedirect{RedirectUri: url} return JSON(200, &dashRedirect) } - log.Warnf("Failed to get slug from database, %s", err.Error()) + hs.log.Warn("Failed to get slug from database", "err", err) } filePath := hs.Cfg.DefaultHomeDashboardPath @@ -342,11 +341,18 @@ func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) Response { filePath = filepath.Join(hs.Cfg.StaticRootPath, "dashboards/home.json") } + // It's safe to ignore gosec warning G304 since the variable part of the file path comes from a configuration + // variable + // nolint:gosec file, err := os.Open(filePath) if err != nil { return Error(500, "Failed to load home dashboard", err) } - defer file.Close() + defer func() { + if err := file.Close(); err != nil { + hs.log.Warn("Failed to close dashboard file", "path", filePath, "err", err) + } + }() dash := dtos.DashboardFullWithMeta{} dash.Meta.IsHome = true diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index 558ca9b3da4..311b686165e 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -291,6 +291,10 @@ func extractFile(file *zip.File, filePath string) (err error) { fileMode = os.FileMode(0755) } + // We can ignore the gosec G304 warning on this one, since the variable part of the file path stems + // from command line flag "pluginsDir", and the only possible damage would be writing to the wrong directory. + // If the user shouldn't be writing to this directory, they shouldn't have the permission in the file system. + // nolint:gosec dst, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode) if err != nil { if os.IsPermission(err) { diff --git a/pkg/cmd/grafana-cli/services/api_client.go b/pkg/cmd/grafana-cli/services/api_client.go index 2aa40a2a2de..1ac8cbf6458 100644 --- a/pkg/cmd/grafana-cli/services/api_client.go +++ b/pkg/cmd/grafana-cli/services/api_client.go @@ -44,8 +44,11 @@ func (client *GrafanaComClient) GetPlugin(pluginId, repoUrl string) (models.Plug } func (client *GrafanaComClient) DownloadFile(pluginName string, tmpFile *os.File, url string, checksum string) (err error) { - // Try handling url like local file path first + // Try handling URL as a local file path first if _, err := os.Stat(url); err == nil { + // We can ignore this gosec G304 warning since `url` stems from command line flag "pluginUrl". If the + // user shouldn't be able to read the file, it should be handled through filesystem permissions. + // nolint:gosec f, err := os.Open(url) if err != nil { return errutil.Wrap("Failed to read plugin archive", err) diff --git a/pkg/cmd/grafana-cli/services/io_util.go b/pkg/cmd/grafana-cli/services/io_util.go index b1f1c140e2c..38aa81d94f7 100644 --- a/pkg/cmd/grafana-cli/services/io_util.go +++ b/pkg/cmd/grafana-cli/services/io_util.go @@ -21,5 +21,9 @@ func (i IoUtilImp) ReadDir(path string) ([]os.FileInfo, error) { } func (i IoUtilImp) ReadFile(filename string) ([]byte, error) { + // We can ignore the gosec G304 warning on this one, since the variable part of the file path stems + // from command line flag "pluginsDir". If the user shouldn't be reading from this directory, they shouldn't have + // the permission in the file system. + // nolint:gosec return ioutil.ReadFile(filename) } diff --git a/pkg/cmd/grafana-cli/utils/grafana_path.go b/pkg/cmd/grafana-cli/utils/grafana_path.go index aba5e1b8d48..51f953d26cd 100644 --- a/pkg/cmd/grafana-cli/utils/grafana_path.go +++ b/pkg/cmd/grafana-cli/utils/grafana_path.go @@ -16,8 +16,8 @@ func GetGrafanaPluginDir(currentOS string) string { return returnOsDefault(currentOS) } -// getGrafanaRoot tries to get root of directory when developing grafana ie repo root. It is not perfect it just -// checks what is the binary path and tries to guess based on that but if it is not running in dev env you get a bogus +// getGrafanaRoot tries to get root of directory when developing grafana, ie. repo root. It is not perfect, it just +// checks what is the binary path and tries to guess based on that, but if it is not running in dev env you get a bogus // path back. func getGrafanaRoot() (string, error) { ex, err := os.Executable() diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index b68a76238e8..0ab0a6a7186 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -116,7 +116,11 @@ func executeServer(configFile, homePath, pidFile, packaging string, traceDiagnos if err != nil { panic(err) } - defer f.Close() + defer func() { + if err := f.Close(); err != nil { + log.Error("Failed to write trace diagnostics", "path", traceDiagnostics.file, "err", err) + } + }() if err := trace.Start(f); err != nil { panic(err) diff --git a/pkg/components/imguploader/azureblobuploader.go b/pkg/components/imguploader/azureblobuploader.go index 50f5165b4af..80023b8c30c 100644 --- a/pkg/components/imguploader/azureblobuploader.go +++ b/pkg/components/imguploader/azureblobuploader.go @@ -45,11 +45,18 @@ func (az *AzureBlobUploader) Upload(ctx context.Context, imageDiskPath string) ( // setup client blob := NewStorageClient(az.account_name, az.account_key) + // We can ignore the gosec G304 warning on this one because `imageDiskPath` comes + // from alert notifiers and is only used to upload images generated by alerting. + // nolint:gosec file, err := os.Open(imageDiskPath) if err != nil { return "", err } - defer file.Close() + defer func() { + if err := file.Close(); err != nil { + az.log.Warn("Failed to close file", "path", imageDiskPath, "err", err) + } + }() randomFileName, err := util.GetRandomString(30) if err != nil { diff --git a/pkg/components/imguploader/gcs/gcsuploader.go b/pkg/components/imguploader/gcs/gcsuploader.go index a2fee45b580..391e5b78ece 100644 --- a/pkg/components/imguploader/gcs/gcsuploader.go +++ b/pkg/components/imguploader/gcs/gcsuploader.go @@ -149,11 +149,19 @@ func (u *Uploader) uploadFile( key string, ) error { u.log.Debug("Opening image file", "path", imageDiskPath) + + // We can ignore the gosec G304 warning on this one because `imageDiskPath` comes + // from alert notifiers and is only used to upload images generated by alerting. + // nolint:gosec fileReader, err := os.Open(imageDiskPath) if err != nil { return err } - defer fileReader.Close() + defer func() { + if err := fileReader.Close(); err != nil { + u.log.Warn("Failed to close file", "err", err, "path", imageDiskPath) + } + }() // Set public access if not generating a signed URL pubAcc := !u.enableSignedURLs diff --git a/pkg/components/imguploader/s3uploader.go b/pkg/components/imguploader/s3uploader.go index d49e9018f91..d0b8b251740 100644 --- a/pkg/components/imguploader/s3uploader.go +++ b/pkg/components/imguploader/s3uploader.go @@ -76,16 +76,24 @@ func (u *S3Uploader) Upload(ctx context.Context, imageDiskPath string) (string, key := u.path + rand + pngExt log.Debugf("Uploading image to s3. bucket = %s, path = %s", u.bucket, key) + // We can ignore the gosec G304 warning on this one because `imageDiskPath` comes + // from alert notifiers and is only used to upload images generated by alerting. + // nolint:gosec file, err := os.Open(imageDiskPath) if err != nil { return "", err } - defer file.Close() + defer func() { + if err := file.Close(); err != nil { + u.log.Warn("Failed to close file", "path", imageDiskPath, "err", err) + } + }() sess, err = session.NewSession(cfg) if err != nil { return "", err } + uploader := s3manager.NewUploader(sess) result, err := uploader.UploadWithContext(ctx, &s3manager.UploadInput{ Bucket: aws.String(u.bucket), diff --git a/pkg/components/imguploader/webdavuploader.go b/pkg/components/imguploader/webdavuploader.go index f0a157a8291..3fd9894a8cf 100644 --- a/pkg/components/imguploader/webdavuploader.go +++ b/pkg/components/imguploader/webdavuploader.go @@ -45,7 +45,7 @@ func (u *WebdavUploader) PublicURL(filename string) string { return publicURL.String() } -func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error) { +func (u *WebdavUploader) Upload(ctx context.Context, imgToUpload string) (string, error) { url, _ := url.Parse(u.url) filename, err := util.GetRandomString(20) if err != nil { @@ -55,7 +55,10 @@ func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error) filename += pngExt url.Path = path.Join(url.Path, filename) - imgData, err := ioutil.ReadFile(pa) + // We can ignore the gosec G304 warning on this one because `imgToUpload` comes + // from alert notifiers and is only used to upload images generated by alerting. + // nolint:gosec + imgData, err := ioutil.ReadFile(imgToUpload) if err != nil { return "", err } diff --git a/pkg/infra/log/log.go b/pkg/infra/log/log.go index 737d48941c3..ccec1058e9f 100644 --- a/pkg/infra/log/log.go +++ b/pkg/infra/log/log.go @@ -80,6 +80,10 @@ func Warnf(format string, v ...interface{}) { Root.Warn(message) } +func Error(msg string, args ...interface{}) { + Root.Error(msg, args...) +} + func Errorf(skip int, format string, v ...interface{}) { Root.Error(fmt.Sprintf(format, v...)) } diff --git a/pkg/middleware/auth_test.go b/pkg/middleware/auth_test.go index 6f6b243d8a9..1cf03aa4df2 100644 --- a/pkg/middleware/auth_test.go +++ b/pkg/middleware/auth_test.go @@ -7,103 +7,105 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - . "github.com/smartystreets/goconvey/convey" ) func TestMiddlewareAuth(t *testing.T) { - Convey("Given the grafana middleware", t, func() { - reqSignIn := Auth(&AuthOptions{ReqSignedIn: true}) + reqSignIn := Auth(&AuthOptions{ReqSignedIn: true}) - middlewareScenario(t, "ReqSignIn true and unauthenticated request", func(sc *scenarioContext) { - sc.m.Get("/secure", reqSignIn, sc.defaultHandler) + middlewareScenario(t, "ReqSignIn true and unauthenticated request", func(t *testing.T, sc *scenarioContext) { + sc.m.Get("/secure", reqSignIn, sc.defaultHandler) - sc.fakeReq("GET", "/secure").exec() + sc.fakeReq("GET", "/secure").exec() - Convey("Should redirect to login", func() { - So(sc.resp.Code, ShouldEqual, 302) - }) + assert.Equal(t, 302, sc.resp.Code) + }) + + middlewareScenario(t, "ReqSignIn true and unauthenticated API request", func(t *testing.T, sc *scenarioContext) { + sc.m.Get("/api/secure", reqSignIn, sc.defaultHandler) + + sc.fakeReq("GET", "/api/secure").exec() + + assert.Equal(t, 401, sc.resp.Code) + }) + + t.Run("Anonymous auth enabled", func(t *testing.T) { + const orgID int64 = 1 + + origEnabled := setting.AnonymousEnabled + t.Cleanup(func() { + setting.AnonymousEnabled = origEnabled }) - - middlewareScenario(t, "ReqSignIn true and unauthenticated API request", func(sc *scenarioContext) { - sc.m.Get("/api/secure", reqSignIn, sc.defaultHandler) - - sc.fakeReq("GET", "/api/secure").exec() - - Convey("Should return 401", func() { - So(sc.resp.Code, ShouldEqual, 401) - }) + origName := setting.AnonymousOrgName + t.Cleanup(func() { + setting.AnonymousOrgName = origName }) + setting.AnonymousEnabled = true + setting.AnonymousOrgName = "test" - Convey("Anonymous auth enabled", func() { - origEnabled := setting.AnonymousEnabled - t.Cleanup(func() { - setting.AnonymousEnabled = origEnabled - }) - origName := setting.AnonymousOrgName - t.Cleanup(func() { - setting.AnonymousOrgName = origName - }) - setting.AnonymousEnabled = true - setting.AnonymousOrgName = "test" - + middlewareScenario(t, "ReqSignIn true and request with forceLogin in query string", func( + t *testing.T, sc *scenarioContext) { bus.AddHandler("test", func(query *models.GetOrgByNameQuery) error { - query.Result = &models.Org{Id: 1, Name: "test"} + query.Result = &models.Org{Id: orgID, Name: "test"} return nil }) - middlewareScenario(t, "ReqSignIn true and request with forceLogin in query string", func(sc *scenarioContext) { - sc.m.Get("/secure", reqSignIn, sc.defaultHandler) + sc.m.Get("/secure", reqSignIn, sc.defaultHandler) - sc.fakeReq("GET", "/secure?forceLogin=true").exec() + sc.fakeReq("GET", "/secure?forceLogin=true").exec() - Convey("Should redirect to login", func() { - So(sc.resp.Code, ShouldEqual, 302) - location, ok := sc.resp.Header()["Location"] - So(ok, ShouldBeTrue) - So(location[0], ShouldEqual, "/login") - }) - }) - - middlewareScenario(t, "ReqSignIn true and request with same org provided in query string", func(sc *scenarioContext) { - sc.m.Get("/secure", reqSignIn, sc.defaultHandler) - - sc.fakeReq("GET", "/secure?orgId=1").exec() - - Convey("Should not redirect to login", func() { - So(sc.resp.Code, ShouldEqual, 200) - }) - }) - - middlewareScenario(t, "ReqSignIn true and request with different org provided in query string", func(sc *scenarioContext) { - sc.m.Get("/secure", reqSignIn, sc.defaultHandler) - - sc.fakeReq("GET", "/secure?orgId=2").exec() - - Convey("Should redirect to login", func() { - So(sc.resp.Code, ShouldEqual, 302) - location, ok := sc.resp.Header()["Location"] - So(ok, ShouldBeTrue) - So(location[0], ShouldEqual, "/login") - }) - }) + assert.Equal(t, 302, sc.resp.Code) + location, ok := sc.resp.Header()["Location"] + assert.True(t, ok) + assert.Equal(t, "/login", location[0]) }) - Convey("snapshot public mode or signed in", func() { - middlewareScenario(t, "Snapshot public mode disabled and unauthenticated request should return 401", func(sc *scenarioContext) { - sc.m.Get("/api/snapshot", SnapshotPublicModeOrSignedIn(), sc.defaultHandler) - sc.fakeReq("GET", "/api/snapshot").exec() - So(sc.resp.Code, ShouldEqual, 401) + middlewareScenario(t, "ReqSignIn true and request with same org provided in query string", func( + t *testing.T, sc *scenarioContext) { + bus.AddHandler("test", func(query *models.GetOrgByNameQuery) error { + query.Result = &models.Org{Id: orgID, Name: "test"} + return nil }) - middlewareScenario(t, "Snapshot public mode enabled and unauthenticated request should return 200", func(sc *scenarioContext) { - setting.SnapshotPublicMode = true - sc.m.Get("/api/snapshot", SnapshotPublicModeOrSignedIn(), sc.defaultHandler) - sc.fakeReq("GET", "/api/snapshot").exec() - So(sc.resp.Code, ShouldEqual, 200) - }) + sc.m.Get("/secure", reqSignIn, sc.defaultHandler) + + sc.fakeReq("GET", fmt.Sprintf("/secure?orgId=%d", orgID)).exec() + + assert.Equal(t, 200, sc.resp.Code) }) + + middlewareScenario(t, "ReqSignIn true and request with different org provided in query string", func( + t *testing.T, sc *scenarioContext) { + bus.AddHandler("test", func(query *models.GetOrgByNameQuery) error { + query.Result = &models.Org{Id: orgID, Name: "test"} + return nil + }) + + sc.m.Get("/secure", reqSignIn, sc.defaultHandler) + + sc.fakeReq("GET", "/secure?orgId=2").exec() + + assert.Equal(t, 302, sc.resp.Code) + location, ok := sc.resp.Header()["Location"] + assert.True(t, ok) + assert.Equal(t, "/login", location[0]) + }) + }) + + middlewareScenario(t, "Snapshot public mode disabled and unauthenticated request should return 401", func( + t *testing.T, sc *scenarioContext) { + sc.m.Get("/api/snapshot", SnapshotPublicModeOrSignedIn(), sc.defaultHandler) + sc.fakeReq("GET", "/api/snapshot").exec() + assert.Equal(t, 401, sc.resp.Code) + }) + + middlewareScenario(t, "Snapshot public mode enabled and unauthenticated request should return 200", func( + t *testing.T, sc *scenarioContext) { + setting.SnapshotPublicMode = true + sc.m.Get("/api/snapshot", SnapshotPublicModeOrSignedIn(), sc.defaultHandler) + sc.fakeReq("GET", "/api/snapshot").exec() + assert.Equal(t, 200, sc.resp.Code) }) } diff --git a/pkg/middleware/dashboard_redirect_test.go b/pkg/middleware/dashboard_redirect_test.go index 5730bf15d14..30a9d27fe4c 100644 --- a/pkg/middleware/dashboard_redirect_test.go +++ b/pkg/middleware/dashboard_redirect_test.go @@ -8,6 +8,8 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/util" . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMiddlewareDashboardRedirect(t *testing.T) { @@ -22,62 +24,57 @@ func TestMiddlewareDashboardRedirect(t *testing.T) { fakeDash.HasAcl = false fakeDash.Uid = util.GenerateShortUID() - bus.AddHandler("test", func(query *models.GetDashboardQuery) error { - query.Result = fakeDash - return nil - }) + middlewareScenario(t, "GET dashboard by legacy url", func(t *testing.T, sc *scenarioContext) { + bus.AddHandler("test", func(query *models.GetDashboardQuery) error { + query.Result = fakeDash + return nil + }) - middlewareScenario(t, "GET dashboard by legacy url", func(sc *scenarioContext) { sc.m.Get("/dashboard/db/:slug", redirectFromLegacyDashboardUrl, sc.defaultHandler) sc.fakeReqWithParams("GET", "/dashboard/db/dash?orgId=1&panelId=2", map[string]string{}).exec() - Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() { - So(sc.resp.Code, ShouldEqual, 301) - resp := sc.resp.Result() - defer resp.Body.Close() - redirectURL, err := resp.Location() - So(err, ShouldBeNil) - So(redirectURL.Path, ShouldEqual, models.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug)) - So(len(redirectURL.Query()), ShouldEqual, 2) - }) + assert.Equal(t, 301, sc.resp.Code) + resp := sc.resp.Result() + resp.Body.Close() + redirectURL, err := resp.Location() + require.NoError(t, err) + assert.Equal(t, models.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug), redirectURL.Path) + assert.Equal(t, 2, len(redirectURL.Query())) }) - middlewareScenario(t, "GET dashboard solo by legacy url", func(sc *scenarioContext) { + middlewareScenario(t, "GET dashboard solo by legacy url", func(t *testing.T, sc *scenarioContext) { + bus.AddHandler("test", func(query *models.GetDashboardQuery) error { + query.Result = fakeDash + return nil + }) + sc.m.Get("/dashboard-solo/db/:slug", redirectFromLegacyDashboardSoloUrl, sc.defaultHandler) sc.fakeReqWithParams("GET", "/dashboard-solo/db/dash?orgId=1&panelId=2", map[string]string{}).exec() - Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() { - So(sc.resp.Code, ShouldEqual, 301) - resp := sc.resp.Result() - defer resp.Body.Close() - redirectURL, err := resp.Location() - So(err, ShouldBeNil) - expectedURL := models.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug) - expectedURL = strings.Replace(expectedURL, "/d/", "/d-solo/", 1) - So(redirectURL.Path, ShouldEqual, expectedURL) - So(len(redirectURL.Query()), ShouldEqual, 2) - }) + assert.Equal(t, 301, sc.resp.Code) + resp := sc.resp.Result() + resp.Body.Close() + redirectURL, err := resp.Location() + require.NoError(t, err) + expectedURL := models.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug) + expectedURL = strings.Replace(expectedURL, "/d/", "/d-solo/", 1) + assert.Equal(t, expectedURL, redirectURL.Path) + assert.Equal(t, 2, len(redirectURL.Query())) }) }) - Convey("Given the dashboard legacy edit panel middleware", t, func() { - bus.ClearBusHandlers() + middlewareScenario(t, "GET dashboard by legacy edit url", func(t *testing.T, sc *scenarioContext) { + sc.m.Get("/d/:uid/:slug", RedirectFromLegacyPanelEditURL(), sc.defaultHandler) - middlewareScenario(t, "GET dashboard by legacy edit url", func(sc *scenarioContext) { - sc.m.Get("/d/:uid/:slug", RedirectFromLegacyPanelEditURL(), sc.defaultHandler) + sc.fakeReqWithParams("GET", "/d/asd/dash?orgId=1&panelId=12&fullscreen&edit", map[string]string{}).exec() - sc.fakeReqWithParams("GET", "/d/asd/dash?orgId=1&panelId=12&fullscreen&edit", map[string]string{}).exec() - - Convey("Should redirect to new dashboard edit url with a 301 Moved Permanently", func() { - So(sc.resp.Code, ShouldEqual, 301) - resp := sc.resp.Result() - defer resp.Body.Close() - redirectURL, err := resp.Location() - So(err, ShouldBeNil) - So(redirectURL.String(), ShouldEqual, "/d/asd/d/asd/dash?editPanel=12&orgId=1") - }) - }) + assert.Equal(t, 301, sc.resp.Code) + resp := sc.resp.Result() + resp.Body.Close() + redirectURL, err := resp.Location() + require.NoError(t, err) + assert.Equal(t, "/d/asd/d/asd/dash?editPanel=12&orgId=1", redirectURL.String()) }) } diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 6d5dd32fc26..00531f22823 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -54,10 +54,13 @@ func GetContextHandler( Logger: log.New("context"), } - orgId := int64(0) - orgIdHeader := ctx.Req.Header.Get("X-Grafana-Org-Id") - if orgIdHeader != "" { - orgId, _ = strconv.ParseInt(orgIdHeader, 10, 64) + orgID := int64(0) + orgIDHeader := ctx.Req.Header.Get("X-Grafana-Org-Id") + if orgIDHeader != "" { + orgIDParsed, err := strconv.ParseInt(orgIDHeader, 10, 64) + if err == nil { + orgID = orgIDParsed + } } // the order in which these are tested are important @@ -68,9 +71,9 @@ func GetContextHandler( switch { case initContextWithRenderAuth(ctx, renderService): case initContextWithApiKey(ctx): - case initContextWithBasicAuth(ctx, orgId): - case initContextWithAuthProxy(remoteCache, ctx, orgId): - case initContextWithToken(ats, ctx, orgId): + case initContextWithBasicAuth(ctx, orgID): + case initContextWithAuthProxy(remoteCache, ctx, orgID): + case initContextWithToken(ats, ctx, orgID): case initContextWithAnonymousUser(ctx): } diff --git a/pkg/middleware/middleware_basic_auth_test.go b/pkg/middleware/middleware_basic_auth_test.go index 1535af0f5ea..447d715dd79 100644 --- a/pkg/middleware/middleware_basic_auth_test.go +++ b/pkg/middleware/middleware_basic_auth_test.go @@ -4,149 +4,136 @@ import ( "encoding/json" "testing" - . "github.com/smartystreets/goconvey/convey" - "github.com/grafana/grafana/pkg/bus" - authLogin "github.com/grafana/grafana/pkg/login" + "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMiddlewareBasicAuth(t *testing.T) { - Convey("Given the basic auth", t, func() { - var oldBasicAuthEnabled = setting.BasicAuthEnabled - var oldDisableBruteForceLoginProtection = setting.DisableBruteForceLoginProtection - var id int64 = 12 + var origBasicAuthEnabled = setting.BasicAuthEnabled + var origDisableBruteForceLoginProtection = setting.DisableBruteForceLoginProtection + t.Cleanup(func() { + setting.BasicAuthEnabled = origBasicAuthEnabled + setting.DisableBruteForceLoginProtection = origDisableBruteForceLoginProtection + }) + setting.BasicAuthEnabled = true + setting.DisableBruteForceLoginProtection = true - Convey("Setup", func() { - setting.BasicAuthEnabled = true - setting.DisableBruteForceLoginProtection = true - bus.ClearBusHandlers() + bus.ClearBusHandlers() + + const id int64 = 12 + + middlewareScenario(t, "Valid API key", func(t *testing.T, sc *scenarioContext) { + const orgID int64 = 2 + keyhash, err := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd") + require.NoError(t, err) + + bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error { + query.Result = &models.ApiKey{OrgId: orgID, Role: models.ROLE_EDITOR, Key: keyhash} + return nil }) - middlewareScenario(t, "Valid API key", func(sc *scenarioContext) { - var orgID int64 = 2 - keyhash, err := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd") - So(err, ShouldBeNil) + authHeader := util.GetBasicAuthHeader("api_key", "eyJrIjoidjVuQXdwTWFmRlA2em5hUzR1cmhkV0RMUzU1MTFNNDIiLCJuIjoiYXNkIiwiaWQiOjF9") + sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec() - bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error { - query.Result = &models.ApiKey{OrgId: orgID, Role: models.ROLE_EDITOR, Key: keyhash} - return nil - }) + assert.Equal(t, 200, sc.resp.Code) + assert.True(t, sc.context.IsSignedIn) + assert.Equal(t, orgID, sc.context.OrgId) + assert.Equal(t, models.ROLE_EDITOR, sc.context.OrgRole) + }) - authHeader := util.GetBasicAuthHeader("api_key", "eyJrIjoidjVuQXdwTWFmRlA2em5hUzR1cmhkV0RMUzU1MTFNNDIiLCJuIjoiYXNkIiwiaWQiOjF9") - sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec() + middlewareScenario(t, "Handle auth", func(t *testing.T, sc *scenarioContext) { + const password = "MyPass" + const salt = "Salt" + const orgID int64 = 2 - Convey("Should return 200", func() { - So(sc.resp.Code, ShouldEqual, 200) - }) + t.Cleanup(bus.ClearBusHandlers) - Convey("Should init middleware context", func() { - So(sc.context.IsSignedIn, ShouldEqual, true) - So(sc.context.OrgId, ShouldEqual, orgID) - So(sc.context.OrgRole, ShouldEqual, models.ROLE_EDITOR) - }) + bus.AddHandler("grafana-auth", func(query *models.LoginUserQuery) error { + encoded, err := util.EncodePassword(password, salt) + if err != nil { + return err + } + query.User = &models.User{ + Password: encoded, + Salt: salt, + } + return nil }) - middlewareScenario(t, "Handle auth", func(sc *scenarioContext) { - var password = "MyPass" - var salt = "Salt" - var orgID int64 = 2 - - bus.AddHandler("grafana-auth", func(query *models.LoginUserQuery) error { - encoded, err := util.EncodePassword(password, salt) - if err != nil { - return err - } - query.User = &models.User{ - Password: encoded, - Salt: salt, - } - return nil - }) - - bus.AddHandler("get-sign-user", func(query *models.GetSignedInUserQuery) error { - query.Result = &models.SignedInUser{OrgId: orgID, UserId: id} - return nil - }) - - authHeader := util.GetBasicAuthHeader("myUser", password) - sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec() - - Convey("Should init middleware context with users", func() { - So(sc.context.IsSignedIn, ShouldEqual, true) - So(sc.context.OrgId, ShouldEqual, orgID) - So(sc.context.UserId, ShouldEqual, id) - }) - - bus.ClearBusHandlers() + bus.AddHandler("get-sign-user", func(query *models.GetSignedInUserQuery) error { + query.Result = &models.SignedInUser{OrgId: orgID, UserId: id} + return nil }) - middlewareScenario(t, "Auth sequence", func(sc *scenarioContext) { - var password = "MyPass" - var salt = "Salt" + authHeader := util.GetBasicAuthHeader("myUser", password) + sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec() - authLogin.Init() + assert.True(t, sc.context.IsSignedIn) + assert.Equal(t, orgID, sc.context.OrgId) + assert.Equal(t, id, sc.context.UserId) + }) - bus.AddHandler("user-query", func(query *models.GetUserByLoginQuery) error { - encoded, err := util.EncodePassword(password, salt) - if err != nil { - return err - } - query.Result = &models.User{ - Password: encoded, - Id: id, - Salt: salt, - } - return nil - }) + middlewareScenario(t, "Auth sequence", func(t *testing.T, sc *scenarioContext) { + const password = "MyPass" + const salt = "Salt" - bus.AddHandler("get-sign-user", func(query *models.GetSignedInUserQuery) error { - query.Result = &models.SignedInUser{UserId: query.UserId} - return nil - }) + login.Init() - authHeader := util.GetBasicAuthHeader("myUser", password) - sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec() - - Convey("Should init middleware context with user", func() { - So(sc.context.IsSignedIn, ShouldEqual, true) - So(sc.context.UserId, ShouldEqual, id) - }) + bus.AddHandler("user-query", func(query *models.GetUserByLoginQuery) error { + encoded, err := util.EncodePassword(password, salt) + if err != nil { + return err + } + query.Result = &models.User{ + Password: encoded, + Id: id, + Salt: salt, + } + return nil }) - middlewareScenario(t, "Should return error if user is not found", func(sc *scenarioContext) { - sc.fakeReq("GET", "/") - sc.req.SetBasicAuth("user", "password") - sc.exec() - - err := json.NewDecoder(sc.resp.Body).Decode(&sc.respJson) - So(err, ShouldNotBeNil) - - So(sc.resp.Code, ShouldEqual, 401) - So(sc.respJson["message"], ShouldEqual, errStringInvalidUsernamePassword) + bus.AddHandler("get-sign-user", func(query *models.GetSignedInUserQuery) error { + query.Result = &models.SignedInUser{UserId: query.UserId} + return nil }) - middlewareScenario(t, "Should return error if user & password do not match", func(sc *scenarioContext) { - bus.AddHandler("user-query", func(loginUserQuery *models.GetUserByLoginQuery) error { - return nil - }) + authHeader := util.GetBasicAuthHeader("myUser", password) + sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec() - sc.fakeReq("GET", "/") - sc.req.SetBasicAuth("killa", "gorilla") - sc.exec() + assert.True(t, sc.context.IsSignedIn) + assert.Equal(t, id, sc.context.UserId) + }) - err := json.NewDecoder(sc.resp.Body).Decode(&sc.respJson) - So(err, ShouldNotBeNil) + middlewareScenario(t, "Should return error if user is not found", func(t *testing.T, sc *scenarioContext) { + sc.fakeReq("GET", "/") + sc.req.SetBasicAuth("user", "password") + sc.exec() - So(sc.resp.Code, ShouldEqual, 401) - So(sc.respJson["message"], ShouldEqual, errStringInvalidUsernamePassword) + err := json.NewDecoder(sc.resp.Body).Decode(&sc.respJson) + require.Error(t, err) + + assert.Equal(t, 401, sc.resp.Code) + assert.Equal(t, errStringInvalidUsernamePassword, sc.respJson["message"]) + }) + + middlewareScenario(t, "Should return error if user & password do not match", func(t *testing.T, sc *scenarioContext) { + bus.AddHandler("user-query", func(loginUserQuery *models.GetUserByLoginQuery) error { + return nil }) - Convey("Destroy", func() { - setting.BasicAuthEnabled = oldBasicAuthEnabled - setting.DisableBruteForceLoginProtection = oldDisableBruteForceLoginProtection - }) + sc.fakeReq("GET", "/") + sc.req.SetBasicAuth("killa", "gorilla") + sc.exec() + + err := json.NewDecoder(sc.resp.Body).Decode(&sc.respJson) + require.Error(t, err) + + assert.Equal(t, 401, sc.resp.Code) + assert.Equal(t, errStringInvalidUsernamePassword, sc.respJson["message"]) }) } diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index f78906485c8..b4dfde543b9 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -11,7 +11,6 @@ import ( "testing" "time" - . "github.com/smartystreets/goconvey/convey" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/macaron.v1" @@ -45,226 +44,250 @@ func resetGetTime() { } func TestMiddleWareSecurityHeaders(t *testing.T) { + origErrTemplateName := setting.ErrTemplateName + t.Cleanup(func() { + setting.ErrTemplateName = origErrTemplateName + }) setting.ErrTemplateName = errorTemplate - Convey("Given the grafana middleware", t, func() { - middlewareScenario(t, "middleware should get correct x-xss-protection header", func(sc *scenarioContext) { - setting.XSSProtectionHeader = true - sc.fakeReq("GET", "/api/").exec() - So(sc.resp.Header().Get("X-XSS-Protection"), ShouldEqual, "1; mode=block") + middlewareScenario(t, "middleware should get correct x-xss-protection header", func(t *testing.T, sc *scenarioContext) { + origXSSProtectionHeader := setting.XSSProtectionHeader + t.Cleanup(func() { + setting.XSSProtectionHeader = origXSSProtectionHeader }) + setting.XSSProtectionHeader = true + sc.fakeReq("GET", "/api/").exec() + assert.Equal(t, "1; mode=block", sc.resp.Header().Get("X-XSS-Protection")) + }) - middlewareScenario(t, "middleware should not get x-xss-protection when disabled", func(sc *scenarioContext) { - setting.XSSProtectionHeader = false - sc.fakeReq("GET", "/api/").exec() - So(sc.resp.Header().Get("X-XSS-Protection"), ShouldBeEmpty) + middlewareScenario(t, "middleware should not get x-xss-protection when disabled", func(t *testing.T, sc *scenarioContext) { + origXSSProtectionHeader := setting.XSSProtectionHeader + t.Cleanup(func() { + setting.XSSProtectionHeader = origXSSProtectionHeader }) + setting.XSSProtectionHeader = false + sc.fakeReq("GET", "/api/").exec() + assert.Empty(t, sc.resp.Header().Get("X-XSS-Protection")) + }) - middlewareScenario(t, "middleware should add correct Strict-Transport-Security header", func(sc *scenarioContext) { - setting.StrictTransportSecurity = true - setting.Protocol = setting.HTTPSScheme - setting.StrictTransportSecurityMaxAge = 64000 - sc.fakeReq("GET", "/api/").exec() - So(sc.resp.Header().Get("Strict-Transport-Security"), ShouldEqual, "max-age=64000") - setting.StrictTransportSecurityPreload = true - sc.fakeReq("GET", "/api/").exec() - So(sc.resp.Header().Get("Strict-Transport-Security"), ShouldEqual, "max-age=64000; preload") - setting.StrictTransportSecuritySubDomains = true - sc.fakeReq("GET", "/api/").exec() - So(sc.resp.Header().Get("Strict-Transport-Security"), ShouldEqual, "max-age=64000; preload; includeSubDomains") + middlewareScenario(t, "middleware should add correct Strict-Transport-Security header", func(t *testing.T, sc *scenarioContext) { + origStrictTransportSecurity := setting.StrictTransportSecurity + origProtocol := setting.Protocol + origStrictTransportSecurityMaxAge := setting.StrictTransportSecurityMaxAge + t.Cleanup(func() { + setting.StrictTransportSecurity = origStrictTransportSecurity + setting.Protocol = origProtocol + setting.StrictTransportSecurityMaxAge = origStrictTransportSecurityMaxAge }) + setting.StrictTransportSecurity = true + setting.Protocol = setting.HTTPSScheme + setting.StrictTransportSecurityMaxAge = 64000 + + sc.fakeReq("GET", "/api/").exec() + assert.Equal(t, "max-age=64000", sc.resp.Header().Get("Strict-Transport-Security")) + setting.StrictTransportSecurityPreload = true + sc.fakeReq("GET", "/api/").exec() + assert.Equal(t, "max-age=64000; preload", sc.resp.Header().Get("Strict-Transport-Security")) + setting.StrictTransportSecuritySubDomains = true + sc.fakeReq("GET", "/api/").exec() + assert.Equal(t, "max-age=64000; preload; includeSubDomains", sc.resp.Header().Get("Strict-Transport-Security")) }) } func TestMiddlewareContext(t *testing.T) { + origErrTemplateName := setting.ErrTemplateName + t.Cleanup(func() { + setting.ErrTemplateName = origErrTemplateName + }) setting.ErrTemplateName = errorTemplate - Convey("Given the grafana middleware", t, func() { - middlewareScenario(t, "middleware should add context to injector", func(sc *scenarioContext) { - sc.fakeReq("GET", "/").exec() - So(sc.context, ShouldNotBeNil) - }) + middlewareScenario(t, "middleware should add context to injector", func(t *testing.T, sc *scenarioContext) { + sc.fakeReq("GET", "/").exec() + assert.NotNil(t, sc.context) + }) - middlewareScenario(t, "Default middleware should allow get request", func(sc *scenarioContext) { - sc.fakeReq("GET", "/").exec() - So(sc.resp.Code, ShouldEqual, 200) - }) + middlewareScenario(t, "Default middleware should allow get request", func(t *testing.T, sc *scenarioContext) { + sc.fakeReq("GET", "/").exec() + assert.Equal(t, 200, sc.resp.Code) + }) - middlewareScenario(t, "middleware should add Cache-Control header for requests to API", func(sc *scenarioContext) { - sc.fakeReq("GET", "/api/search").exec() - So(sc.resp.Header().Get("Cache-Control"), ShouldEqual, "no-cache") - So(sc.resp.Header().Get("Pragma"), ShouldEqual, "no-cache") - So(sc.resp.Header().Get("Expires"), ShouldEqual, "-1") - }) + middlewareScenario(t, "middleware should add Cache-Control header for requests to API", func(t *testing.T, sc *scenarioContext) { + sc.fakeReq("GET", "/api/search").exec() + assert.Equal(t, "no-cache", sc.resp.Header().Get("Cache-Control")) + assert.Equal(t, "no-cache", sc.resp.Header().Get("Pragma")) + assert.Equal(t, "-1", sc.resp.Header().Get("Expires")) + }) - middlewareScenario(t, "middleware should not add Cache-Control header for requests to datasource proxy API", func(sc *scenarioContext) { - sc.fakeReq("GET", "/api/datasources/proxy/1/test").exec() - So(sc.resp.Header().Get("Cache-Control"), ShouldBeEmpty) - So(sc.resp.Header().Get("Pragma"), ShouldBeEmpty) - So(sc.resp.Header().Get("Expires"), ShouldBeEmpty) - }) + middlewareScenario(t, "middleware should not add Cache-Control header for requests to datasource proxy API", func( + t *testing.T, sc *scenarioContext) { + sc.fakeReq("GET", "/api/datasources/proxy/1/test").exec() + assert.Empty(t, sc.resp.Header().Get("Cache-Control")) + assert.Empty(t, sc.resp.Header().Get("Pragma")) + assert.Empty(t, sc.resp.Header().Get("Expires")) + }) - middlewareScenario(t, "middleware should add Cache-Control header for requests with html response", func(sc *scenarioContext) { - sc.handler(func(c *models.ReqContext) { - data := &dtos.IndexViewData{ - User: &dtos.CurrentUser{}, - Settings: map[string]interface{}{}, - NavTree: []*dtos.NavLink{}, - } - c.HTML(200, "index-template", data) - }) - sc.fakeReq("GET", "/").exec() - So(sc.resp.Code, ShouldEqual, 200) - So(sc.resp.Header().Get("Cache-Control"), ShouldEqual, "no-cache") - So(sc.resp.Header().Get("Pragma"), ShouldEqual, "no-cache") - So(sc.resp.Header().Get("Expires"), ShouldEqual, "-1") - }) - - middlewareScenario(t, "middleware should add X-Frame-Options header with deny for request when not allowing embedding", func(sc *scenarioContext) { - sc.fakeReq("GET", "/api/search").exec() - So(sc.resp.Header().Get("X-Frame-Options"), ShouldEqual, "deny") - }) - - middlewareScenario(t, "middleware should not add X-Frame-Options header for request when allowing embedding", func(sc *scenarioContext) { - setting.AllowEmbedding = true - sc.fakeReq("GET", "/api/search").exec() - So(sc.resp.Header().Get("X-Frame-Options"), ShouldBeEmpty) - }) - - middlewareScenario(t, "Invalid api key", func(sc *scenarioContext) { - sc.apiKey = "invalid_key_test" - sc.fakeReq("GET", "/").exec() - - Convey("Should not init session", func() { - So(sc.resp.Header().Get("Set-Cookie"), ShouldBeEmpty) - }) - - Convey("Should return 401", func() { - So(sc.resp.Code, ShouldEqual, 401) - So(sc.respJson["message"], ShouldEqual, errStringInvalidAPIKey) - }) - }) - - middlewareScenario(t, "Valid api key", func(sc *scenarioContext) { - keyhash, err := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd") - So(err, ShouldBeNil) - - bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error { - query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash} - return nil - }) - - sc.fakeReq("GET", "/").withValidApiKey().exec() - - Convey("Should return 200", func() { - So(sc.resp.Code, ShouldEqual, 200) - }) - - Convey("Should init middleware context", func() { - So(sc.context.IsSignedIn, ShouldEqual, true) - So(sc.context.OrgId, ShouldEqual, 12) - So(sc.context.OrgRole, ShouldEqual, models.ROLE_EDITOR) - }) - }) - - middlewareScenario(t, "Valid api key, but does not match db hash", func(sc *scenarioContext) { - keyhash := "Something_not_matching" - - bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error { - query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash} - return nil - }) - - sc.fakeReq("GET", "/").withValidApiKey().exec() - - Convey("Should return api key invalid", func() { - So(sc.resp.Code, ShouldEqual, 401) - So(sc.respJson["message"], ShouldEqual, errStringInvalidAPIKey) - }) - }) - - middlewareScenario(t, "Valid api key, but expired", func(sc *scenarioContext) { - mockGetTime() - defer resetGetTime() - - keyhash, err := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd") - So(err, ShouldBeNil) - - bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error { - // api key expired one second before - expires := getTime().Add(-1 * time.Second).Unix() - query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash, - Expires: &expires} - return nil - }) - - sc.fakeReq("GET", "/").withValidApiKey().exec() - - Convey("Should return 401", func() { - So(sc.resp.Code, ShouldEqual, 401) - So(sc.respJson["message"], ShouldEqual, "Expired API key") - }) - }) - - middlewareScenario(t, "Non-expired auth token in cookie which not are being rotated", func(sc *scenarioContext) { - sc.withTokenSessionCookie("token") - - bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { - query.Result = &models.SignedInUser{OrgId: 2, UserId: 12} - return nil - }) - - sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { - return &models.UserToken{ - UserId: 12, - UnhashedToken: unhashedToken, - }, nil + middlewareScenario(t, "middleware should add Cache-Control header for requests with html response", func( + t *testing.T, sc *scenarioContext) { + sc.handler(func(c *models.ReqContext) { + data := &dtos.IndexViewData{ + User: &dtos.CurrentUser{}, + Settings: map[string]interface{}{}, + NavTree: []*dtos.NavLink{}, } + c.HTML(200, "index-template", data) + }) + sc.fakeReq("GET", "/").exec() + assert.Equal(t, 200, sc.resp.Code) + assert.Equal(t, "no-cache", sc.resp.Header().Get("Cache-Control")) + assert.Equal(t, "no-cache", sc.resp.Header().Get("Pragma")) + assert.Equal(t, "-1", sc.resp.Header().Get("Expires")) + }) - sc.fakeReq("GET", "/").exec() + middlewareScenario(t, "middleware should add X-Frame-Options header with deny for request when not allowing embedding", func( + t *testing.T, sc *scenarioContext) { + sc.fakeReq("GET", "/api/search").exec() + assert.Equal(t, "deny", sc.resp.Header().Get("X-Frame-Options")) + }) - Convey("Should init context with user info", func() { - So(sc.context.IsSignedIn, ShouldBeTrue) - So(sc.context.UserId, ShouldEqual, 12) - So(sc.context.UserToken.UserId, ShouldEqual, 12) - So(sc.context.UserToken.UnhashedToken, ShouldEqual, "token") - }) + middlewareScenario(t, "middleware should not add X-Frame-Options header for request when allowing embedding", func( + t *testing.T, sc *scenarioContext) { + origAllowEmbedding := setting.AllowEmbedding + t.Cleanup(func() { + setting.AllowEmbedding = origAllowEmbedding + }) + setting.AllowEmbedding = true + sc.fakeReq("GET", "/api/search").exec() + assert.Empty(t, sc.resp.Header().Get("X-Frame-Options")) + }) - Convey("Should not set cookie", func() { - So(sc.resp.Header().Get("Set-Cookie"), ShouldEqual, "") - }) + middlewareScenario(t, "Invalid api key", func(t *testing.T, sc *scenarioContext) { + sc.apiKey = "invalid_key_test" + sc.fakeReq("GET", "/").exec() + + assert.Empty(t, sc.resp.Header().Get("Set-Cookie")) + assert.Equal(t, 401, sc.resp.Code) + assert.Equal(t, errStringInvalidAPIKey, sc.respJson["message"]) + }) + + middlewareScenario(t, "Valid api key", func(t *testing.T, sc *scenarioContext) { + const orgID int64 = 12 + keyhash, err := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd") + require.NoError(t, err) + + bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error { + query.Result = &models.ApiKey{OrgId: orgID, Role: models.ROLE_EDITOR, Key: keyhash} + return nil }) - middlewareScenario(t, "Non-expired auth token in cookie which are being rotated", func(sc *scenarioContext) { - sc.withTokenSessionCookie("token") + sc.fakeReq("GET", "/").withValidApiKey().exec() - bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { - query.Result = &models.SignedInUser{OrgId: 2, UserId: 12} - return nil - }) + assert.Equal(t, 200, sc.resp.Code) - sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { - return &models.UserToken{ - UserId: 12, - UnhashedToken: "", - }, nil - } + assert.True(t, sc.context.IsSignedIn) + assert.Equal(t, orgID, sc.context.OrgId) + assert.Equal(t, models.ROLE_EDITOR, sc.context.OrgRole) + }) - sc.userAuthTokenService.TryRotateTokenProvider = func(ctx context.Context, userToken *models.UserToken, - clientIP net.IP, userAgent string) (bool, error) { - userToken.UnhashedToken = "rotated" - return true, nil - } + middlewareScenario(t, "Valid api key, but does not match db hash", func(t *testing.T, sc *scenarioContext) { + keyhash := "Something_not_matching" - maxAge := int(setting.LoginMaxLifetime.Seconds()) + bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error { + query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash} + return nil + }) + + sc.fakeReq("GET", "/").withValidApiKey().exec() + + assert.Equal(t, 401, sc.resp.Code) + assert.Equal(t, errStringInvalidAPIKey, sc.respJson["message"]) + }) + + middlewareScenario(t, "Valid api key, but expired", func(t *testing.T, sc *scenarioContext) { + mockGetTime() + defer resetGetTime() + + keyhash, err := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd") + require.NoError(t, err) + + bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error { + // api key expired one second before + expires := getTime().Add(-1 * time.Second).Unix() + query.Result = &models.ApiKey{OrgId: 12, Role: models.ROLE_EDITOR, Key: keyhash, + Expires: &expires} + return nil + }) + + sc.fakeReq("GET", "/").withValidApiKey().exec() + + assert.Equal(t, 401, sc.resp.Code) + assert.Equal(t, "Expired API key", sc.respJson["message"]) + }) + + middlewareScenario(t, "Non-expired auth token in cookie which not are being rotated", func( + t *testing.T, sc *scenarioContext) { + const userID int64 = 12 + + sc.withTokenSessionCookie("token") + + bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { + query.Result = &models.SignedInUser{OrgId: 2, UserId: userID} + return nil + }) + + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { + return &models.UserToken{ + UserId: userID, + UnhashedToken: unhashedToken, + }, nil + } + + sc.fakeReq("GET", "/").exec() + + assert.True(t, sc.context.IsSignedIn) + assert.Equal(t, userID, sc.context.UserId) + assert.Equal(t, userID, sc.context.UserToken.UserId) + assert.Equal(t, "token", sc.context.UserToken.UnhashedToken) + assert.Equal(t, "", sc.resp.Header().Get("Set-Cookie")) + }) + + middlewareScenario(t, "Non-expired auth token in cookie which are being rotated", func(t *testing.T, sc *scenarioContext) { + const userID int64 = 12 + + sc.withTokenSessionCookie("token") + + bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { + query.Result = &models.SignedInUser{OrgId: 2, UserId: userID} + return nil + }) + + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { + return &models.UserToken{ + UserId: userID, + UnhashedToken: "", + }, nil + } + + sc.userAuthTokenService.TryRotateTokenProvider = func(ctx context.Context, userToken *models.UserToken, + clientIP net.IP, userAgent string) (bool, error) { + userToken.UnhashedToken = "rotated" + return true, nil + } + + maxAge := int(setting.LoginMaxLifetime.Seconds()) + + sameSiteModes := []http.SameSite{ + http.SameSiteNoneMode, + http.SameSiteLaxMode, + http.SameSiteStrictMode, + } + for _, sameSiteMode := range sameSiteModes { + t.Run(fmt.Sprintf("Same site mode %d", sameSiteMode), func(t *testing.T) { + origCookieSameSiteMode := setting.CookieSameSiteMode + t.Cleanup(func() { + setting.CookieSameSiteMode = origCookieSameSiteMode + }) + setting.CookieSameSiteMode = sameSiteMode - sameSitePolicies := []http.SameSite{ - http.SameSiteNoneMode, - http.SameSiteLaxMode, - http.SameSiteStrictMode, - } - for _, sameSitePolicy := range sameSitePolicies { - setting.CookieSameSiteMode = sameSitePolicy expectedCookiePath := "/" if len(setting.AppSubUrl) > 0 { expectedCookiePath = setting.AppSubUrl @@ -276,283 +299,334 @@ func TestMiddlewareContext(t *testing.T) { HttpOnly: true, MaxAge: maxAge, Secure: setting.CookieSecure, - SameSite: sameSitePolicy, + SameSite: sameSiteMode, } sc.fakeReq("GET", "/").exec() - Convey(fmt.Sprintf("Should init context with user info and setting.SameSite=%v", sameSitePolicy), func() { - So(sc.context.IsSignedIn, ShouldBeTrue) - So(sc.context.UserId, ShouldEqual, 12) - So(sc.context.UserToken.UserId, ShouldEqual, 12) - So(sc.context.UserToken.UnhashedToken, ShouldEqual, "rotated") - }) - - Convey(fmt.Sprintf("Should set cookie with setting.SameSite=%v", sameSitePolicy), func() { - So(sc.resp.Header().Get("Set-Cookie"), ShouldEqual, expectedCookie.String()) - }) - } - - Convey("Should not set cookie with SameSite attribute when setting.CookieSameSiteDisabled is true", func() { - setting.CookieSameSiteDisabled = true - setting.CookieSameSiteMode = http.SameSiteLaxMode - expectedCookiePath := "/" - if len(setting.AppSubUrl) > 0 { - expectedCookiePath = setting.AppSubUrl - } - expectedCookie := &http.Cookie{ - Name: setting.LoginCookieName, - Value: "rotated", - Path: expectedCookiePath, - HttpOnly: true, - MaxAge: maxAge, - Secure: setting.CookieSecure, - } - - sc.fakeReq("GET", "/").exec() - So(sc.resp.Header().Get("Set-Cookie"), ShouldEqual, expectedCookie.String()) + assert.True(t, sc.context.IsSignedIn) + assert.Equal(t, userID, sc.context.UserId) + assert.Equal(t, userID, sc.context.UserToken.UserId) + assert.Equal(t, "rotated", sc.context.UserToken.UnhashedToken) + assert.Equal(t, expectedCookie.String(), sc.resp.Header().Get("Set-Cookie")) }) - }) + } - middlewareScenario(t, "Invalid/expired auth token in cookie", func(sc *scenarioContext) { - sc.withTokenSessionCookie("token") + t.Run("Should not set cookie with SameSite attribute when setting.CookieSameSiteDisabled is true", func(t *testing.T) { + origCookieSameSiteDisabled := setting.CookieSameSiteDisabled + origCookieSameSiteMode := setting.CookieSameSiteMode + t.Cleanup(func() { + setting.CookieSameSiteDisabled = origCookieSameSiteDisabled + setting.CookieSameSiteMode = origCookieSameSiteMode + }) + setting.CookieSameSiteDisabled = true + setting.CookieSameSiteMode = http.SameSiteLaxMode - sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { - return nil, models.ErrUserTokenNotFound + expectedCookiePath := "/" + if len(setting.AppSubUrl) > 0 { + expectedCookiePath = setting.AppSubUrl + } + expectedCookie := &http.Cookie{ + Name: setting.LoginCookieName, + Value: "rotated", + Path: expectedCookiePath, + HttpOnly: true, + MaxAge: maxAge, + Secure: setting.CookieSecure, } sc.fakeReq("GET", "/").exec() + assert.Equal(t, expectedCookie.String(), sc.resp.Header().Get("Set-Cookie")) + }) + }) - Convey("Should not init context with user info", func() { - So(sc.context.IsSignedIn, ShouldBeFalse) - So(sc.context.UserId, ShouldEqual, 0) - So(sc.context.UserToken, ShouldBeNil) - }) + middlewareScenario(t, "Invalid/expired auth token in cookie", func(t *testing.T, sc *scenarioContext) { + sc.withTokenSessionCookie("token") + + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { + return nil, models.ErrUserTokenNotFound + } + + sc.fakeReq("GET", "/").exec() + + assert.False(t, sc.context.IsSignedIn) + assert.Equal(t, int64(0), sc.context.UserId) + assert.Nil(t, sc.context.UserToken) + }) + + middlewareScenario(t, "When anonymous access is enabled", func(t *testing.T, sc *scenarioContext) { + const orgID int64 = 2 + + origAnonymousEnabled := setting.AnonymousEnabled + origAnonymousOrgName := setting.AnonymousOrgName + origAnonymousOrgRole := setting.AnonymousOrgRole + t.Cleanup(func() { + setting.AnonymousEnabled = origAnonymousEnabled + setting.AnonymousOrgName = origAnonymousOrgName + setting.AnonymousOrgRole = origAnonymousOrgRole + }) + setting.AnonymousEnabled = true + setting.AnonymousOrgName = "test" + setting.AnonymousOrgRole = string(models.ROLE_EDITOR) + + bus.AddHandler("test", func(query *models.GetOrgByNameQuery) error { + assert.Equal(t, "test", query.Name) + + query.Result = &models.Org{Id: orgID, Name: "test"} + return nil }) - middlewareScenario(t, "When anonymous access is enabled", func(sc *scenarioContext) { - setting.AnonymousEnabled = true - setting.AnonymousOrgName = "test" - setting.AnonymousOrgRole = string(models.ROLE_EDITOR) + sc.fakeReq("GET", "/").exec() - bus.AddHandler("test", func(query *models.GetOrgByNameQuery) error { - So(query.Name, ShouldEqual, "test") + assert.Equal(t, int64(0), sc.context.UserId) + assert.Equal(t, orgID, sc.context.OrgId) + assert.Equal(t, models.ROLE_EDITOR, sc.context.OrgRole) + assert.False(t, sc.context.IsSignedIn) + }) - query.Result = &models.Org{Id: 2, Name: "test"} + t.Run("auth_proxy", func(t *testing.T) { + const userID int64 = 33 + const orgID int64 = 4 + + origAuthProxyEnabled := setting.AuthProxyEnabled + origAuthProxyWhitelist := setting.AuthProxyWhitelist + origAuthProxyAutoSignUp := setting.AuthProxyAutoSignUp + origLDAPEnabled := setting.LDAPEnabled + origAuthProxyHeaderName := setting.AuthProxyHeaderName + origAuthProxyHeaderProperty := setting.AuthProxyHeaderProperty + origAuthProxyHeaders := setting.AuthProxyHeaders + t.Cleanup(func() { + setting.AuthProxyEnabled = origAuthProxyEnabled + setting.AuthProxyWhitelist = origAuthProxyWhitelist + setting.AuthProxyAutoSignUp = origAuthProxyAutoSignUp + setting.LDAPEnabled = origLDAPEnabled + setting.AuthProxyHeaderName = origAuthProxyHeaderName + setting.AuthProxyHeaderProperty = origAuthProxyHeaderProperty + setting.AuthProxyHeaders = origAuthProxyHeaders + }) + setting.AuthProxyEnabled = true + setting.AuthProxyWhitelist = "" + setting.AuthProxyAutoSignUp = true + setting.LDAPEnabled = true + setting.AuthProxyHeaderName = "X-WEBAUTH-USER" + setting.AuthProxyHeaderProperty = "username" + setting.AuthProxyHeaders = map[string]string{"Groups": "X-WEBAUTH-GROUPS"} + + const hdrName = "markelog" + const group = "grafana-core-team" + + middlewareScenario(t, "Should not sync the user if it's in the cache", func(t *testing.T, sc *scenarioContext) { + bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { + query.Result = &models.SignedInUser{OrgId: orgID, UserId: query.UserId} return nil }) - sc.fakeReq("GET", "/").exec() + key := fmt.Sprintf(authproxy.CachePrefix, authproxy.HashCacheKey(hdrName+"-"+group)) + err := sc.remoteCacheService.Set(key, userID, 0) + require.NoError(t, err) + sc.fakeReq("GET", "/") - Convey("Should init context with org info", func() { - So(sc.context.UserId, ShouldEqual, 0) - So(sc.context.OrgId, ShouldEqual, 2) - So(sc.context.OrgRole, ShouldEqual, models.ROLE_EDITOR) - }) + sc.req.Header.Set(setting.AuthProxyHeaderName, hdrName) + sc.req.Header.Set("X-WEBAUTH-GROUPS", group) + sc.exec() - Convey("context signed in should be false", func() { - So(sc.context.IsSignedIn, ShouldBeFalse) - }) + assert.True(t, sc.context.IsSignedIn) + assert.Equal(t, userID, sc.context.UserId) + assert.Equal(t, orgID, sc.context.OrgId) }) - Convey("auth_proxy", func() { - setting.AuthProxyEnabled = true - setting.AuthProxyWhitelist = "" + middlewareScenario(t, "Should respect auto signup option", func(t *testing.T, sc *scenarioContext) { + origLDAPEnabled = setting.LDAPEnabled + origAuthProxyAutoSignUp = setting.AuthProxyAutoSignUp + t.Cleanup(func() { + setting.LDAPEnabled = origLDAPEnabled + setting.AuthProxyAutoSignUp = origAuthProxyAutoSignUp + }) + setting.LDAPEnabled = false + setting.AuthProxyAutoSignUp = false + + var actualAuthProxyAutoSignUp *bool = nil + + bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error { + actualAuthProxyAutoSignUp = &cmd.SignupAllowed + return login.ErrInvalidCredentials + }) + + sc.fakeReq("GET", "/") + sc.req.Header.Set(setting.AuthProxyHeaderName, hdrName) + sc.exec() + + assert.False(t, *actualAuthProxyAutoSignUp) + assert.Equal(t, sc.resp.Code, 407) + assert.Nil(t, sc.context) + }) + + middlewareScenario(t, "Should create an user from a header", func(t *testing.T, sc *scenarioContext) { + origLDAPEnabled = setting.LDAPEnabled + origAuthProxyAutoSignUp = setting.AuthProxyAutoSignUp + t.Cleanup(func() { + setting.LDAPEnabled = origLDAPEnabled + setting.AuthProxyAutoSignUp = origAuthProxyAutoSignUp + }) + setting.LDAPEnabled = false setting.AuthProxyAutoSignUp = true - setting.LDAPEnabled = true - setting.AuthProxyHeaderName = "X-WEBAUTH-USER" - setting.AuthProxyHeaderProperty = "username" - setting.AuthProxyHeaders = map[string]string{"Groups": "X-WEBAUTH-GROUPS"} - name := "markelog" - group := "grafana-core-team" - middlewareScenario(t, "Should not sync the user if it's in the cache", func(sc *scenarioContext) { - bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { - query.Result = &models.SignedInUser{OrgId: 4, UserId: query.UserId} + bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { + if query.UserId > 0 { + query.Result = &models.SignedInUser{OrgId: orgID, UserId: userID} return nil - }) - - key := fmt.Sprintf(authproxy.CachePrefix, authproxy.HashCacheKey(name+"-"+group)) - err := sc.remoteCacheService.Set(key, int64(33), 0) - So(err, ShouldBeNil) - sc.fakeReq("GET", "/") - - sc.req.Header.Add(setting.AuthProxyHeaderName, name) - sc.req.Header.Add("X-WEBAUTH-GROUPS", group) - sc.exec() - - Convey("Should init user via cache", func() { - So(sc.context.IsSignedIn, ShouldBeTrue) - So(sc.context.UserId, ShouldEqual, 33) - So(sc.context.OrgId, ShouldEqual, 4) - }) + } + return models.ErrUserNotFound }) - middlewareScenario(t, "Should respect auto signup option", func(sc *scenarioContext) { - setting.LDAPEnabled = false - setting.AuthProxyAutoSignUp = false - var actualAuthProxyAutoSignUp *bool = nil - - bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error { - actualAuthProxyAutoSignUp = &cmd.SignupAllowed - return login.ErrInvalidCredentials - }) - - sc.fakeReq("GET", "/") - sc.req.Header.Add(setting.AuthProxyHeaderName, name) - sc.exec() - - assert.False(t, *actualAuthProxyAutoSignUp) - assert.Equal(t, sc.resp.Code, 407) - assert.Nil(t, sc.context) + bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error { + cmd.Result = &models.User{Id: userID} + return nil }) - middlewareScenario(t, "Should create an user from a header", func(sc *scenarioContext) { - setting.LDAPEnabled = false - setting.AuthProxyAutoSignUp = true + sc.fakeReq("GET", "/") + sc.req.Header.Set(setting.AuthProxyHeaderName, hdrName) + sc.exec() - bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { - if query.UserId > 0 { - query.Result = &models.SignedInUser{OrgId: 4, UserId: 33} - return nil - } - return models.ErrUserNotFound - }) + assert.True(t, sc.context.IsSignedIn) + assert.Equal(t, userID, sc.context.UserId) + assert.Equal(t, orgID, sc.context.OrgId) + }) - bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error { - cmd.Result = &models.User{Id: 33} - return nil - }) + middlewareScenario(t, "Should get an existing user from header", func(t *testing.T, sc *scenarioContext) { + const userID int64 = 12 + const orgID int64 = 2 - sc.fakeReq("GET", "/") - sc.req.Header.Add(setting.AuthProxyHeaderName, name) - sc.exec() + origLDAPEnabled = setting.LDAPEnabled + t.Cleanup(func() { + setting.LDAPEnabled = origLDAPEnabled + }) + setting.LDAPEnabled = false - Convey("Should create user from header info", func() { - So(sc.context.IsSignedIn, ShouldBeTrue) - So(sc.context.UserId, ShouldEqual, 33) - So(sc.context.OrgId, ShouldEqual, 4) - }) + bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { + query.Result = &models.SignedInUser{OrgId: orgID, UserId: userID} + return nil }) - middlewareScenario(t, "Should get an existing user from header", func(sc *scenarioContext) { - setting.LDAPEnabled = false - - bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { - query.Result = &models.SignedInUser{OrgId: 2, UserId: 12} - return nil - }) - - bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error { - cmd.Result = &models.User{Id: 12} - return nil - }) - - sc.fakeReq("GET", "/") - sc.req.Header.Add(setting.AuthProxyHeaderName, name) - sc.exec() - - Convey("Should init context with user info", func() { - So(sc.context.IsSignedIn, ShouldBeTrue) - So(sc.context.UserId, ShouldEqual, 12) - So(sc.context.OrgId, ShouldEqual, 2) - }) + bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error { + cmd.Result = &models.User{Id: userID} + return nil }) - middlewareScenario(t, "Should allow the request from whitelist IP", func(sc *scenarioContext) { - setting.AuthProxyWhitelist = "192.168.1.0/24, 2001::0/120" - setting.LDAPEnabled = false + sc.fakeReq("GET", "/") + sc.req.Header.Set(setting.AuthProxyHeaderName, hdrName) + sc.exec() - bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { - query.Result = &models.SignedInUser{OrgId: 4, UserId: 33} - return nil - }) + assert.True(t, sc.context.IsSignedIn) + assert.Equal(t, userID, sc.context.UserId) + assert.Equal(t, orgID, sc.context.OrgId) + }) - bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error { - cmd.Result = &models.User{Id: 33} - return nil - }) + middlewareScenario(t, "Should allow the request from whitelist IP", func(t *testing.T, sc *scenarioContext) { + origAuthProxyWhitelist = setting.AuthProxyWhitelist + origLDAPEnabled = setting.LDAPEnabled + t.Cleanup(func() { + setting.AuthProxyWhitelist = origAuthProxyWhitelist + setting.LDAPEnabled = origLDAPEnabled + }) + setting.AuthProxyWhitelist = "192.168.1.0/24, 2001::0/120" + setting.LDAPEnabled = false - sc.fakeReq("GET", "/") - sc.req.Header.Add(setting.AuthProxyHeaderName, name) - sc.req.RemoteAddr = "[2001::23]:12345" - sc.exec() - - Convey("Should init context with user info", func() { - So(sc.context.IsSignedIn, ShouldBeTrue) - So(sc.context.UserId, ShouldEqual, 33) - So(sc.context.OrgId, ShouldEqual, 4) - }) + bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { + query.Result = &models.SignedInUser{OrgId: orgID, UserId: userID} + return nil }) - middlewareScenario(t, "Should not allow the request from whitelist IP", func(sc *scenarioContext) { - setting.AuthProxyWhitelist = "8.8.8.8" - setting.LDAPEnabled = false - - bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { - query.Result = &models.SignedInUser{OrgId: 4, UserId: 33} - return nil - }) - - bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error { - cmd.Result = &models.User{Id: 33} - return nil - }) - - sc.fakeReq("GET", "/") - sc.req.Header.Add(setting.AuthProxyHeaderName, name) - sc.req.RemoteAddr = "[2001::23]:12345" - sc.exec() - - Convey("Should return 407 status code", func() { - So(sc.resp.Code, ShouldEqual, 407) - So(sc.context, ShouldBeNil) - }) + bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error { + cmd.Result = &models.User{Id: userID} + return nil }) - middlewareScenario(t, "Should return 407 status code if LDAP says no", func(sc *scenarioContext) { - bus.AddHandler("LDAP", func(cmd *models.UpsertUserCommand) error { - return errors.New("Do not add user") - }) + sc.fakeReq("GET", "/") + sc.req.Header.Set(setting.AuthProxyHeaderName, hdrName) + sc.req.RemoteAddr = "[2001::23]:12345" + sc.exec() - sc.fakeReq("GET", "/") - sc.req.Header.Add(setting.AuthProxyHeaderName, name) - sc.exec() + assert.True(t, sc.context.IsSignedIn) + assert.Equal(t, userID, sc.context.UserId) + assert.Equal(t, orgID, sc.context.OrgId) + }) - Convey("Should return 407 status code", func() { - So(sc.resp.Code, ShouldEqual, 407) - So(sc.context, ShouldBeNil) - }) + middlewareScenario(t, "Should not allow the request from whitelist IP", func(t *testing.T, sc *scenarioContext) { + origAuthProxyWhitelist = setting.AuthProxyWhitelist + origLDAPEnabled = setting.LDAPEnabled + t.Cleanup(func() { + setting.AuthProxyWhitelist = origAuthProxyWhitelist + setting.LDAPEnabled = origLDAPEnabled + }) + setting.AuthProxyWhitelist = "8.8.8.8" + setting.LDAPEnabled = false + + bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { + query.Result = &models.SignedInUser{OrgId: orgID, UserId: userID} + return nil }) - middlewareScenario(t, "Should return 407 status code if there is cache mishap", func(sc *scenarioContext) { - bus.AddHandler("Do not have the user", func(query *models.GetSignedInUserQuery) error { - return errors.New("Do not add user") - }) - - sc.fakeReq("GET", "/") - sc.req.Header.Add(setting.AuthProxyHeaderName, name) - sc.exec() - - Convey("Should return 407 status code", func() { - So(sc.resp.Code, ShouldEqual, 407) - So(sc.context, ShouldBeNil) - }) + bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error { + cmd.Result = &models.User{Id: userID} + return nil }) + + sc.fakeReq("GET", "/") + sc.req.Header.Set(setting.AuthProxyHeaderName, hdrName) + sc.req.RemoteAddr = "[2001::23]:12345" + sc.exec() + + assert.Equal(t, 407, sc.resp.Code) + assert.Nil(t, sc.context) + }) + + middlewareScenario(t, "Should return 407 status code if LDAP says no", func(t *testing.T, sc *scenarioContext) { + bus.AddHandler("LDAP", func(cmd *models.UpsertUserCommand) error { + return errors.New("Do not add user") + }) + + sc.fakeReq("GET", "/") + sc.req.Header.Set(setting.AuthProxyHeaderName, hdrName) + sc.exec() + + assert.Equal(t, 407, sc.resp.Code) + assert.Nil(t, sc.context) + }) + + middlewareScenario(t, "Should return 407 status code if there is cache mishap", func(t *testing.T, sc *scenarioContext) { + bus.AddHandler("Do not have the user", func(query *models.GetSignedInUserQuery) error { + return errors.New("Do not add user") + }) + + sc.fakeReq("GET", "/") + sc.req.Header.Set(setting.AuthProxyHeaderName, hdrName) + sc.exec() + + assert.Equal(t, 407, sc.resp.Code) + assert.Nil(t, sc.context) }) }) } func middlewareScenario(t *testing.T, desc string, fn scenarioFunc) { - Convey(desc, func() { - defer bus.ClearBusHandlers() + t.Helper() + t.Run(desc, func(t *testing.T) { + t.Cleanup(bus.ClearBusHandlers) + + origLoginCookieName := setting.LoginCookieName + origLoginMaxLifetime := setting.LoginMaxLifetime + t.Cleanup(func() { + setting.LoginCookieName = origLoginCookieName + setting.LoginMaxLifetime = origLoginMaxLifetime + }) setting.LoginCookieName = "grafana_session" var err error setting.LoginMaxLifetime, err = gtime.ParseDuration("30d") require.NoError(t, err) - sc := &scenarioContext{} + sc := &scenarioContext{t: t} viewsPath, err := filepath.Abs("../../public/views") require.NoError(t, err) @@ -584,13 +658,13 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc) { sc.m.Get("/", sc.defaultHandler) - fn(sc) + fn(t, sc) }) } func TestDontRotateTokensOnCancelledRequests(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) - reqContext, _, err := initTokenRotationTest(ctx) + reqContext, _, err := initTokenRotationTest(ctx, t) require.NoError(t, err) tryRotateCallCount := 0 @@ -612,7 +686,7 @@ func TestDontRotateTokensOnCancelledRequests(t *testing.T) { } func TestTokenRotationAtEndOfRequest(t *testing.T) { - reqContext, rr, err := initTokenRotationTest(context.Background()) + reqContext, rr, err := initTokenRotationTest(context.Background(), t) require.NoError(t, err) uts := &auth.FakeUserAuthTokenService{ @@ -643,7 +717,15 @@ func TestTokenRotationAtEndOfRequest(t *testing.T) { assert.True(t, foundLoginCookie, "Could not find cookie") } -func initTokenRotationTest(ctx context.Context) (*models.ReqContext, *httptest.ResponseRecorder, error) { +func initTokenRotationTest(ctx context.Context, t *testing.T) (*models.ReqContext, *httptest.ResponseRecorder, error) { + t.Helper() + + origLoginCookieName := setting.LoginCookieName + origLoginMaxLifetime := setting.LoginMaxLifetime + t.Cleanup(func() { + setting.LoginCookieName = origLoginCookieName + setting.LoginMaxLifetime = origLoginMaxLifetime + }) setting.LoginCookieName = "login_token" var err error setting.LoginMaxLifetime, err = gtime.ParseDuration("7d") diff --git a/pkg/middleware/org_redirect_test.go b/pkg/middleware/org_redirect_test.go index f0698ba30e4..0d1ababb4c9 100644 --- a/pkg/middleware/org_redirect_test.go +++ b/pkg/middleware/org_redirect_test.go @@ -7,61 +7,55 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" - . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/assert" ) func TestOrgRedirectMiddleware(t *testing.T) { - Convey("Can redirect to correct org", t, func() { - middlewareScenario(t, "when setting a correct org for the user", func(sc *scenarioContext) { - sc.withTokenSessionCookie("token") - bus.AddHandler("test", func(query *models.SetUsingOrgCommand) error { - return nil - }) - - bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { - query.Result = &models.SignedInUser{OrgId: 1, UserId: 12} - return nil - }) - - sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { - return &models.UserToken{ - UserId: 0, - UnhashedToken: "", - }, nil - } - - sc.m.Get("/", sc.defaultHandler) - sc.fakeReq("GET", "/?orgId=3").exec() - - Convey("change org and redirect", func() { - So(sc.resp.Code, ShouldEqual, 302) - }) + middlewareScenario(t, "when setting a correct org for the user", func(t *testing.T, sc *scenarioContext) { + sc.withTokenSessionCookie("token") + bus.AddHandler("test", func(query *models.SetUsingOrgCommand) error { + return nil }) - middlewareScenario(t, "when setting an invalid org for user", func(sc *scenarioContext) { - sc.withTokenSessionCookie("token") - bus.AddHandler("test", func(query *models.SetUsingOrgCommand) error { - return fmt.Errorf("") - }) - - bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { - query.Result = &models.SignedInUser{OrgId: 1, UserId: 12} - return nil - }) - - sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { - return &models.UserToken{ - UserId: 12, - UnhashedToken: "", - }, nil - } - - sc.m.Get("/", sc.defaultHandler) - sc.fakeReq("GET", "/?orgId=3").exec() - - Convey("not allowed to change org", func() { - So(sc.resp.Code, ShouldEqual, 404) - }) + bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { + query.Result = &models.SignedInUser{OrgId: 1, UserId: 12} + return nil }) + + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { + return &models.UserToken{ + UserId: 0, + UnhashedToken: "", + }, nil + } + + sc.m.Get("/", sc.defaultHandler) + sc.fakeReq("GET", "/?orgId=3").exec() + + assert.Equal(t, 302, sc.resp.Code) + }) + + middlewareScenario(t, "when setting an invalid org for user", func(t *testing.T, sc *scenarioContext) { + sc.withTokenSessionCookie("token") + bus.AddHandler("test", func(query *models.SetUsingOrgCommand) error { + return fmt.Errorf("") + }) + + bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { + query.Result = &models.SignedInUser{OrgId: 1, UserId: 12} + return nil + }) + + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { + return &models.UserToken{ + UserId: 12, + UnhashedToken: "", + }, nil + } + + sc.m.Get("/", sc.defaultHandler) + sc.fakeReq("GET", "/?orgId=3").exec() + + assert.Equal(t, 404, sc.resp.Code) }) } diff --git a/pkg/middleware/quota_test.go b/pkg/middleware/quota_test.go index 75135e20612..bf2696d5f04 100644 --- a/pkg/middleware/quota_test.go +++ b/pkg/middleware/quota_test.go @@ -9,40 +9,40 @@ import ( "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/setting" - . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/assert" ) func TestMiddlewareQuota(t *testing.T) { - Convey("Given the grafana quota middleware", t, func() { - setting.AnonymousEnabled = false - setting.Quota = setting.QuotaSettings{ - Enabled: true, - Org: &setting.OrgQuota{ - User: 5, - Dashboard: 5, - DataSource: 5, - ApiKey: 5, - }, - User: &setting.UserQuota{ - Org: 5, - }, - Global: &setting.GlobalQuota{ - Org: 5, - User: 5, - Dashboard: 5, - DataSource: 5, - ApiKey: 5, - Session: 5, - }, - } + setting.AnonymousEnabled = false + setting.Quota = setting.QuotaSettings{ + Enabled: true, + Org: &setting.OrgQuota{ + User: 5, + Dashboard: 5, + DataSource: 5, + ApiKey: 5, + }, + User: &setting.UserQuota{ + Org: 5, + }, + Global: &setting.GlobalQuota{ + Org: 5, + User: 5, + Dashboard: 5, + DataSource: 5, + ApiKey: 5, + Session: 5, + }, + } - fakeAuthTokenService := auth.NewFakeUserAuthTokenService() - qs := "a.QuotaService{ - AuthTokenService: fakeAuthTokenService, - } - QuotaFn := Quota(qs) + fakeAuthTokenService := auth.NewFakeUserAuthTokenService() + qs := "a.QuotaService{ + AuthTokenService: fakeAuthTokenService, + } + quotaFn := Quota(qs) - middlewareScenario(t, "with user not logged in", func(sc *scenarioContext) { + t.Run("With user not logged in", func(t *testing.T) { + middlewareScenario(t, "and global quota not reached", func(t *testing.T, sc *scenarioContext) { bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error { query.Result = &models.GlobalQuotaDTO{ Target: query.Target, @@ -52,48 +52,12 @@ func TestMiddlewareQuota(t *testing.T) { return nil }) - Convey("global quota not reached", func() { - sc.m.Get("/user", QuotaFn("user"), sc.defaultHandler) - sc.fakeReq("GET", "/user").exec() - So(sc.resp.Code, ShouldEqual, 200) - }) - - Convey("global quota reached", func() { - setting.Quota.Global.User = 4 - sc.m.Get("/user", QuotaFn("user"), sc.defaultHandler) - sc.fakeReq("GET", "/user").exec() - So(sc.resp.Code, ShouldEqual, 403) - }) - - Convey("global session quota not reached", func() { - setting.Quota.Global.Session = 10 - sc.m.Get("/user", QuotaFn("session"), sc.defaultHandler) - sc.fakeReq("GET", "/user").exec() - So(sc.resp.Code, ShouldEqual, 200) - }) - - Convey("global session quota reached", func() { - setting.Quota.Global.Session = 1 - sc.m.Get("/user", QuotaFn("session"), sc.defaultHandler) - sc.fakeReq("GET", "/user").exec() - So(sc.resp.Code, ShouldEqual, 403) - }) + sc.m.Get("/user", quotaFn("user"), sc.defaultHandler) + sc.fakeReq("GET", "/user").exec() + assert.Equal(t, 200, sc.resp.Code) }) - middlewareScenario(t, "with user logged in", func(sc *scenarioContext) { - sc.withTokenSessionCookie("token") - bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { - query.Result = &models.SignedInUser{OrgId: 2, UserId: 12} - return nil - }) - - sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { - return &models.UserToken{ - UserId: 12, - UnhashedToken: "", - }, nil - } - + middlewareScenario(t, "and global quota reached", func(t *testing.T, sc *scenarioContext) { bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error { query.Result = &models.GlobalQuotaDTO{ Target: query.Target, @@ -103,8 +67,20 @@ func TestMiddlewareQuota(t *testing.T) { return nil }) - bus.AddHandler("userQuota", func(query *models.GetUserQuotaByTargetQuery) error { - query.Result = &models.UserQuotaDTO{ + origUser := setting.Quota.Global.User + t.Cleanup(func() { + setting.Quota.Global.User = origUser + }) + setting.Quota.Global.User = 4 + + sc.m.Get("/user", quotaFn("user"), sc.defaultHandler) + sc.fakeReq("GET", "/user").exec() + assert.Equal(t, 403, sc.resp.Code) + }) + + middlewareScenario(t, "and global session quota not reached", func(t *testing.T, sc *scenarioContext) { + bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error { + query.Result = &models.GlobalQuotaDTO{ Target: query.Target, Limit: query.Default, Used: 4, @@ -112,57 +88,112 @@ func TestMiddlewareQuota(t *testing.T) { return nil }) - bus.AddHandler("orgQuota", func(query *models.GetOrgQuotaByTargetQuery) error { - query.Result = &models.OrgQuotaDTO{ - Target: query.Target, - Limit: query.Default, - Used: 4, - } - return nil + origSession := setting.Quota.Global.Session + t.Cleanup(func() { + setting.Quota.Global.Session = origSession }) + setting.Quota.Global.Session = 10 - Convey("global datasource quota reached", func() { - setting.Quota.Global.DataSource = 4 - sc.m.Get("/ds", QuotaFn("data_source"), sc.defaultHandler) - sc.fakeReq("GET", "/ds").exec() - So(sc.resp.Code, ShouldEqual, 403) - }) + sc.m.Get("/user", quotaFn("session"), sc.defaultHandler) + sc.fakeReq("GET", "/user").exec() + assert.Equal(t, 200, sc.resp.Code) + }) - Convey("user Org quota not reached", func() { - setting.Quota.User.Org = 5 - sc.m.Get("/org", QuotaFn("org"), sc.defaultHandler) - sc.fakeReq("GET", "/org").exec() - So(sc.resp.Code, ShouldEqual, 200) + middlewareScenario(t, "and global session quota reached", func(t *testing.T, sc *scenarioContext) { + origSession := setting.Quota.Global.Session + t.Cleanup(func() { + setting.Quota.Global.Session = origSession }) + setting.Quota.Global.Session = 1 - Convey("user Org quota reached", func() { - setting.Quota.User.Org = 4 - sc.m.Get("/org", QuotaFn("org"), sc.defaultHandler) - sc.fakeReq("GET", "/org").exec() - So(sc.resp.Code, ShouldEqual, 403) - }) + sc.m.Get("/user", quotaFn("session"), sc.defaultHandler) + sc.fakeReq("GET", "/user").exec() + assert.Equal(t, 403, sc.resp.Code) + }) + }) - Convey("org dashboard quota not reached", func() { - setting.Quota.Org.Dashboard = 10 - sc.m.Get("/dashboard", QuotaFn("dashboard"), sc.defaultHandler) - sc.fakeReq("GET", "/dashboard").exec() - So(sc.resp.Code, ShouldEqual, 200) - }) + middlewareScenario(t, "with user logged in", func(t *testing.T, sc *scenarioContext) { + sc.withTokenSessionCookie("token") + bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error { + query.Result = &models.SignedInUser{OrgId: 2, UserId: 12} + return nil + }) - Convey("org dashboard quota reached", func() { - setting.Quota.Org.Dashboard = 4 - sc.m.Get("/dashboard", QuotaFn("dashboard"), sc.defaultHandler) - sc.fakeReq("GET", "/dashboard").exec() - So(sc.resp.Code, ShouldEqual, 403) - }) + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { + return &models.UserToken{ + UserId: 12, + UnhashedToken: "", + }, nil + } - Convey("org dashboard quota reached but quotas disabled", func() { - setting.Quota.Org.Dashboard = 4 - setting.Quota.Enabled = false - sc.m.Get("/dashboard", QuotaFn("dashboard"), sc.defaultHandler) - sc.fakeReq("GET", "/dashboard").exec() - So(sc.resp.Code, ShouldEqual, 200) - }) + bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error { + query.Result = &models.GlobalQuotaDTO{ + Target: query.Target, + Limit: query.Default, + Used: 4, + } + return nil + }) + + bus.AddHandler("userQuota", func(query *models.GetUserQuotaByTargetQuery) error { + query.Result = &models.UserQuotaDTO{ + Target: query.Target, + Limit: query.Default, + Used: 4, + } + return nil + }) + + bus.AddHandler("orgQuota", func(query *models.GetOrgQuotaByTargetQuery) error { + query.Result = &models.OrgQuotaDTO{ + Target: query.Target, + Limit: query.Default, + Used: 4, + } + return nil + }) + + t.Run("global datasource quota reached", func(t *testing.T) { + setting.Quota.Global.DataSource = 4 + sc.m.Get("/ds", quotaFn("data_source"), sc.defaultHandler) + sc.fakeReq("GET", "/ds").exec() + assert.Equal(t, 403, sc.resp.Code) + }) + + t.Run("user Org quota not reached", func(t *testing.T) { + setting.Quota.User.Org = 5 + sc.m.Get("/org", quotaFn("org"), sc.defaultHandler) + sc.fakeReq("GET", "/org").exec() + assert.Equal(t, 200, sc.resp.Code) + }) + + t.Run("user Org quota reached", func(t *testing.T) { + setting.Quota.User.Org = 4 + sc.m.Get("/org", quotaFn("org"), sc.defaultHandler) + sc.fakeReq("GET", "/org").exec() + assert.Equal(t, 403, sc.resp.Code) + }) + + t.Run("org dashboard quota not reached", func(t *testing.T) { + setting.Quota.Org.Dashboard = 10 + sc.m.Get("/dashboard", quotaFn("dashboard"), sc.defaultHandler) + sc.fakeReq("GET", "/dashboard").exec() + assert.Equal(t, 200, sc.resp.Code) + }) + + t.Run("org dashboard quota reached", func(t *testing.T) { + setting.Quota.Org.Dashboard = 4 + sc.m.Get("/dashboard", quotaFn("dashboard"), sc.defaultHandler) + sc.fakeReq("GET", "/dashboard").exec() + assert.Equal(t, 403, sc.resp.Code) + }) + + t.Run("org dashboard quota reached but quotas disabled", func(t *testing.T) { + setting.Quota.Org.Dashboard = 4 + setting.Quota.Enabled = false + sc.m.Get("/dashboard", quotaFn("dashboard"), sc.defaultHandler) + sc.fakeReq("GET", "/dashboard").exec() + assert.Equal(t, 200, sc.resp.Code) }) }) } diff --git a/pkg/middleware/recovery.go b/pkg/middleware/recovery.go index c238ecbfeee..8048cc6f53b 100644 --- a/pkg/middleware/recovery.go +++ b/pkg/middleware/recovery.go @@ -52,6 +52,9 @@ func stack(skip int) []byte { // Print this much at least. If we can't find the source, it won't show. fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc) if file != lastFile { + // We can ignore the gosec G304 warning on this one because `file` + // comes from the runtime.Caller() function. + // nolint:gosec data, err := ioutil.ReadFile(file) if err != nil { continue diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go index 03746daa2d4..5507ba5aac5 100644 --- a/pkg/middleware/recovery_test.go +++ b/pkg/middleware/recovery_test.go @@ -2,6 +2,7 @@ package middleware import ( "path/filepath" + "strings" "testing" "github.com/grafana/grafana/pkg/bus" @@ -9,52 +10,55 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/setting" - . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" macaron "gopkg.in/macaron.v1" ) func TestRecoveryMiddleware(t *testing.T) { setting.ErrTemplateName = "error-template" - Convey("Given an api route that panics", t, func() { + t.Run("Given an API route that panics", func(t *testing.T) { apiURL := "/api/whatever" - recoveryScenario(t, "recovery middleware should return json", apiURL, func(sc *scenarioContext) { - sc.handlerFunc = PanicHandler + recoveryScenario(t, "recovery middleware should return json", apiURL, func(t *testing.T, sc *scenarioContext) { + sc.handlerFunc = panicHandler sc.fakeReq("GET", apiURL).exec() sc.req.Header.Add("content-type", "application/json") - So(sc.resp.Code, ShouldEqual, 500) - So(sc.respJson["message"], ShouldStartWith, "Internal Server Error - Check the Grafana server logs for the detailed error message.") - So(sc.respJson["error"], ShouldStartWith, "Server Error") + assert.Equal(t, 500, sc.resp.Code) + assert.Equal(t, "Internal Server Error - Check the Grafana server logs for the detailed error message.", sc.respJson["message"]) + assert.True(t, strings.HasPrefix(sc.respJson["error"].(string), "Server Error")) }) }) - Convey("Given a non-api route that panics", t, func() { + t.Run("Given a non-API route that panics", func(t *testing.T) { apiURL := "/whatever" - recoveryScenario(t, "recovery middleware should return html", apiURL, func(sc *scenarioContext) { - sc.handlerFunc = PanicHandler + recoveryScenario(t, "recovery middleware should return html", apiURL, func(t *testing.T, sc *scenarioContext) { + sc.handlerFunc = panicHandler sc.fakeReq("GET", apiURL).exec() - So(sc.resp.Code, ShouldEqual, 500) - So(sc.resp.Header().Get("content-type"), ShouldEqual, "text/html; charset=UTF-8") - So(sc.resp.Body.String(), ShouldContainSubstring, "Grafana - Error") + assert.Equal(t, 500, sc.resp.Code) + assert.Equal(t, "text/html; charset=UTF-8", sc.resp.Header().Get("content-type")) + assert.True(t, strings.Contains(sc.resp.Body.String(), "Grafana - Error")) }) }) } -func PanicHandler(c *models.ReqContext) { +func panicHandler(c *models.ReqContext) { panic("Handler has panicked") } func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) { - Convey(desc, func() { + t.Run(desc, func(t *testing.T) { defer bus.ClearBusHandlers() sc := &scenarioContext{ + t: t, url: url, } - viewsPath, _ := filepath.Abs("../../public/views") + viewsPath, err := filepath.Abs("../../public/views") + require.NoError(t, err) sc.m = macaron.New() sc.m.Use(Recovery()) @@ -81,6 +85,6 @@ func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) { sc.m.Get(url, sc.defaultHandler) - fn(sc) + fn(t, sc) }) } diff --git a/pkg/middleware/testing.go b/pkg/middleware/testing.go index 0e24cc3e7cc..8229fb36116 100644 --- a/pkg/middleware/testing.go +++ b/pkg/middleware/testing.go @@ -4,6 +4,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "testing" "gopkg.in/macaron.v1" @@ -11,10 +12,11 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/setting" - "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/require" ) type scenarioContext struct { + t *testing.T m *macaron.Macaron context *models.ReqContext resp *httptest.ResponseRecorder @@ -47,15 +49,19 @@ func (sc *scenarioContext) withAuthorizationHeader(authHeader string) *scenarioC } func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext { + sc.t.Helper() + sc.resp = httptest.NewRecorder() req, err := http.NewRequest(method, url, nil) - convey.So(err, convey.ShouldBeNil) + require.NoError(sc.t, err) sc.req = req return sc } func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map[string]string) *scenarioContext { + sc.t.Helper() + sc.resp = httptest.NewRecorder() req, err := http.NewRequest(method, url, nil) q := req.URL.Query() @@ -63,7 +69,7 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map q.Add(k, v) } req.URL.RawQuery = q.Encode() - convey.So(err, convey.ShouldBeNil) + require.NoError(sc.t, err) sc.req = req return sc @@ -75,15 +81,20 @@ func (sc *scenarioContext) handler(fn handlerFunc) *scenarioContext { } func (sc *scenarioContext) exec() { + sc.t.Helper() + if sc.apiKey != "" { + sc.t.Logf(`Adding header "Authorization: Bearer %s"`, sc.apiKey) sc.req.Header.Add("Authorization", "Bearer "+sc.apiKey) } if sc.authHeader != "" { + sc.t.Logf(`Adding header "Authorization: %s"`, sc.authHeader) sc.req.Header.Add("Authorization", sc.authHeader) } if sc.tokenSessionCookie != "" { + sc.t.Log(`Adding cookie`, "name", setting.LoginCookieName, "value", sc.tokenSessionCookie) sc.req.AddCookie(&http.Cookie{ Name: setting.LoginCookieName, Value: sc.tokenSessionCookie, @@ -94,9 +105,9 @@ func (sc *scenarioContext) exec() { if sc.resp.Header().Get("Content-Type") == "application/json; charset=UTF-8" { err := json.NewDecoder(sc.resp.Body).Decode(&sc.respJson) - convey.So(err, convey.ShouldBeNil) + require.NoError(sc.t, err) } } -type scenarioFunc func(c *scenarioContext) +type scenarioFunc func(t *testing.T, c *scenarioContext) type handlerFunc func(c *models.ReqContext) diff --git a/pkg/plugins/dashboards.go b/pkg/plugins/dashboards.go index 30396bf2004..6cd9f827449 100644 --- a/pkg/plugins/dashboards.go +++ b/pkg/plugins/dashboards.go @@ -91,18 +91,25 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT func loadPluginDashboard(pluginId, path string) (*models.Dashboard, error) { plugin, exists := Plugins[pluginId] - if !exists { return nil, PluginNotFoundError{pluginId} } + // nolint:gosec + // We can ignore the gosec G304 warning on this one because `plugin.PluginDir` is based + // on plugin folder structure on disk and not user input. `path` comes from the + // `plugin.json` configuration file for the loaded plugin dashboardFilePath := filepath.Join(plugin.PluginDir, path) reader, err := os.Open(dashboardFilePath) if err != nil { return nil, err } - defer reader.Close() + defer func() { + if err := reader.Close(); err != nil { + plog.Warn("Failed to close file", "path", dashboardFilePath, "err", err) + } + }() data, err := simplejson.NewFromReader(reader) if err != nil { diff --git a/pkg/plugins/manifest.go b/pkg/plugins/manifest.go index 9b6e2cb00c4..4eaf721bad6 100644 --- a/pkg/plugins/manifest.go +++ b/pkg/plugins/manifest.go @@ -87,6 +87,9 @@ func getPluginSignatureState(log log.Logger, plugin *PluginBase) PluginSignature log.Debug("Getting signature state of plugin", "plugin", plugin.Id, "isBackend", plugin.Backend) manifestPath := filepath.Join(plugin.PluginDir, "MANIFEST.txt") + // nolint:gosec + // We can ignore the gosec G304 warning on this one because `manifestPath` is based + // on plugin the folder structure on disk and not user input. byteValue, err := ioutil.ReadFile(manifestPath) if err != nil || len(byteValue) < 10 { log.Debug("Plugin is unsigned", "id", plugin.Id) @@ -109,11 +112,19 @@ func getPluginSignatureState(log log.Logger, plugin *PluginBase) PluginSignature for p, hash := range manifest.Files { // Open the file fp := filepath.Join(plugin.PluginDir, p) + + // nolint:gosec + // We can ignore the gosec G304 warning on this one because `fp` is based + // on the manifest file for a plugin and not user input. f, err := os.Open(fp) if err != nil { return PluginSignatureModified } - defer f.Close() + defer func() { + if err := f.Close(); err != nil { + log.Warn("Failed to close plugin file", "path", fp, "err", err) + } + }() h := sha256.New() if _, err := io.Copy(h, f); err != nil { diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 0818a8dde05..754a5eab16e 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -269,11 +269,18 @@ func (pm *PluginManager) scan(pluginDir string, requireSigned bool) error { } } + // nolint:gosec + // We can ignore the gosec G304 warning on this one because `jsonFPath` is based + // on plugin the folder structure on disk and not user input. reader, err := os.Open(jsonFPath) if err != nil { return err } - defer reader.Close() + defer func() { + if err := reader.Close(); err != nil { + scanner.log.Warn("Failed to close JSON file", "path", jsonFPath, "err", err) + } + }() jsonParser := json.NewDecoder(reader) @@ -328,6 +335,9 @@ func (s *PluginScanner) walker(currentPath string, f os.FileInfo, err error) err return nil } + // nolint:gosec + // We can ignore the gosec G304 warning on this one because `currentPath` is based + // on plugin the folder structure on disk and not user input. if err := s.loadPlugin(currentPath); err != nil { s.log.Error("Failed to load plugin", "error", err, "pluginPath", filepath.Dir(currentPath)) s.errors = append(s.errors, err) @@ -343,7 +353,11 @@ func (s *PluginScanner) loadPlugin(pluginJSONFilePath string) error { if err != nil { return err } - defer reader.Close() + defer func() { + if err := reader.Close(); err != nil { + s.log.Warn("Failed to close JSON file", "path", pluginJSONFilePath, "err", err) + } + }() jsonParser := json.NewDecoder(reader) pluginCommon := PluginBase{} @@ -463,6 +477,9 @@ func GetPluginMarkdown(pluginId string, name string) ([]byte, error) { return nil, PluginNotFoundError{pluginId} } + // nolint:gosec + // We can ignore the gosec G304 warning on this one because `plug.PluginDir` is based + // on plugin the folder structure on disk and not user input. path := filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToUpper(name))) exists, err := fs.Exists(path) if err != nil { @@ -480,6 +497,9 @@ func GetPluginMarkdown(pluginId string, name string) ([]byte, error) { return make([]byte, 0), nil } + // nolint:gosec + // We can ignore the gosec G304 warning on this one because `plug.PluginDir` is based + // on plugin the folder structure on disk and not user input. data, err := ioutil.ReadFile(path) if err != nil { return nil, err diff --git a/pkg/services/alerting/notifier_test.go b/pkg/services/alerting/notifier_test.go index 0b2f2b779bc..fcb49e3abc0 100644 --- a/pkg/services/alerting/notifier_test.go +++ b/pkg/services/alerting/notifier_test.go @@ -30,94 +30,102 @@ func TestNotificationService(t *testing.T) { } evalCtx := NewEvalContext(context.Background(), testRule) - notificationServiceScenario(t, "Given alert rule with upload image enabled should render and upload image and send notification", evalCtx, true, func(scenarioCtx *scenarioContext) { - err := scenarioCtx.notificationService.SendIfNeeded(evalCtx) - require.NoError(t, err) + notificationServiceScenario(t, "Given alert rule with upload image enabled should render and upload image and send notification", + evalCtx, true, func(sc *scenarioContext) { + err := sc.notificationService.SendIfNeeded(evalCtx) + require.NoError(sc.t, err) - require.Equalf(t, 1, scenarioCtx.renderCount, "expected render to be called, but wasn't") - require.Equalf(t, 1, scenarioCtx.imageUploadCount, "expected image to be uploaded, but wasn't") - require.Truef(t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't") - }) + require.Equalf(sc.t, 1, sc.renderCount, "expected render to be called, but wasn't") + require.Equalf(sc.t, 1, sc.imageUploadCount, "expected image to be uploaded, but wasn't") + require.Truef(sc.t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't") + }) - notificationServiceScenario(t, "Given alert rule with upload image enabled but no renderer available should render and upload unavailable image and send notification", evalCtx, true, func(scenarioCtx *scenarioContext) { - scenarioCtx.rendererAvailable = false - err := scenarioCtx.notificationService.SendIfNeeded(evalCtx) - require.NoError(t, err) + notificationServiceScenario(t, + "Given alert rule with upload image enabled but no renderer available should render and upload unavailable image and send notification", + evalCtx, true, func(sc *scenarioContext) { + sc.rendererAvailable = false + err := sc.notificationService.SendIfNeeded(evalCtx) + require.NoError(sc.t, err) - require.Equalf(t, 1, scenarioCtx.renderCount, "expected render to be called, but it wasn't") - require.Equalf(t, 1, scenarioCtx.imageUploadCount, "expected image to be uploaded, but it wasn't") - require.Truef(t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't") - }) + require.Equalf(sc.t, 1, sc.renderCount, "expected render to be called, but it wasn't") + require.Equalf(sc.t, 1, sc.imageUploadCount, "expected image to be uploaded, but it wasn't") + require.Truef(sc.t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't") + }) - notificationServiceScenario(t, "Given alert rule with upload image disabled should not render and upload image, but send notification", evalCtx, false, func(scenarioCtx *scenarioContext) { - err := scenarioCtx.notificationService.SendIfNeeded(evalCtx) - require.NoError(t, err) + notificationServiceScenario( + t, "Given alert rule with upload image disabled should not render and upload image, but send notification", + evalCtx, false, func(sc *scenarioContext) { + err := sc.notificationService.SendIfNeeded(evalCtx) + require.NoError(t, err) - require.Equalf(t, 0, scenarioCtx.renderCount, "expected render not to be called, but it was") - require.Equalf(t, 0, scenarioCtx.imageUploadCount, "expected image not to be uploaded, but it was") - require.Truef(t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't") - }) + require.Equalf(sc.t, 0, sc.renderCount, "expected render not to be called, but it was") + require.Equalf(sc.t, 0, sc.imageUploadCount, "expected image not to be uploaded, but it was") + require.Truef(sc.t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't") + }) - notificationServiceScenario(t, "Given alert rule with upload image enabled and render times out should send notification", evalCtx, true, func(scenarioCtx *scenarioContext) { - setting.AlertingNotificationTimeout = 200 * time.Millisecond - scenarioCtx.renderProvider = func(ctx context.Context, opts rendering.Opts) (*rendering.RenderResult, error) { - wait := make(chan bool) + notificationServiceScenario(t, "Given alert rule with upload image enabled and render times out should send notification", + evalCtx, true, func(sc *scenarioContext) { + setting.AlertingNotificationTimeout = 200 * time.Millisecond + sc.renderProvider = func(ctx context.Context, opts rendering.Opts) (*rendering.RenderResult, error) { + wait := make(chan bool) - go func() { - time.Sleep(1 * time.Second) - wait <- true - }() + go func() { + time.Sleep(1 * time.Second) + wait <- true + }() - select { - case <-ctx.Done(): - if err := ctx.Err(); err != nil { - return nil, err + select { + case <-ctx.Done(): + if err := ctx.Err(); err != nil { + return nil, err + } + break + case <-wait: } - break - case <-wait: + + return nil, nil } + err := sc.notificationService.SendIfNeeded(evalCtx) + require.NoError(sc.t, err) - return nil, nil - } - err := scenarioCtx.notificationService.SendIfNeeded(evalCtx) - require.NoError(t, err) + require.Equalf(sc.t, 0, sc.renderCount, "expected render not to be called, but it was") + require.Equalf(sc.t, 0, sc.imageUploadCount, "expected image not to be uploaded, but it was") + require.Truef(sc.t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't") + }) - require.Equalf(t, 0, scenarioCtx.renderCount, "expected render not to be called, but it was") - require.Equalf(t, 0, scenarioCtx.imageUploadCount, "expected image not to be uploaded, but it was") - require.Truef(t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't") - }) + notificationServiceScenario(t, "Given alert rule with upload image enabled and upload times out should send notification", + evalCtx, true, func(sc *scenarioContext) { + setting.AlertingNotificationTimeout = 200 * time.Millisecond + sc.uploadProvider = func(ctx context.Context, path string) (string, error) { + wait := make(chan bool) - notificationServiceScenario(t, "Given alert rule with upload image enabled and upload times out should send notification", evalCtx, true, func(scenarioCtx *scenarioContext) { - setting.AlertingNotificationTimeout = 200 * time.Millisecond - scenarioCtx.uploadProvider = func(ctx context.Context, path string) (string, error) { - wait := make(chan bool) + go func() { + time.Sleep(1 * time.Second) + wait <- true + }() - go func() { - time.Sleep(1 * time.Second) - wait <- true - }() - - select { - case <-ctx.Done(): - if err := ctx.Err(); err != nil { - return "", err + select { + case <-ctx.Done(): + if err := ctx.Err(); err != nil { + return "", err + } + break + case <-wait: } - break - case <-wait: + + return "", nil } + err := sc.notificationService.SendIfNeeded(evalCtx) + require.NoError(sc.t, err) - return "", nil - } - err := scenarioCtx.notificationService.SendIfNeeded(evalCtx) - require.NoError(t, err) - - require.Equalf(t, 1, scenarioCtx.renderCount, "expected render to be called, but wasn't") - require.Equalf(t, 0, scenarioCtx.imageUploadCount, "expected image not to be uploaded, but it was") - require.Truef(t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't") - }) + require.Equalf(sc.t, 1, sc.renderCount, "expected render to be called, but wasn't") + require.Equalf(sc.t, 0, sc.imageUploadCount, "expected image not to be uploaded, but it was") + require.Truef(sc.t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't") + }) } type scenarioContext struct { + t *testing.T evalCtx *EvalContext notificationService *notificationService imageUploadCount int @@ -175,6 +183,7 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext setting.AlertingNotificationTimeout = 30 * time.Second scenarioCtx := &scenarioContext{ + t: t, evalCtx: evalCtx, } diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go index 3801b91b286..5bbc5039b93 100644 --- a/pkg/services/alerting/notifiers/discord.go +++ b/pkg/services/alerting/notifiers/discord.go @@ -2,6 +2,7 @@ package notifiers import ( "bytes" + "fmt" "io" "mime/multipart" "os" @@ -158,6 +159,9 @@ func (dn *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error { } func (dn *DiscordNotifier) embedImage(cmd *models.SendWebhookSync, imagePath string, existingJSONBody []byte) error { + // nolint:gosec + // We can ignore the gosec G304 warning on this one because `imagePath` comes + // from the alert `evalContext` that generates the images. f, err := os.Open(imagePath) if err != nil { if os.IsNotExist(err) { @@ -168,12 +172,20 @@ func (dn *DiscordNotifier) embedImage(cmd *models.SendWebhookSync, imagePath str return err } } - - defer f.Close() + defer func() { + if err := f.Close(); err != nil { + dn.log.Warn("Failed to close file", "path", imagePath, "err", err) + } + }() var b bytes.Buffer w := multipart.NewWriter(&b) - + defer func() { + if err := w.Close(); err != nil { + // Should be OK since we already close it on non-error path + dn.log.Warn("Failed to close multipart writer", "err", err) + } + }() fw, err := w.CreateFormField("payload_json") if err != nil { return err @@ -192,7 +204,9 @@ func (dn *DiscordNotifier) embedImage(cmd *models.SendWebhookSync, imagePath str return err } - w.Close() + if err := w.Close(); err != nil { + return fmt.Errorf("failed to close multipart writer: %w", err) + } cmd.Body = b.String() cmd.ContentType = w.FormDataContentType() diff --git a/pkg/services/alerting/notifiers/pushover.go b/pkg/services/alerting/notifiers/pushover.go index 79cc97520cc..c13270205bc 100644 --- a/pkg/services/alerting/notifiers/pushover.go +++ b/pkg/services/alerting/notifiers/pushover.go @@ -292,7 +292,11 @@ func (pn *PushoverNotifier) genPushoverBody(evalContext *alerting.EvalContext, m if err != nil { return nil, b, err } - defer f.Close() + defer func() { + if err := f.Close(); err != nil { + pn.log.Warn("Failed to close file", "path", evalContext.ImageOnDiskPath, "err", err) + } + }() fw, err := w.CreateFormFile("attachment", evalContext.ImageOnDiskPath) if err != nil { diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index 3ec33a30ffa..cfd4c053cc6 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -321,7 +321,7 @@ func (sn *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { return err } if sn.Token != "" && sn.UploadImage { - err = slackFileUpload(evalContext, sn.log, "https://slack.com/api/files.upload", sn.Recipient, sn.Token) + err = sn.slackFileUpload(evalContext, sn.log, "https://slack.com/api/files.upload", sn.Recipient, sn.Token) if err != nil { return err } @@ -329,12 +329,14 @@ func (sn *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { return nil } -func slackFileUpload(evalContext *alerting.EvalContext, log log.Logger, url string, recipient string, token string) error { +func (sn *SlackNotifier) slackFileUpload(evalContext *alerting.EvalContext, log log.Logger, url string, recipient string, token string) error { if evalContext.ImageOnDiskPath == "" { + // nolint:gosec + // We can ignore the gosec G304 warning on this one because `setting.HomePath` comes from Grafana's configuration file. evalContext.ImageOnDiskPath = filepath.Join(setting.HomePath, "public/img/mixed_styles.png") } log.Info("Uploading to slack via file.upload API") - headers, uploadBody, err := generateSlackBody(evalContext.ImageOnDiskPath, token, recipient) + headers, uploadBody, err := sn.generateSlackBody(evalContext.ImageOnDiskPath, token, recipient) if err != nil { return err } @@ -346,37 +348,51 @@ func slackFileUpload(evalContext *alerting.EvalContext, log log.Logger, url stri return nil } -func generateSlackBody(file string, token string, recipient string) (map[string]string, bytes.Buffer, error) { +func (sn *SlackNotifier) generateSlackBody(path string, token string, recipient string) (map[string]string, bytes.Buffer, error) { // Slack requires all POSTs to files.upload to present // an "application/x-www-form-urlencoded" encoded querystring // See https://api.slack.com/methods/files.upload var b bytes.Buffer w := multipart.NewWriter(&b) + defer func() { + if err := w.Close(); err != nil { + // Shouldn't matter since we already close w explicitly on the non-error path + sn.log.Warn("Failed to close multipart writer", "err", err) + } + }() + // Add the generated image file - f, err := os.Open(file) + // We can ignore the gosec G304 warning on this one because `imagePath` comes + // from the alert `evalContext` that generates the images. `evalContext` in turn derives the root of the file + // path from configuration variables. + // nolint:gosec + f, err := os.Open(path) if err != nil { return nil, b, err } - defer f.Close() - fw, err := w.CreateFormFile("file", file) + defer func() { + if err := f.Close(); err != nil { + sn.log.Warn("Failed to close file", "path", path, "err", err) + } + }() + fw, err := w.CreateFormFile("file", path) if err != nil { return nil, b, err } - _, err = io.Copy(fw, f) - if err != nil { + if _, err := io.Copy(fw, f); err != nil { return nil, b, err } // Add the authorization token - err = w.WriteField("token", token) - if err != nil { + if err := w.WriteField("token", token); err != nil { return nil, b, err } // Add the channel(s) to POST to - err = w.WriteField("channels", recipient) - if err != nil { + if err := w.WriteField("channels", recipient); err != nil { return nil, b, err } - w.Close() + if err := w.Close(); err != nil { + return nil, b, fmt.Errorf("failed to close multipart writer: %w", err) + } headers := map[string]string{ "Content-Type": w.FormDataContentType(), "Authorization": "auth_token=\"" + token + "\"", diff --git a/pkg/services/ldap/ldap.go b/pkg/services/ldap/ldap.go index 1d9b02d786e..c7084839b04 100644 --- a/pkg/services/ldap/ldap.go +++ b/pkg/services/ldap/ldap.go @@ -95,6 +95,8 @@ func (server *Server) Dial() error { if server.Config.RootCACert != "" { certPool = x509.NewCertPool() for _, caCertFile := range strings.Split(server.Config.RootCACert, " ") { + // nolint:gosec + // We can ignore the gosec G304 warning on this one because `caCertFile` comes from ldap config. pem, err := ioutil.ReadFile(caCertFile) if err != nil { return err diff --git a/pkg/services/ldap/settings.go b/pkg/services/ldap/settings.go index fa4248108e3..93e94f3e016 100644 --- a/pkg/services/ldap/settings.go +++ b/pkg/services/ldap/settings.go @@ -115,6 +115,8 @@ func readConfig(configFile string) (*Config, error) { logger.Info("LDAP enabled, reading config file", "file", configFile) + // nolint:gosec + // We can ignore the gosec G304 warning on this one because `filename` comes from grafana configuration file fileBytes, err := ioutil.ReadFile(configFile) if err != nil { return nil, errutil.Wrap("Failed to load LDAP config file", err) diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go index df2d6c7bb39..92eb9a10eae 100644 --- a/pkg/services/provisioning/dashboards/config_reader.go +++ b/pkg/services/provisioning/dashboards/config_reader.go @@ -19,6 +19,9 @@ type configReader struct { func (cr *configReader) parseConfigs(file os.FileInfo) ([]*config, error) { filename, _ := filepath.Abs(filepath.Join(cr.path, file.Name())) + + // nolint:gosec + // We can ignore the gosec G304 warning on this one because `filename` comes from ps.Cfg.ProvisioningPath yamlFile, err := ioutil.ReadFile(filename) if err != nil { return nil, err diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 709dba0bb3f..576b62d4cf0 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -343,11 +343,17 @@ type dashboardJSONFile struct { } func (fr *FileReader) readDashboardFromFile(path string, lastModified time.Time, folderID int64) (*dashboardJSONFile, error) { + // nolint:gosec + // We can ignore the gosec G304 warning on this one because `path` comes from the provisioning configuration file. reader, err := os.Open(path) if err != nil { return nil, err } - defer reader.Close() + defer func() { + if err := reader.Close(); err != nil { + fr.log.Warn("Failed to close file", "path", path, "err", err) + } + }() all, err := ioutil.ReadAll(reader) if err != nil { diff --git a/pkg/services/provisioning/datasources/config_reader.go b/pkg/services/provisioning/datasources/config_reader.go index 52a5aa430b8..191ab962342 100644 --- a/pkg/services/provisioning/datasources/config_reader.go +++ b/pkg/services/provisioning/datasources/config_reader.go @@ -49,6 +49,9 @@ func (cr *configReader) readConfig(path string) ([]*configs, error) { func (cr *configReader) parseDatasourceConfig(path string, file os.FileInfo) (*configs, error) { filename, _ := filepath.Abs(filepath.Join(path, file.Name())) + + // nolint:gosec + // We can ignore the gosec G304 warning on this one because `filename` comes from ps.Cfg.ProvisioningPath yamlFile, err := ioutil.ReadFile(filename) if err != nil { return nil, err diff --git a/pkg/services/provisioning/notifiers/config_reader.go b/pkg/services/provisioning/notifiers/config_reader.go index c2d40270bb4..17886adba36 100644 --- a/pkg/services/provisioning/notifiers/config_reader.go +++ b/pkg/services/provisioning/notifiers/config_reader.go @@ -61,6 +61,9 @@ func (cr *configReader) readConfig(path string) ([]*notificationsAsConfig, error func (cr *configReader) parseNotificationConfig(path string, file os.FileInfo) (*notificationsAsConfig, error) { filename, _ := filepath.Abs(filepath.Join(path, file.Name())) + + // nolint:gosec + // We can ignore the gosec G304 warning on this one because `filename` comes from ps.Cfg.ProvisioningPath yamlFile, err := ioutil.ReadFile(filename) if err != nil { return nil, err diff --git a/pkg/services/provisioning/plugins/config_reader.go b/pkg/services/provisioning/plugins/config_reader.go index ec95c40cf78..fe5175cf066 100644 --- a/pkg/services/provisioning/plugins/config_reader.go +++ b/pkg/services/provisioning/plugins/config_reader.go @@ -69,6 +69,8 @@ func (cr *configReaderImpl) parsePluginConfig(path string, file os.FileInfo) (*p return nil, err } + // nolint:gosec + // We can ignore the gosec G304 warning on this one because `filename` comes from ps.Cfg.ProvisioningPath yamlFile, err := ioutil.ReadFile(filename) if err != nil { return nil, err diff --git a/pkg/services/rendering/http_mode.go b/pkg/services/rendering/http_mode.go index 23bb912a75f..7cc30c891a6 100644 --- a/pkg/services/rendering/http_mode.go +++ b/pkg/services/rendering/http_mode.go @@ -96,7 +96,12 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, renderKey string, if err != nil { return nil, err } - defer out.Close() + defer func() { + if err := out.Close(); err != nil { + // We already close the file explicitly in the non-error path, so shouldn't be a problem + rs.log.Warn("Failed to close file", "path", filePath, "err", err) + } + }() _, err = io.Copy(out, resp.Body) if err != nil { // check that we didn't timeout while receiving the response. @@ -107,6 +112,9 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, renderKey string, rs.log.Error("Remote rendering request failed", "error", err) return nil, fmt.Errorf("remote rendering request failed: %w", err) } + if err := out.Close(); err != nil { + return nil, fmt.Errorf("failed to write to %q: %w", filePath, err) + } return &RenderResult{FilePath: filePath}, err } diff --git a/pkg/services/sqlstore/migrations/migrations_test.go b/pkg/services/sqlstore/migrations/migrations_test.go index 19b57ff93f0..0bf54752876 100644 --- a/pkg/services/sqlstore/migrations/migrations_test.go +++ b/pkg/services/sqlstore/migrations/migrations_test.go @@ -5,56 +5,45 @@ import ( . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" + "github.com/stretchr/testify/require" "xorm.io/xorm" - - . "github.com/smartystreets/goconvey/convey" ) func TestMigrations(t *testing.T) { - testDBs := []sqlutil.TestDB{ - sqlutil.SQLite3TestDB(), - } + testDB := sqlutil.SQLite3TestDB() + const query = `select count(*) as count from migration_log` + result := struct{ Count int }{} - for _, testDB := range testDBs { - sql := `select count(*) as count from migration_log` - r := struct { - Count int64 - }{} + x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr) + require.NoError(t, err) - Convey("Initial "+testDB.DriverName+" migration", t, func() { - x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr) - So(err, ShouldBeNil) + err = NewDialect(x).CleanDB() + require.NoError(t, err) - err = NewDialect(x).CleanDB() - So(err, ShouldBeNil) + _, err = x.SQL(query).Get(&result) + require.Error(t, err) - _, err = x.SQL(sql).Get(&r) - So(err, ShouldNotBeNil) + mg := NewMigrator(x) + AddMigrations(mg) + expectedMigrations := mg.MigrationsCount() - mg := NewMigrator(x) - AddMigrations(mg) + err = mg.Start() + require.NoError(t, err) - err = mg.Start() - So(err, ShouldBeNil) + has, err := x.SQL(query).Get(&result) + require.NoError(t, err) + require.True(t, has) - has, err := x.SQL(sql).Get(&r) - So(err, ShouldBeNil) - So(has, ShouldBeTrue) - // we currently skip to migrations. We should rewrite skipped migrations to write in the log as well. - // until then we have to keep this - expectedMigrations := mg.MigrationsCount() - So(r.Count, ShouldEqual, expectedMigrations) + require.Equal(t, expectedMigrations, result.Count) - mg = NewMigrator(x) - AddMigrations(mg) + mg = NewMigrator(x) + AddMigrations(mg) - err = mg.Start() - So(err, ShouldBeNil) + err = mg.Start() + require.NoError(t, err) - has, err = x.SQL(sql).Get(&r) - So(err, ShouldBeNil) - So(has, ShouldBeTrue) - So(r.Count, ShouldEqual, expectedMigrations) - }) - } + has, err = x.SQL(query).Get(&result) + require.NoError(t, err) + require.True(t, has) + require.Equal(t, expectedMigrations, result.Count) } diff --git a/pkg/setting/expanders.go b/pkg/setting/expanders.go index 021722ba998..c6f9a61f582 100644 --- a/pkg/setting/expanders.go +++ b/pkg/setting/expanders.go @@ -138,6 +138,8 @@ func (e fileExpander) Expand(s string) (string, error) { return "", err } + // nolint:gosec + // We can ignore the gosec G304 warning on this one because `s` comes from configuration section keys f, err := ioutil.ReadFile(s) if err != nil { return "", err diff --git a/pkg/tsdb/prometheus/prometheus_test.go b/pkg/tsdb/prometheus/prometheus_test.go index 0eec856be0b..43338258781 100644 --- a/pkg/tsdb/prometheus/prometheus_test.go +++ b/pkg/tsdb/prometheus/prometheus_test.go @@ -4,156 +4,125 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/tsdb" - - "github.com/grafana/grafana/pkg/components/simplejson" p "github.com/prometheus/common/model" - . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/require" ) func TestPrometheus(t *testing.T) { - Convey("Prometheus", t, func() { - dsInfo := &models.DataSource{ - JsonData: simplejson.New(), + dsInfo := &models.DataSource{ + JsonData: simplejson.New(), + } + + t.Run("converting metric name", func(t *testing.T) { + metric := map[p.LabelName]p.LabelValue{ + p.LabelName("app"): p.LabelValue("backend"), + p.LabelName("device"): p.LabelValue("mobile"), } - Convey("converting metric name", func() { - metric := map[p.LabelName]p.LabelValue{ - p.LabelName("app"): p.LabelValue("backend"), - p.LabelName("device"): p.LabelValue("mobile"), - } + query := &PrometheusQuery{ + LegendFormat: "legend {{app}} {{ device }} {{broken}}", + } - query := &PrometheusQuery{ - LegendFormat: "legend {{app}} {{ device }} {{broken}}", - } + require.Equal(t, "legend backend mobile ", formatLegend(metric, query)) + }) - So(formatLegend(metric, query), ShouldEqual, "legend backend mobile ") - }) + t.Run("build full series name", func(t *testing.T) { + metric := map[p.LabelName]p.LabelValue{ + p.LabelName(p.MetricNameLabel): p.LabelValue("http_request_total"), + p.LabelName("app"): p.LabelValue("backend"), + p.LabelName("device"): p.LabelValue("mobile"), + } - Convey("build full series name", func() { - metric := map[p.LabelName]p.LabelValue{ - p.LabelName(p.MetricNameLabel): p.LabelValue("http_request_total"), - p.LabelName("app"): p.LabelValue("backend"), - p.LabelName("device"): p.LabelValue("mobile"), - } + query := &PrometheusQuery{ + LegendFormat: "", + } - query := &PrometheusQuery{ - LegendFormat: "", - } + require.Equal(t, `http_request_total{app="backend", device="mobile"}`, formatLegend(metric, query)) + }) - So(formatLegend(metric, query), ShouldEqual, `http_request_total{app="backend", device="mobile"}`) - }) - - Convey("parsing query model with step", func() { - json := `{ + t.Run("parsing query model with step", func(t *testing.T) { + json := `{ "expr": "go_goroutines", "format": "time_series", "refId": "A" }` - jsonModel, _ := simplejson.NewJson([]byte(json)) - queryContext := &tsdb.TsdbQuery{} - queryModels := []*tsdb.Query{ - {Model: jsonModel}, - } + jsonModel, _ := simplejson.NewJson([]byte(json)) + queryContext := &tsdb.TsdbQuery{} + queryModels := []*tsdb.Query{ + {Model: jsonModel}, + } - Convey("with 48h time range", func() { - queryContext.TimeRange = tsdb.NewTimeRange("12h", "now") + queryContext.TimeRange = tsdb.NewTimeRange("12h", "now") - models, err := parseQuery(dsInfo, queryModels, queryContext) - So(err, ShouldBeNil) + models, err := parseQuery(dsInfo, queryModels, queryContext) + require.NoError(t, err) + require.Equal(t, time.Second*30, models[0].Step) + }) - model := models[0] - So(model.Step, ShouldEqual, time.Second*30) - }) - }) - - Convey("parsing query model without step parameter", func() { - json := `{ + t.Run("parsing query model without step parameter", func(t *testing.T) { + json := `{ "expr": "go_goroutines", "format": "time_series", "intervalFactor": 1, "refId": "A" }` - jsonModel, _ := simplejson.NewJson([]byte(json)) - queryContext := &tsdb.TsdbQuery{} - queryModels := []*tsdb.Query{ - {Model: jsonModel}, - } + jsonModel, _ := simplejson.NewJson([]byte(json)) + queryContext := &tsdb.TsdbQuery{} + queryModels := []*tsdb.Query{ + {Model: jsonModel}, + } - Convey("with 48h time range", func() { - queryContext.TimeRange = tsdb.NewTimeRange("48h", "now") + queryContext.TimeRange = tsdb.NewTimeRange("48h", "now") + models, err := parseQuery(dsInfo, queryModels, queryContext) + require.NoError(t, err) + require.Equal(t, time.Minute*2, models[0].Step) - models, err := parseQuery(dsInfo, queryModels, queryContext) + queryContext.TimeRange = tsdb.NewTimeRange("1h", "now") + models, err = parseQuery(dsInfo, queryModels, queryContext) + require.NoError(t, err) + require.Equal(t, time.Second*15, models[0].Step) + }) - So(err, ShouldBeNil) - - model := models[0] - So(model.Step, ShouldEqual, time.Minute*2) - }) - - Convey("with 1h time range", func() { - queryContext.TimeRange = tsdb.NewTimeRange("1h", "now") - - models, err := parseQuery(dsInfo, queryModels, queryContext) - - So(err, ShouldBeNil) - - model := models[0] - So(model.Step, ShouldEqual, time.Second*15) - }) - }) - - Convey("parsing query model with intervalFactor", func() { - Convey("high intervalFactor", func() { - json := `{ + t.Run("parsing query model with high intervalFactor", func(t *testing.T) { + json := `{ "expr": "go_goroutines", "format": "time_series", "intervalFactor": 10, "refId": "A" }` - jsonModel, _ := simplejson.NewJson([]byte(json)) - queryContext := &tsdb.TsdbQuery{} - queryModels := []*tsdb.Query{ - {Model: jsonModel}, - } + jsonModel, _ := simplejson.NewJson([]byte(json)) + queryContext := &tsdb.TsdbQuery{} + queryModels := []*tsdb.Query{ + {Model: jsonModel}, + } - Convey("with 48h time range", func() { - queryContext.TimeRange = tsdb.NewTimeRange("48h", "now") + queryContext.TimeRange = tsdb.NewTimeRange("48h", "now") - models, err := parseQuery(dsInfo, queryModels, queryContext) + models, err := parseQuery(dsInfo, queryModels, queryContext) + require.NoError(t, err) + require.Equal(t, time.Minute*20, models[0].Step) + }) - So(err, ShouldBeNil) - - model := models[0] - So(model.Step, ShouldEqual, time.Minute*20) - }) - }) - - Convey("low intervalFactor", func() { - json := `{ + t.Run("parsing query model with low intervalFactor", func(t *testing.T) { + json := `{ "expr": "go_goroutines", "format": "time_series", "intervalFactor": 1, "refId": "A" }` - jsonModel, _ := simplejson.NewJson([]byte(json)) - queryContext := &tsdb.TsdbQuery{} - queryModels := []*tsdb.Query{ - {Model: jsonModel}, - } + jsonModel, _ := simplejson.NewJson([]byte(json)) + queryContext := &tsdb.TsdbQuery{} + queryModels := []*tsdb.Query{ + {Model: jsonModel}, + } - Convey("with 48h time range", func() { - queryContext.TimeRange = tsdb.NewTimeRange("48h", "now") + queryContext.TimeRange = tsdb.NewTimeRange("48h", "now") - models, err := parseQuery(dsInfo, queryModels, queryContext) - - So(err, ShouldBeNil) - - model := models[0] - So(model.Step, ShouldEqual, time.Minute*2) - }) - }) - }) + models, err := parseQuery(dsInfo, queryModels, queryContext) + require.NoError(t, err) + require.Equal(t, time.Minute*2, models[0].Step) }) } diff --git a/pkg/tsdb/testdatasource/scenarios_test.go b/pkg/tsdb/testdatasource/scenarios_test.go index 559fd058490..9c6cd35604d 100644 --- a/pkg/tsdb/testdatasource/scenarios_test.go +++ b/pkg/tsdb/testdatasource/scenarios_test.go @@ -6,14 +6,14 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/tsdb" - . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/require" ) func TestTestdataScenarios(t *testing.T) { - Convey("random walk ", t, func() { + t.Run("random walk ", func(t *testing.T) { scenario := ScenarioRegistry["random_walk"] - Convey("Should start at the requested value", func() { + t.Run("Should start at the requested value", func(t *testing.T) { req := &tsdb.TsdbQuery{ TimeRange: tsdb.NewFakeTimeRange("5m", "now", time.Now()), Queries: []*tsdb.Query{ @@ -24,17 +24,17 @@ func TestTestdataScenarios(t *testing.T) { query.Model.Set("startValue", 1.234) result := scenario.Handler(req.Queries[0], req) - points := result.Series[0].Points + require.NotNil(t, result.Series) - So(result.Series, ShouldNotBeNil) - So(points[0][0].Float64, ShouldEqual, 1.234) + points := result.Series[0].Points + require.Equal(t, 1.234, points[0][0].Float64) }) }) - Convey("random walk table", t, func() { + t.Run("random walk table", func(t *testing.T) { scenario := ScenarioRegistry["random_walk_table"] - Convey("Should return a table that looks like value/min/max", func() { + t.Run("Should return a table that looks like value/min/max", func(t *testing.T) { req := &tsdb.TsdbQuery{ TimeRange: tsdb.NewFakeTimeRange("5m", "now", time.Now()), Queries: []*tsdb.Query{ @@ -45,18 +45,18 @@ func TestTestdataScenarios(t *testing.T) { result := scenario.Handler(req.Queries[0], req) table := result.Tables[0] - So(len(table.Rows), ShouldBeGreaterThan, 50) + require.Greater(t, len(table.Rows), 50) for _, row := range table.Rows { value := row[1] min := row[2] max := row[3] - So(min, ShouldBeLessThan, value) - So(max, ShouldBeGreaterThan, value) + require.Less(t, min, value) + require.Greater(t, max, value) } }) - Convey("Should return a table with some nil values", func() { + t.Run("Should return a table with some nil values", func(t *testing.T) { req := &tsdb.TsdbQuery{ TimeRange: tsdb.NewFakeTimeRange("5m", "now", time.Now()), Queries: []*tsdb.Query{ @@ -73,7 +73,7 @@ func TestTestdataScenarios(t *testing.T) { nil2 := false nil3 := false - So(len(table.Rows), ShouldBeGreaterThan, 50) + require.Greater(t, len(table.Rows), 50) for _, row := range table.Rows { if row[1] == nil { nil1 = true @@ -86,41 +86,37 @@ func TestTestdataScenarios(t *testing.T) { } } - So(nil1, ShouldBeTrue) - So(nil2, ShouldBeTrue) - So(nil3, ShouldBeTrue) + require.True(t, nil1) + require.True(t, nil2) + require.True(t, nil3) }) }) } -func TestToLabels(t *testing.T) { - Convey("read labels", t, func() { - tags := make(map[string]string) - tags["job"] = "foo" - tags["instance"] = "bar" +func TestParseLabels(t *testing.T) { + expectedTags := map[string]string{ + "job": "foo", + "instance": "bar", + } - query1 := tsdb.Query{ - Model: simplejson.NewFromAny(map[string]interface{}{ - "labels": `{job="foo", instance="bar"}`, - }), - } + query1 := tsdb.Query{ + Model: simplejson.NewFromAny(map[string]interface{}{ + "labels": `{job="foo", instance="bar"}`, + }), + } + require.Equal(t, expectedTags, parseLabels(&query1)) - So(parseLabels(&query1), ShouldResemble, tags) + query2 := tsdb.Query{ + Model: simplejson.NewFromAny(map[string]interface{}{ + "labels": `job=foo, instance=bar`, + }), + } + require.Equal(t, expectedTags, parseLabels(&query2)) - query2 := tsdb.Query{ - Model: simplejson.NewFromAny(map[string]interface{}{ - "labels": `job=foo, instance=bar`, - }), - } - - So(parseLabels(&query2), ShouldResemble, tags) - - query3 := tsdb.Query{ - Model: simplejson.NewFromAny(map[string]interface{}{ - "labels": `job = foo,instance = bar`, - }), - } - - So(parseLabels(&query3), ShouldResemble, tags) - }) + query3 := tsdb.Query{ + Model: simplejson.NewFromAny(map[string]interface{}{ + "labels": `job = foo,instance = bar`, + }), + } + require.Equal(t, expectedTags, parseLabels(&query3)) } diff --git a/public/app/core/components/ForgottenPassword/ChangePassword.tsx b/public/app/core/components/ForgottenPassword/ChangePassword.tsx index dda6ced168f..1e6faab9447 100644 --- a/public/app/core/components/ForgottenPassword/ChangePassword.tsx +++ b/public/app/core/components/ForgottenPassword/ChangePassword.tsx @@ -47,7 +47,7 @@ export const ChangePassword: FC = ({ onSubmit, onSkip }) => { {onSkip && ( diff --git a/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap b/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap index c1220d90a25..98be19581a9 100644 --- a/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap +++ b/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap @@ -7,7 +7,7 @@ exports[`Render should render component 1`] = `
- { expect(fetchSpy).toHaveBeenCalledTimes(1); // wait out the retry-after and call again - great success - await new Promise(resolve => setTimeout(() => resolve(), 1001)); + await new Promise(resolve => setTimeout(() => resolve(null), 1001)); await expect(transport.sendEvent(event)).resolves.toBeTruthy(); expect(fetchSpy).toHaveBeenCalledTimes(2); }); diff --git a/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap b/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap index 635471e0d48..1417adbade2 100644 --- a/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap +++ b/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap @@ -13,7 +13,7 @@ exports[`Render should render alert rules 1`] = `
- - `; 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 6b83de8b8cc..586d1c01cb5 100644 --- a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap +++ b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap @@ -43,7 +43,7 @@ exports[`Render should render CTA if there are no API keys 1`] = ` proTip="Remember you can provide view-only API access to other applications." title="You haven't added any API Keys yet." /> -
- Time to live - +
-
+ `; diff --git a/public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap b/public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap index 16e27a47d2a..6e7cc12cdb7 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap +++ b/public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap @@ -15,7 +15,7 @@ exports[`Render should render component 1`] = ` onPasteCopiedPanel={[Function]} />
- - +
diff --git a/public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx b/public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx index 39f51130290..28b207d83b3 100644 --- a/public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx @@ -33,7 +33,7 @@ export const DynamicConfigValueEditor: React.FC = // eslint-disable-next-line react/display-name const renderLabel = (includeDescription = true, includeCounter = false) => (isExpanded = false) => ( -
`; diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index a3773b4ce03..973d44de5a0 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -1,6 +1,5 @@ import React, { PureComponent } from 'react'; import classNames from 'classnames'; -import { isEqual } from 'lodash'; import { DataLink, LoadingState, PanelData, PanelMenuItem, QueryResultMetaNotice, ScopedVars } from '@grafana/data'; import { AngularComponent, config, getTemplateSrv } from '@grafana/runtime'; import { ClickOutsideWrapper, Icon, IconName, Tooltip, stylesFactory } from '@grafana/ui'; @@ -52,8 +51,8 @@ export class PanelHeader extends PureComponent { eventToClickCoordinates = (event: React.MouseEvent) => { return { - x: event.clientX, - y: event.clientY, + x: Math.floor(event.clientX), + y: Math.floor(event.clientY), }; }; @@ -62,7 +61,7 @@ export class PanelHeader extends PureComponent { }; isClick = (clickCoordinates: ClickCoordinates) => { - return isEqual(clickCoordinates, this.clickCoordinates); + return clickCoordinates.x === this.clickCoordinates.x && clickCoordinates.y === this.clickCoordinates.y; }; onMenuToggle = (event: React.MouseEvent) => { diff --git a/public/app/features/datasources/settings/__snapshots__/BasicSettings.test.tsx.snap b/public/app/features/datasources/settings/__snapshots__/BasicSettings.test.tsx.snap index 7fd90bb03c5..e9ee2a6f705 100644 --- a/public/app/features/datasources/settings/__snapshots__/BasicSettings.test.tsx.snap +++ b/public/app/features/datasources/settings/__snapshots__/BasicSettings.test.tsx.snap @@ -16,11 +16,11 @@ exports[`Render should render component 1`] = ` } } > - Name - + @@ -29,5 +29,5 @@ exports[`ErrorContainer should render component 1`] = `
- + `; diff --git a/public/app/features/explore/__snapshots__/Explore.test.tsx.snap b/public/app/features/explore/__snapshots__/Explore.test.tsx.snap index 3b9641966c5..1b67a99f918 100644 --- a/public/app/features/explore/__snapshots__/Explore.test.tsx.snap +++ b/public/app/features/explore/__snapshots__/Explore.test.tsx.snap @@ -37,7 +37,7 @@ exports[`Explore should render component 1`] = ` richHistoryButtonActive={false} /> - ; + abstract getChannelSupport(namespace: string): Promise; /** * List the possible values within this scope */ - abstract async listNamespaces(): Promise>>; + abstract listNamespaces(): Promise>>; } export interface CoreGrafanaLiveFeature { diff --git a/public/app/features/manage-dashboards/state/selectors.ts b/public/app/features/manage-dashboards/state/selectors.ts index 7113dc88913..4f9223b289d 100644 --- a/public/app/features/manage-dashboards/state/selectors.ts +++ b/public/app/features/manage-dashboards/state/selectors.ts @@ -14,7 +14,7 @@ export const getDashboardNavModel = (state: StoreState): NavModel => { } // This needs to be copied to avoid mutating the store in a selector - nav.main.children = [...navModel.main.children]; + nav.main.children = [...(navModel.main.children ?? [])]; for (const item of nav.main.children) { item.active = false; diff --git a/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap b/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap index 95b79ec31ad..682c28d0482 100644 --- a/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap +++ b/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap @@ -35,7 +35,7 @@ exports[`Render should render organization and preferences 1`] = ` - + - + `; diff --git a/public/app/features/plugins/PluginDashboards.tsx b/public/app/features/plugins/PluginDashboards.tsx index 5dbebc5c7a6..ee5461f1866 100644 --- a/public/app/features/plugins/PluginDashboards.tsx +++ b/public/app/features/plugins/PluginDashboards.tsx @@ -44,7 +44,7 @@ export class PluginDashboards extends PureComponent { const { dashboards } = this.state; return this.import(dashboards[index], true).then(() => { if (index + 1 < dashboards.length) { - return new Promise(resolve => { + return new Promise(resolve => { setTimeout(() => { this.importNext(index + 1).then(() => { resolve(); diff --git a/public/app/features/query/components/QueryGroupOptions.tsx b/public/app/features/query/components/QueryGroupOptions.tsx index dc0e27bbda1..b4166436fa5 100644 --- a/public/app/features/query/components/QueryGroupOptions.tsx +++ b/public/app/features/query/components/QueryGroupOptions.tsx @@ -171,7 +171,6 @@ export class QueryGroupOptionsEditor extends PureComponent { type="text" className="width-6" placeholder="60" - name={name} spellCheck={false} onBlur={this.onCacheTimeoutBlur} defaultValue={options.cacheTimeout ?? ''} @@ -205,7 +204,6 @@ export class QueryGroupOptionsEditor extends PureComponent { type="number" className="width-6" placeholder={`${realMd}`} - name={name} spellCheck={false} onBlur={this.onMaxDataPointsBlur} defaultValue={value} @@ -246,7 +244,6 @@ export class QueryGroupOptionsEditor extends PureComponent { type="text" className="width-6" placeholder={`${minIntervalOnDs}`} - name={name} spellCheck={false} onBlur={this.onMinIntervalBlur} defaultValue={options.minInterval ?? ''} diff --git a/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap index cccf6538e73..666898ba725 100644 --- a/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap @@ -23,7 +23,7 @@ exports[`Render should render component 1`] = ` className="page-action-bar__spacer" /> -
-
+ -
-
+
diff --git a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap index 5f9fdbbbf11..8c12eb61fd2 100644 --- a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap @@ -41,7 +41,7 @@ exports[`Render should render teams table 1`] = `
- - - - - - - - - - testName -
- + - testName -
-
+ - testName - - + - testName - - + - - -
-
+
@@ -72,13 +72,13 @@ exports[`Render should render component 1`] = ` Name - Permission - + -
-
-
+
@@ -166,13 +166,13 @@ exports[`Render should render team members 1`] = ` Name - Permission - + - + `; @@ -66,7 +66,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render s - + `; diff --git a/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap index 1967d04e1ca..a8e44c8514d 100644 --- a/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap @@ -1,8 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Render should render component 1`] = ` - - +
-
+
-
+ `; diff --git a/public/app/features/users/__snapshots__/UsersActionBar.test.tsx.snap b/public/app/features/users/__snapshots__/UsersActionBar.test.tsx.snap index 484356e654d..81da5fe74d5 100644 --- a/public/app/features/users/__snapshots__/UsersActionBar.test.tsx.snap +++ b/public/app/features/users/__snapshots__/UsersActionBar.test.tsx.snap @@ -7,7 +7,7 @@ exports[`Render should render component 1`] = `
- - - - - @@ -126,7 +126,7 @@ exports[`Render should render users table 1`] = ` size="sm" variant="destructive" /> - - @@ -195,7 +195,7 @@ exports[`Render should render users table 1`] = ` size="sm" variant="destructive" /> - - @@ -264,7 +264,7 @@ exports[`Render should render users table 1`] = ` size="sm" variant="destructive" /> - - @@ -333,7 +333,7 @@ exports[`Render should render users table 1`] = ` size="sm" variant="destructive" /> - - @@ -402,7 +402,7 @@ exports[`Render should render users table 1`] = ` size="sm" variant="destructive" /> - - @@ -471,7 +471,7 @@ exports[`Render should render users table 1`] = ` size="sm" variant="destructive" /> - { + await new Promise(resolve => { const unsubscribe = store.subscribe(() => { if (!isWaitingForDependencies(dependencies, store.getState())) { unsubscribe(); diff --git a/public/app/plugins/datasource/cloudwatch/components/__snapshots__/ConfigEditor.test.tsx.snap b/public/app/plugins/datasource/cloudwatch/components/__snapshots__/ConfigEditor.test.tsx.snap index 8fe9f7103e6..53fd134e641 100644 --- a/public/app/plugins/datasource/cloudwatch/components/__snapshots__/ConfigEditor.test.tsx.snap +++ b/public/app/plugins/datasource/cloudwatch/components/__snapshots__/ConfigEditor.test.tsx.snap @@ -16,12 +16,12 @@ exports[`Render should disable access key id field 1`] = `
- Authentication Provider - + - Custom Metrics - + - Authentication Provider - + - Custom Metrics - + - Authentication Provider - + - Custom Metrics - + - Authentication Provider - + - Custom Metrics - + - Authentication Provider - + - Custom Metrics - + {
{ diff --git a/public/app/plugins/datasource/elasticsearch/configuration/DataLinks.tsx b/public/app/plugins/datasource/elasticsearch/configuration/DataLinks.tsx index dd7fb13a96b..c579b663754 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/DataLinks.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/DataLinks.tsx @@ -32,9 +32,9 @@ export const DataLinks = (props: Props) => { Add links to existing fields. Links will be shown in log row details next to the field value.
-
- {value && - value.map((field, index) => { + {value && value.length > 0 && ( +
+ {value.map((field, index) => { return ( { /> ); })} -
-
-
+ )} + + ); }; diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx index 2c26f3007db..0cc38d6f63a 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx @@ -34,7 +34,7 @@ export const ElasticDetails = (props: Props) => {
-
+
{ />
-
+
{
- - { - const maxConcurrentShardRequests = getMaxConcurrenShardRequestOrDefault( - value.jsonData.maxConcurrentShardRequests, - option.value! - ); - onChange({ - ...value, - jsonData: { - ...value.jsonData, - esVersion: option.value!, - maxConcurrentShardRequests, - }, - }); - }} - value={esVersions.find(version => version.value === value.jsonData.esVersion)} - /> - } - /> - + { + const maxConcurrentShardRequests = getMaxConcurrenShardRequestOrDefault( + value.jsonData.maxConcurrentShardRequests, + option.value! + ); + onChange({ + ...value, + jsonData: { + ...value.jsonData, + esVersion: option.value!, + maxConcurrentShardRequests, + }, + }); + }} + value={esVersions.find(version => version.value === value.jsonData.esVersion)} + /> + } + />
{value.jsonData.esVersion >= 56 && (
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/supported_namespaces.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/supported_namespaces.ts index 46b03f52982..4e64306b3bb 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/supported_namespaces.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/supported_namespaces.ts @@ -54,6 +54,7 @@ export default class SupportedNamespaces { 'Microsoft.Network/trafficManagerProfiles', 'Microsoft.Network/networkWatchers/connectionMonitors', 'Microsoft.Network/frontdoors', + 'Microsoft.Network/natGateways', 'Microsoft.NotificationHubs/namespaces/notificationHubs', 'Microsoft.OperationalInsights/workspaces', 'Microsoft.PowerBIDedicated/capacities', diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/__snapshots__/AnalyticsConfig.test.tsx.snap b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/__snapshots__/AnalyticsConfig.test.tsx.snap index 1766d027d2e..ad08b089a45 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/__snapshots__/AnalyticsConfig.test.tsx.snap +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/__snapshots__/AnalyticsConfig.test.tsx.snap @@ -33,12 +33,12 @@ exports[`Render should disable log analytics credentials form 1`] = `
- Default Workspace - +
@@ -129,12 +129,12 @@ exports[`Render should enable azure log analytics load workspaces button 1`] = `
- Default Workspace - +
@@ -225,12 +225,12 @@ exports[`Render should render component 1`] = `
- Default Workspace - +
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/__snapshots__/AzureCredentialsForm.test.tsx.snap b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/__snapshots__/AzureCredentialsForm.test.tsx.snap index caf4a184fea..b7b4795554e 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/__snapshots__/AzureCredentialsForm.test.tsx.snap +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/__snapshots__/AzureCredentialsForm.test.tsx.snap @@ -11,12 +11,12 @@ exports[`Render should disable azure monitor secret input 1`] = `
- Azure Cloud - + - Default Subscription - +
@@ -228,12 +228,12 @@ exports[`Render should enable azure monitor load subscriptions button 1`] = `
- Azure Cloud - + - Directory (tenant) ID - +
@@ -523,11 +523,11 @@ exports[`Render should render component 1`] = `
- Application (client) ID - +
@@ -546,11 +546,11 @@ exports[`Render should render component 1`] = `
- Client Secret - +
@@ -569,11 +569,11 @@ exports[`Render should render component 1`] = `
- Default Subscription - +
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/__snapshots__/InsightsConfig.test.tsx.snap b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/__snapshots__/InsightsConfig.test.tsx.snap index 89b735c5299..b2e1b769d2a 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/__snapshots__/InsightsConfig.test.tsx.snap +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/__snapshots__/InsightsConfig.test.tsx.snap @@ -16,11 +16,11 @@ exports[`Render should disable insights api key input 1`] = `
- API Key - + - Application ID - +
@@ -84,11 +84,11 @@ exports[`Render should enable insights api key input 1`] = `
- API Key - +
@@ -106,11 +106,11 @@ exports[`Render should enable insights api key input 1`] = `
- Application ID - +
@@ -141,11 +141,11 @@ exports[`Render should render component 1`] = `
- API Key - +
@@ -163,11 +163,11 @@ exports[`Render should render component 1`] = `
- Application ID - +
diff --git a/public/app/plugins/datasource/influxdb/components/__snapshots__/ConfigEditor.test.tsx.snap b/public/app/plugins/datasource/influxdb/components/__snapshots__/ConfigEditor.test.tsx.snap index fd4dce305c6..b311ad46086 100644 --- a/public/app/plugins/datasource/influxdb/components/__snapshots__/ConfigEditor.test.tsx.snap +++ b/public/app/plugins/datasource/influxdb/components/__snapshots__/ConfigEditor.test.tsx.snap @@ -72,7 +72,7 @@ exports[`Render should disable basic auth password input 1`] = `
- - Database - +
@@ -139,11 +139,11 @@ exports[`Render should disable basic auth password input 1`] = `
- User - +
@@ -177,14 +177,14 @@ exports[`Render should disable basic auth password input 1`] = `
- HTTP Method - + - Min time interval - +
@@ -648,7 +648,7 @@ exports[`Render should hide white listed cookies input when browser access chose
- - Database - +
@@ -715,11 +715,11 @@ exports[`Render should hide white listed cookies input when browser access chose
- User - +
@@ -753,14 +753,14 @@ exports[`Render should hide white listed cookies input when browser access chose
- HTTP Method - + - Min time interval - +
diff --git a/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap b/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap index aa142f94ec3..644a78e2d2a 100644 --- a/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap +++ b/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`LokiExploreQueryEditor should render component 1`] = ` - - Legend - + - Legend - + - Legend - + - An additional lower limit for the step parameter of the Prometheus query and for the @@ -62,7 +62,7 @@ exports[`Render PromQueryEditor with basic options should render 1`] = ` width={7} > Min step - + - @@ -180,7 +180,7 @@ exports[`Render PromQueryEditor with basic options should render 1`] = ` } } /> - +
diff --git a/public/app/plugins/datasource/zipkin/utils/transforms.ts b/public/app/plugins/datasource/zipkin/utils/transforms.ts index 048d8106af0..2e4f29f7025 100644 --- a/public/app/plugins/datasource/zipkin/utils/transforms.ts +++ b/public/app/plugins/datasource/zipkin/utils/transforms.ts @@ -52,7 +52,7 @@ function transformSpan(span: ZipkinSpan): TraceSpanData { type: 'string', value: span.kind, }, - ...jaegerSpan.tags, + ...(jaegerSpan.tags ?? []), ]; } diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 3de95db23d4..abd78aa5bca 100644 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -273,7 +273,7 @@ class GraphElement { const hasLinksValue = hasLinks(field); if (hasLinksValue) { // Append the configured links to the panel datalinks - links = [...links, ...field.config.links]; + links = [...links, ...field.config.links!]; } const fieldConfig = { decimals: yAxisConfig.decimals, diff --git a/public/app/plugins/panel/graph3/module.tsx b/public/app/plugins/panel/graph3/module.tsx index c2d0128e916..2ceee4b145a 100644 --- a/public/app/plugins/panel/graph3/module.tsx +++ b/public/app/plugins/panel/graph3/module.tsx @@ -107,9 +107,8 @@ export const plugin = new PanelPlugin(GraphPanel) path: 'axisWidth', name: 'Width', category: ['Axis'], - defaultValue: 60, settings: { - placeholder: '60', + placeholder: 'Auto', }, showIf: c => c.axisPlacement !== AxisPlacement.Hidden, }); diff --git a/public/app/plugins/panel/graph3/plugins/AnnotationsPlugin.tsx b/public/app/plugins/panel/graph3/plugins/AnnotationsPlugin.tsx index 57d615d59ab..3175239b16b 100644 --- a/public/app/plugins/panel/graph3/plugins/AnnotationsPlugin.tsx +++ b/public/app/plugins/panel/graph3/plugins/AnnotationsPlugin.tsx @@ -32,7 +32,7 @@ export const AnnotationsPlugin: React.FC = ({ annotation ); useEffect(() => { - if (plotCtx.isPlotReady) { + if (plotCtx.getPlotInstance()) { const views: Array> = []; for (const frame of annotations) { @@ -41,7 +41,7 @@ export const AnnotationsPlugin: React.FC = ({ annotation annotationsRef.current = views; } - }, [plotCtx.isPlotReady, annotations]); + }, [plotCtx, annotations]); useEffect(() => { const unregister = plotCtx.registerPlugin({ @@ -93,13 +93,14 @@ export const AnnotationsPlugin: React.FC = ({ annotation const mapAnnotationToXYCoords = useCallback( (annotation: AnnotationsDataFrameViewDTO) => { - if (!annotation.time) { + const plotInstance = plotCtx.getPlotInstance(); + if (!annotation.time || !plotInstance) { return undefined; } return { - x: plotCtx.getPlotInstance().valToPos(annotation.time / 1000, 'x'), - y: plotCtx.getPlotInstance().bbox.height / window.devicePixelRatio + 4, + x: plotInstance.valToPos(annotation.time, 'x'), + y: plotInstance.bbox.height / window.devicePixelRatio + 4, }; }, [plotCtx.getPlotInstance] diff --git a/public/app/plugins/panel/graph3/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/graph3/plugins/ExemplarsPlugin.tsx index 2489f7ce785..14b0a5cc8aa 100644 --- a/public/app/plugins/panel/graph3/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/graph3/plugins/ExemplarsPlugin.tsx @@ -42,7 +42,7 @@ export const ExemplarsPlugin: React.FC = ({ exemplars, tim // THIS EVENT ONLY MOCKS EXEMPLAR Y VALUE!!!! TO BE REMOVED WHEN WE GET CORRECT EXEMPLARS SHAPE VIA PROPS useEffect(() => { - if (plotCtx.isPlotReady) { + if (plotCtx.getPlotInstance()) { const mocks: DataFrame[] = []; for (const frame of exemplars) { @@ -61,18 +61,19 @@ export const ExemplarsPlugin: React.FC = ({ exemplars, tim setExemplarsMock(mocks); } - }, [plotCtx.isPlotReady, exemplars]); + }, [plotCtx, exemplars]); const mapExemplarToXYCoords = useCallback( (exemplar: ExemplarsDataFrameViewDTO) => { - if (!exemplar.time) { + const plotInstance = plotCtx.getPlotInstance(); + if (!exemplar.time || !plotInstance) { return undefined; } return { - x: plotCtx.getPlotInstance().valToPos(exemplar.time / 1000, 'x'), + x: plotInstance.valToPos(exemplar.time / 1000, 'x'), // exemplar.y is a temporary mock for an examplar. This Needs to be calculated according to examplar scale! - y: Math.floor((exemplar.y * plotCtx.getPlotInstance().bbox.height) / window.devicePixelRatio), + y: Math.floor((exemplar.y * plotInstance.bbox.height) / window.devicePixelRatio), }; }, [plotCtx.getPlotInstance] diff --git a/scripts/circle-release-canary-packages.sh b/scripts/circle-release-canary-packages.sh new file mode 100755 index 00000000000..882549d03f0 --- /dev/null +++ b/scripts/circle-release-canary-packages.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -eo pipefail + +# shellcheck source=./scripts/helpers/exit-if-fail.sh +source "$(dirname "$0")/helpers/exit-if-fail.sh" + +function prepare_version_commit () { + echo $'\nCommitting version changes. This commit will not be pushed!' + git config --global user.email "drone@grafana.com" + git config --global user.name "Drone" + git commit -am "Version commit" +} + +# check if there were any changes to packages between current and previous commit +count=$(git diff HEAD~1..HEAD --name-only -- packages | awk '{c++} END {print c}') +count="1" +if [ -z "$count" ]; then + echo "No changes in packages, skipping packages publishing" +else + echo "Changes detected in ${count} packages" + echo "Starting to release latest canary version" + + echo "@grafana:registry=https://npm.pkg.github.com" >> ~/.npmrc + echo "//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGE_TOKEN}" >> ~/.npmrc + + # For some reason the --no-git-reset is not working as described so + # to get lerna to publish the packages we need to do a commit to the + # repository. We will not push this commit to the origin repository. + prepare_version_commit + + echo $'\nPublishing packages' + yarn packages:publishCanary --registry https://npm.pkg.github.com +fi + diff --git a/scripts/circle-release-next-packages.sh b/scripts/circle-release-next-packages.sh deleted file mode 100755 index 04e9bbf6248..00000000000 --- a/scripts/circle-release-next-packages.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail - -PACKAGES=(ui toolkit data runtime e2e e2e-selectors) - -# shellcheck source=./scripts/helpers/exit-if-fail.sh -source "$(dirname "$0")/helpers/exit-if-fail.sh" - -function prepare_version_commit () { - echo $'\nCommitting version changes. This commit will not be pushed!' - git config --global user.email "drone@grafana.com" - git config --global user.name "Drone" - git commit -am "Version commit" -} - -function unpublish_previous_canary () { - _package=$1 - echo $'\nUnpublishing previous canary packages' - # dist-tag next to be changed to canary when https://github.com/grafana/grafana/pull/18195 is merged - CURRENT_CANARY=$(npm view @grafana/"${_package}" dist-tags.canary) - if [ -z "${CURRENT_CANARY}" ]; then - echo "@grafana/${_package} - Nothing to unpublish" - else - echo "Unpublish @grafana/${_package}@${CURRENT_CANARY}" - _response=$(npm unpublish @grafana/"${_package}"@"${CURRENT_CANARY}" 2>&1) || ( - echo "$_response" | grep "404" || ( - # We want to deprecate here, rather than fail and return an non-0 exit code - echo "Unpublish unsuccessful [$?]. Deprecating @grafana/${_package}@${CURRENT_CANARY}" - _response=$(npm deprecate @grafana/"${_package}"@"${CURRENT_CANARY}" "this package has been deprecated" 2>&1) || ( - echo "$_response" | grep "404" && return 0 - ) - ) - ) - fi -} - -# check if there were any changes to packages between current and previous commit -count=$(git diff HEAD~1..HEAD --name-only -- packages | awk '{c++} END {print c}') -count="1" -if [ -z "$count" ]; then - echo "No changes in packages, skipping packages publishing" -else - echo "Changes detected in ${count} packages" - echo "Starting to release latest canary version" - - # For some reason the --no-git-reset is not working as described so - # to get lerna to publish the packages we need to do a commit to the - # repository. We will not push this commit to the origin repository. - prepare_version_commit - - # Frontend packages have already been versioned and built by the - # build-frontend step in drone. We will only unpublish the previous - # canary version and publish the current built version as the new - # latest canary build. - for PACKAGE in "${PACKAGES[@]}" - do - unpublish_previous_canary "$PACKAGE" - done - - echo $'\nPublishing packages' - yarn packages:publishCanary -fi - diff --git a/scripts/lib.star b/scripts/lib.star index 0c9e446dec0..d93948675f3 100644 --- a/scripts/lib.star +++ b/scripts/lib.star @@ -736,25 +736,23 @@ def mysql_integration_tests_step(): ], } -def release_next_npm_packages_step(edition): +def release_canary_npm_packages_step(edition): if edition == 'enterprise': return None return { - 'name': 'release-next-npm-packages', + 'name': 'release-canary-npm-packages', 'image': build_image, 'depends_on': [ 'end-to-end-tests', ], 'environment': { - 'NPM_TOKEN': { - 'from_secret': 'npm_token', + 'GITHUB_PACKAGE_TOKEN': { + 'from_secret': 'github_package_token', }, }, 'commands': [ - './node_modules/.bin/lerna bootstrap', - 'echo "//registry.npmjs.org/:_authToken=$${NPM_TOKEN}" >> ~/.npmrc', - './scripts/circle-release-next-packages.sh', + './scripts/circle-release-canary-packages.sh', ], } diff --git a/scripts/master.star b/scripts/master.star index 7ecca3e3cb6..46404c58c2b 100644 --- a/scripts/master.star +++ b/scripts/master.star @@ -25,7 +25,7 @@ load( 'enterprise_downstream_step', 'frontend_metrics_step', 'publish_storybook_step', - 'release_next_npm_packages_step', + 'release_canary_npm_packages_step', 'upload_packages_step', 'deploy_to_kubernetes_step', 'publish_packages_step', @@ -60,7 +60,7 @@ def get_steps(edition, is_downstream=False): build_docker_images_step(edition=edition, ver_mode=ver_mode, ubuntu=True, publish=publish), postgres_integration_tests_step(), mysql_integration_tests_step(), - release_next_npm_packages_step(edition), + release_canary_npm_packages_step(edition), upload_packages_step(edition=edition, ver_mode=ver_mode, is_downstream=is_downstream), deploy_to_kubernetes_step(edition=edition, is_downstream=is_downstream), ] diff --git a/yarn.lock b/yarn.lock index 451a4d583fd..0c847232aa1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -236,7 +236,7 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@^7.12.1", "@babel/core@^7.12.3", "@babel/core@^7.9.0": +"@babel/core@^7.12.1", "@babel/core@^7.12.3": version "7.12.3" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.3.tgz#1b436884e1e3bff6fb1328dc02b208759de92ad8" integrity sha512-0qXcZYKZp3/6N2jKYVxZv0aNCsxTSVCiK72DTiTYZAu7sjg73W0/aynWjMbiGd87EQL4WyA8reiJVh92AVla9g== @@ -2280,13 +2280,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-react-constant-elements@^7.9.0": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.12.1.tgz#4471f0851feec3231cc9aaa0dccde39947c1ac1e" - integrity sha512-KOHd0tIRLoER+J+8f9DblZDa1fLGPwaaN1DI1TVHuQFOpjHV22C3CUB3obeC4fexHY9nx+fH0hQNvLFFfA1mxA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-transform-react-display-name@^7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.12.1.tgz#1cbcd0c3b1d6648c55374a22fc9b6b7e5341c00d" @@ -2762,7 +2755,7 @@ js-levenshtein "^1.1.3" semver "^5.5.0" -"@babel/preset-env@^7.12.1", "@babel/preset-env@^7.9.5": +"@babel/preset-env@^7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.12.1.tgz#9c7e5ca82a19efc865384bb4989148d2ee5d7ac2" integrity sha512-H8kxXmtPaAGT7TyBvSSkoSTUK6RHh61So05SyEbpmr0MCZrsNYn7mGMzzeYoOUCdHzww61k8XBft2TaES+xPLg== @@ -2864,7 +2857,7 @@ "@babel/plugin-transform-react-jsx-self" "^7.8.3" "@babel/plugin-transform-react-jsx-source" "^7.8.3" -"@babel/preset-react@^7.12.1", "@babel/preset-react@^7.9.4": +"@babel/preset-react@^7.12.1": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.12.5.tgz#d45625f65d53612078a43867c5c6750e78772c56" integrity sha512-jcs++VPrgyFehkMezHtezS2BpnUlR7tQFAyesJn1vGTO9aTFZrgIQrA5YydlTwxbcjMwkFY6i04flCigRRr3GA== @@ -3116,7 +3109,7 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.12.1", "@babel/types@^7.12.5", "@babel/types@^7.9.5": +"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.12.1", "@babel/types@^7.12.5": version "7.12.6" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.6.tgz#ae0e55ef1cce1fbc881cd26f8234eb3e657edc96" integrity sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA== @@ -5134,18 +5127,6 @@ "@sentry/types" "5.25.0" tslib "^1.9.3" -"@sindresorhus/df@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@sindresorhus/df/-/df-1.0.1.tgz#c69b66f52f6fcdd287c807df210305dbaf78500d" - integrity sha1-xptm9S9vzdKHyAffIQMF2694UA0= - -"@sindresorhus/df@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/df/-/df-2.1.0.tgz#d208cf27e06f0bb476d14d7deccd7d726e9aa389" - integrity sha1-0gjPJ+BvC7R20U197M19cm6ao4k= - dependencies: - execa "^0.2.2" - "@sinonjs/commons@^1", "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.0.tgz#f90ffc52a2e519f018b13b6c4da03cbff36ebed6" @@ -5182,17 +5163,17 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== -"@storybook/addon-actions@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.1.2.tgz#6df92f5ac4ed5a5d56c5ff2f52de47cf43e3c42b" - integrity sha512-LBsHgN9BNekagdw5a4Yi6noHH64K+/6/phpz6GgD9HIhULOtHNrL2pG94Zj10rdCdu1y7nscSkfwA52niqyYYg== +"@storybook/addon-actions@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.1.9.tgz#b6d185b8e4e73691acc428b59e4d331195d64a40" + integrity sha512-vYqwuaBHrjRbbuyf4WBc5uDhrSejVEEiCabuu4g00R3dN7P5Ne/tbSw9EkYbbrRKxlEhdkk83DU1/J/+mCY5jw== dependencies: - "@storybook/addons" "6.1.2" - "@storybook/api" "6.1.2" - "@storybook/client-api" "6.1.2" - "@storybook/components" "6.1.2" - "@storybook/core-events" "6.1.2" - "@storybook/theming" "6.1.2" + "@storybook/addons" "6.1.9" + "@storybook/api" "6.1.9" + "@storybook/client-api" "6.1.9" + "@storybook/components" "6.1.9" + "@storybook/core-events" "6.1.9" + "@storybook/theming" "6.1.9" core-js "^3.0.1" fast-deep-equal "^3.1.1" global "^4.3.2" @@ -5205,17 +5186,17 @@ util-deprecate "^1.0.2" uuid "^8.0.0" -"@storybook/addon-backgrounds@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-6.1.2.tgz#f768e1180a0d2838bbd47eac7884896118963624" - integrity sha512-FUNqS94lpkCi1OqsNZQZFkk56nEcxCSrkCauwa6+CeusZoQ2X7I2sHLPnGYKcBzoPX6E2l2map/k0Bk0cDDyJg== +"@storybook/addon-backgrounds@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-6.1.9.tgz#4e9647001b2f396f56428218480f541e3a8a0e60" + integrity sha512-0/nnbnWZqo4NjyHFxcCHkcU+t8uUdk///9jSXRs4swmOLIFRaftEc/ZJ2rScu1Sc7y0CQdbk1Jjyx5/2cHBQtw== dependencies: - "@storybook/addons" "6.1.2" - "@storybook/api" "6.1.2" - "@storybook/client-logger" "6.1.2" - "@storybook/components" "6.1.2" - "@storybook/core-events" "6.1.2" - "@storybook/theming" "6.1.2" + "@storybook/addons" "6.1.9" + "@storybook/api" "6.1.9" + "@storybook/client-logger" "6.1.9" + "@storybook/components" "6.1.9" + "@storybook/core-events" "6.1.9" + "@storybook/theming" "6.1.9" core-js "^3.0.1" global "^4.3.2" memoizerific "^1.11.3" @@ -5223,24 +5204,24 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/addon-controls@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-6.1.2.tgz#86b7d26226ac437ebd8708098a7bed5a95e951d5" - integrity sha512-0knHFTxGmyReS0/LU81YrXzkCB7vUHgV8QOE3efNjEHj0zDr4t4GFNvl+FLt+wlkZAJ6sOyIXOks7ezdxFm4sw== +"@storybook/addon-controls@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-6.1.9.tgz#d7663d4c95db49163c372989f04264b11a4fa5cf" + integrity sha512-ctMXTcouqrPlNpQ0w3q6wjg16s1DL9VJtw1WvkySwezOE0ytfeC8M4nsbsKJnuY+dCadHKGn392DC1qVidY7Hg== dependencies: - "@storybook/addons" "6.1.2" - "@storybook/api" "6.1.2" - "@storybook/client-api" "6.1.2" - "@storybook/components" "6.1.2" - "@storybook/node-logger" "6.1.2" - "@storybook/theming" "6.1.2" + "@storybook/addons" "6.1.9" + "@storybook/api" "6.1.9" + "@storybook/client-api" "6.1.9" + "@storybook/components" "6.1.9" + "@storybook/node-logger" "6.1.9" + "@storybook/theming" "6.1.9" core-js "^3.0.1" ts-dedent "^2.0.0" -"@storybook/addon-docs@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.1.2.tgz#7bb79fd60de516e29cbcae88ed286c48831d848b" - integrity sha512-h9oyd7D0zgSc/LM73oTaVHF8H+et5tzXQ1CDtnh9Fpe9YgxItsJ7jke9z8dNapH6X+pmHE2rN1itqvasjpTsgA== +"@storybook/addon-docs@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.1.9.tgz#41cf82cece4abcf7640a1b7c2be6a7d5908c4678" + integrity sha512-5pwt2WU/yD4cnHU2KOpzU4t7A9FoePhZ15WOajtuV+W/hekNQSOGgNASQZbVOOldl954ireNIjZHtvthpI0tvQ== dependencies: "@babel/core" "^7.12.1" "@babel/generator" "^7.12.1" @@ -5251,18 +5232,18 @@ "@mdx-js/loader" "^1.6.19" "@mdx-js/mdx" "^1.6.19" "@mdx-js/react" "^1.6.19" - "@storybook/addons" "6.1.2" - "@storybook/api" "6.1.2" - "@storybook/client-api" "6.1.2" - "@storybook/client-logger" "6.1.2" - "@storybook/components" "6.1.2" - "@storybook/core" "6.1.2" - "@storybook/core-events" "6.1.2" + "@storybook/addons" "6.1.9" + "@storybook/api" "6.1.9" + "@storybook/client-api" "6.1.9" + "@storybook/client-logger" "6.1.9" + "@storybook/components" "6.1.9" + "@storybook/core" "6.1.9" + "@storybook/core-events" "6.1.9" "@storybook/csf" "0.0.1" - "@storybook/node-logger" "6.1.2" - "@storybook/postinstall" "6.1.2" - "@storybook/source-loader" "6.1.2" - "@storybook/theming" "6.1.2" + "@storybook/node-logger" "6.1.9" + "@storybook/postinstall" "6.1.9" + "@storybook/source-loader" "6.1.9" + "@storybook/theming" "6.1.9" acorn "^7.1.0" acorn-jsx "^5.1.0" acorn-walk "^7.0.0" @@ -5283,36 +5264,36 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/addon-essentials@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-6.1.2.tgz#5cc5146bb257937e0602d1a4b9f8d05dffa8531b" - integrity sha512-wQmDD9+p36K26PMxpMcuZblyXFAjJ0pG6JNxy3XULuPGZfeRPQGlmcU2JTDUVbryOJfxHu0nzDI7D+CJ8Q2wAg== +"@storybook/addon-essentials@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-6.1.9.tgz#e689de480ea626a784bc3784b36ceb4c9b366561" + integrity sha512-mw05LsBjWLu79u0lFsd3/kSpU7Dctt0xzcx8hXcaUKsiq7QvqPoKVutFuC8tvgoAVaCmmokLygscbMBOD2hq0w== dependencies: - "@storybook/addon-actions" "6.1.2" - "@storybook/addon-backgrounds" "6.1.2" - "@storybook/addon-controls" "6.1.2" - "@storybook/addon-docs" "6.1.2" - "@storybook/addon-toolbars" "6.1.2" - "@storybook/addon-viewport" "6.1.2" - "@storybook/addons" "6.1.2" - "@storybook/api" "6.1.2" - "@storybook/node-logger" "6.1.2" + "@storybook/addon-actions" "6.1.9" + "@storybook/addon-backgrounds" "6.1.9" + "@storybook/addon-controls" "6.1.9" + "@storybook/addon-docs" "6.1.9" + "@storybook/addon-toolbars" "6.1.9" + "@storybook/addon-viewport" "6.1.9" + "@storybook/addons" "6.1.9" + "@storybook/api" "6.1.9" + "@storybook/node-logger" "6.1.9" core-js "^3.0.1" regenerator-runtime "^0.13.7" ts-dedent "^2.0.0" -"@storybook/addon-knobs@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-6.1.2.tgz#6daf89fd4fdf6830ca0546b22f448ade3f49b10f" - integrity sha512-pNQIJJF68PBBEHNKzgXGUb1IyO3jtF+Gwkuxn5r1msrp4p7yTckl5alH4Abiy9hvcjThhyCTULLO2eee7eawLg== +"@storybook/addon-knobs@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-6.1.9.tgz#49773931770effc208db094c61eeecbceaf5836b" + integrity sha512-aUKD9FaQGl/WOkGT6utElspYZJ7cBtUARe41JN59qOao7RNviabipQUASrjORcYUUU55sfKsLPEqQaEKB3x2bw== dependencies: - "@storybook/addons" "6.1.2" - "@storybook/api" "6.1.2" - "@storybook/channels" "6.1.2" - "@storybook/client-api" "6.1.2" - "@storybook/components" "6.1.2" - "@storybook/core-events" "6.1.2" - "@storybook/theming" "6.1.2" + "@storybook/addons" "6.1.9" + "@storybook/api" "6.1.9" + "@storybook/channels" "6.1.9" + "@storybook/client-api" "6.1.9" + "@storybook/components" "6.1.9" + "@storybook/core-events" "6.1.9" + "@storybook/theming" "6.1.9" copy-to-clipboard "^3.0.8" core-js "^3.0.1" escape-html "^1.0.3" @@ -5326,18 +5307,18 @@ react-select "^3.0.8" regenerator-runtime "^0.13.7" -"@storybook/addon-storysource@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/addon-storysource/-/addon-storysource-6.1.2.tgz#a8b3eb94c05cca06d6671942ac2d550934e8a687" - integrity sha512-yAuGvlNFrsurXbyJBlTcgUgDVI00JaR7rDabw+lZKQq2LjkR19j7y8HoeuOafSBVhqon4PXTxP/LgQ1UwQfwzQ== +"@storybook/addon-storysource@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/addon-storysource/-/addon-storysource-6.1.9.tgz#9f7c50522b4de4bc90508a3882ba473bec82f435" + integrity sha512-xG3UAHRFfdn7dvoOjlvIHUNWgMPRLy87qjjHgmdW0jWyvGEtT7k10IRGgnozXp64ETy8dAnIyt3kmRUwXj/pkg== dependencies: - "@storybook/addons" "6.1.2" - "@storybook/api" "6.1.2" - "@storybook/client-logger" "6.1.2" - "@storybook/components" "6.1.2" - "@storybook/router" "6.1.2" - "@storybook/source-loader" "6.1.2" - "@storybook/theming" "6.1.2" + "@storybook/addons" "6.1.9" + "@storybook/api" "6.1.9" + "@storybook/client-logger" "6.1.9" + "@storybook/components" "6.1.9" + "@storybook/router" "6.1.9" + "@storybook/source-loader" "6.1.9" + "@storybook/theming" "6.1.9" core-js "^3.0.1" estraverse "^4.2.0" loader-utils "^2.0.0" @@ -5346,62 +5327,62 @@ react-syntax-highlighter "^13.5.0" regenerator-runtime "^0.13.7" -"@storybook/addon-toolbars@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-6.1.2.tgz#ed59dc005c2a47c35c18f00fdac98e182934b032" - integrity sha512-eudMdRrMjNDJ8TctROqiCyUxHJZJhhcU6GCvZxU3JT2uiWeARx6zmGPnZh7BlMG+YCNUboSAxy3nXbeEJAkQuw== +"@storybook/addon-toolbars@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-6.1.9.tgz#99c8b48afc24773faf9490753788ee354e97f5d8" + integrity sha512-rXYkVqSahrb6eG6CwyuI2TNzazUBGTsSQK1ywOmLwFK2xs+ybn1jDkkh5UmzuPN7g6ouT3qJfz1H2PMqfGEY8Q== dependencies: - "@storybook/addons" "6.1.2" - "@storybook/api" "6.1.2" - "@storybook/client-api" "6.1.2" - "@storybook/components" "6.1.2" + "@storybook/addons" "6.1.9" + "@storybook/api" "6.1.9" + "@storybook/client-api" "6.1.9" + "@storybook/components" "6.1.9" core-js "^3.0.1" -"@storybook/addon-viewport@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-6.1.2.tgz#61d7f1e8597f177c5d68a4a46bce94ed7d2df464" - integrity sha512-da2zrACzkrMtsUpn9hXKgH5FqJZZLCkxeZVGh9juBg3OOmOE6mPEF3WLtZiYVcYCPeoobJNScsnC1o+x97CGtw== +"@storybook/addon-viewport@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-6.1.9.tgz#7160b7598a34b8c19ff7571b781329fe8ad5de8d" + integrity sha512-+raES4+fDc0eCnpF4JW5kYwOTFEGiVmjmhFSrsXXuRXNGrCx4s5t9Bpklwm4YswIgmubB0Fpd0kaQS2LjRz94Q== dependencies: - "@storybook/addons" "6.1.2" - "@storybook/api" "6.1.2" - "@storybook/client-logger" "6.1.2" - "@storybook/components" "6.1.2" - "@storybook/core-events" "6.1.2" - "@storybook/theming" "6.1.2" + "@storybook/addons" "6.1.9" + "@storybook/api" "6.1.9" + "@storybook/client-logger" "6.1.9" + "@storybook/components" "6.1.9" + "@storybook/core-events" "6.1.9" + "@storybook/theming" "6.1.9" core-js "^3.0.1" global "^4.3.2" memoizerific "^1.11.3" prop-types "^15.7.2" regenerator-runtime "^0.13.7" -"@storybook/addons@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.1.2.tgz#e073d4d930d84cb5d54cef5bd4376724369b56de" - integrity sha512-Q+xTYikVz4cfS3IWw4Ns9YDY8jzC4E8C7meuoAMWZMaQn3Y8CdvTNrnoPZ1XnxaArVvkROCF45cdsDX7t0GNRQ== +"@storybook/addons@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.1.9.tgz#78f3cb27b7d934f091f311f89b6ca312d34f12b8" + integrity sha512-NRxdlGLmmSoVwlirVRgKC8xmW9cFkG+Sp5GEd4XkJDaaIg2vKR3RuFU9GuvIOVMxOhhERqhQ07bnDaAMKbFzGw== dependencies: - "@storybook/api" "6.1.2" - "@storybook/channels" "6.1.2" - "@storybook/client-logger" "6.1.2" - "@storybook/core-events" "6.1.2" - "@storybook/router" "6.1.2" - "@storybook/theming" "6.1.2" + "@storybook/api" "6.1.9" + "@storybook/channels" "6.1.9" + "@storybook/client-logger" "6.1.9" + "@storybook/core-events" "6.1.9" + "@storybook/router" "6.1.9" + "@storybook/theming" "6.1.9" core-js "^3.0.1" global "^4.3.2" regenerator-runtime "^0.13.7" -"@storybook/api@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.1.2.tgz#d941f4b6d449e204d8bb42e251a92b62274451f3" - integrity sha512-tuYpQiBPjHse6OwodWXPdUiwwYuFNIlgQmGS2gKRv5Aw3dZEIvB7BLymi+cgz8KqMf5T1NaZhsQO/+3FPiQ+Kw== +"@storybook/api@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.1.9.tgz#3f9bf00b2b18fa02965079fe775bd713677b30a3" + integrity sha512-S9SXlSiMeI450NIbOnx3UU9TZNyVD7jcBCjfNzhj0PqzRX/IG5Usj+R88Jm6MSIDjtsVjrWRCou+PrCh2xMnlQ== dependencies: "@reach/router" "^1.3.3" - "@storybook/channels" "6.1.2" - "@storybook/client-logger" "6.1.2" - "@storybook/core-events" "6.1.2" + "@storybook/channels" "6.1.9" + "@storybook/client-logger" "6.1.9" + "@storybook/core-events" "6.1.9" "@storybook/csf" "0.0.1" - "@storybook/router" "6.1.2" + "@storybook/router" "6.1.9" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.1.2" + "@storybook/theming" "6.1.9" "@types/reach__router" "^1.3.5" core-js "^3.0.1" fast-deep-equal "^3.1.1" @@ -5414,38 +5395,38 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/channel-postmessage@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.1.2.tgz#92205f381059d4d155679aba4a1d89d2f438f9b5" - integrity sha512-zKEPK4MAgBy3RanCpl4afJpcYJMlb56gpIFALm2WJZ8zsHUUFb45NgruqNPiBsF0IthD6bvemcCJ5mmyn5D4xg== +"@storybook/channel-postmessage@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.1.9.tgz#5d73c67ba94bcf68b14138bba6c5bb0850c72c5e" + integrity sha512-tX7pD9Xrf1WsatpJqtJ6o8MlgxG7jH+oFhNPkGvUbWiolVDQmuDndwM8Hh1kUnOWlyE1AN5hlM7av8MY+9D3NA== dependencies: - "@storybook/channels" "6.1.2" - "@storybook/client-logger" "6.1.2" - "@storybook/core-events" "6.1.2" + "@storybook/channels" "6.1.9" + "@storybook/client-logger" "6.1.9" + "@storybook/core-events" "6.1.9" core-js "^3.0.1" global "^4.3.2" qs "^6.6.0" telejson "^5.0.2" -"@storybook/channels@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.1.2.tgz#e42b2cb3502291cfde36082b4844bd6de839ce86" - integrity sha512-jek1xyLXgMnk3XbNjjVxx4xirOE7QwbwCbiESS7g2ELhRum4C636iVzLYUpt8rujXFmGibqwnoeOH4rPNIFuvA== +"@storybook/channels@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.1.9.tgz#94f07ff3615b11c07d1902be6b6cd298c0eea55c" + integrity sha512-aV+KsZPuoTtFKSMUkSCyVlVmtVHkSH35dSbyMazjlUD9cOLwkXB1s+LZL/GxxSR6a6uR75V0QWxItfNxaJETMQ== dependencies: core-js "^3.0.1" ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-api@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.1.2.tgz#52fc44883ad6004ace16638ec9837e841ae38c47" - integrity sha512-E5iS9vcElKpL8ve9Ifr2gLLWl+Ig7SXty0DjX7B5rtBFZmq7JENtdWXCOKbHvYiP/wQfxdoQLMDFW1dsg3FnAA== +"@storybook/client-api@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.1.9.tgz#d4a8d38bc657f26e4837831b961e085da1954d51" + integrity sha512-b2DFaGAS5G2ly3UJY5NJNXh/LxgLgSJLbqPL4t48MFW5XjH+rmEWXE9P+ujCaPclH1/y7mZRMprDj3ycDbRo3Q== dependencies: - "@storybook/addons" "6.1.2" - "@storybook/channel-postmessage" "6.1.2" - "@storybook/channels" "6.1.2" - "@storybook/client-logger" "6.1.2" - "@storybook/core-events" "6.1.2" + "@storybook/addons" "6.1.9" + "@storybook/channel-postmessage" "6.1.9" + "@storybook/channels" "6.1.9" + "@storybook/client-logger" "6.1.9" + "@storybook/core-events" "6.1.9" "@storybook/csf" "0.0.1" "@types/qs" "^6.9.0" "@types/webpack-env" "^1.15.3" @@ -5460,23 +5441,23 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-logger@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.1.2.tgz#4036d6fdd8d8865da17c4da3dd8e23322c89140b" - integrity sha512-s/OGSBxVQ5PAWeoXtVoPJXOaJ8eL4V8a7+szd/3Gvl81WLDDqJnH9x/mTM4qzRpfEL8LXfCRBOilt7R3x6EG/A== +"@storybook/client-logger@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.1.9.tgz#1d61a64000d4691780d75e19b78fd44adfdb5d9c" + integrity sha512-i7Q2ky9+Jwv+wmnlOGxmDOEdmaTIB69OQnnZNWGKufOwoIMjn6QO0VifARyA9W++nNSijjJ5th84tLJALaoCTA== dependencies: core-js "^3.0.1" global "^4.3.2" -"@storybook/components@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.1.2.tgz#8d543206a662d0ca95ce47c1ab912523a7b487d4" - integrity sha512-JUW3KxRuX7Fzp7rEtQ7MhpozXcKlQJJbIctgsUdie+1CkWQHXma7gPUqDcBJjrU4nPV1EoTC2xs1c7TduavlpQ== +"@storybook/components@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.1.9.tgz#f25d18f3a410cc7e9549ddb3c971c40d9108d4d8" + integrity sha512-cYYm3fHo9MW0bbl47lu1ncwulV7V9VEF8FC96uvys07oaCTFWKzQ0z/FD0nCqeK6eEz1+SEqnGwLFmOtqlRXDQ== dependencies: "@popperjs/core" "^2.4.4" - "@storybook/client-logger" "6.1.2" + "@storybook/client-logger" "6.1.9" "@storybook/csf" "0.0.1" - "@storybook/theming" "6.1.2" + "@storybook/theming" "6.1.9" "@types/overlayscrollbars" "^1.9.0" "@types/react-color" "^3.0.1" "@types/react-syntax-highlighter" "11.0.4" @@ -5494,17 +5475,17 @@ react-textarea-autosize "^8.1.1" ts-dedent "^2.0.0" -"@storybook/core-events@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.1.2.tgz#d29e9a8c7ef6bb41135282f4da8027bb6f399545" - integrity sha512-Vhv22JyDLLJ9kqPCiXyMmePFYVbvYHREzXOtA3vgcf/leKxp6aGHE+OId1e4fycQN4ZU6kPbVnRrI9WFJwV+pA== +"@storybook/core-events@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.1.9.tgz#0a88281837d1aa657a93a9abf7f5aad65b8d68e7" + integrity sha512-oOpqpjCTJCt0U5lnQ16OZU0iKIDh2/MIg4yrnDw+Pt6zGyX3zSvtB+9W8LQFnMwm+cXaNmiizGwt/W+4OiORjQ== dependencies: core-js "^3.0.1" -"@storybook/core@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.1.2.tgz#999f80da29f7e476a51a34e953c837278917aeb0" - integrity sha512-LR5YnqrEciIOYc5MkAAOKW4/ijibC7x+eM9sB/pjwdq/+Rspj4XKUzVl1J5oAUHKl8EAdtUGkdWB8fpU6l4fdg== +"@storybook/core@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.1.9.tgz#e6575e294cb4d2d9b57d5976a145cae8f4a88594" + integrity sha512-guz+R6eDX923Cw7NqgS5PrpTmmjDB+m5X1iF9pwKlpPTfzIiT/wTzJm4PwhFoGONNoXrItObX/6hW6OQbX4aOA== dependencies: "@babel/core" "^7.12.3" "@babel/plugin-proposal-class-properties" "^7.12.1" @@ -5528,20 +5509,20 @@ "@babel/preset-react" "^7.12.1" "@babel/preset-typescript" "^7.12.1" "@babel/register" "^7.12.1" - "@storybook/addons" "6.1.2" - "@storybook/api" "6.1.2" - "@storybook/channel-postmessage" "6.1.2" - "@storybook/channels" "6.1.2" - "@storybook/client-api" "6.1.2" - "@storybook/client-logger" "6.1.2" - "@storybook/components" "6.1.2" - "@storybook/core-events" "6.1.2" + "@storybook/addons" "6.1.9" + "@storybook/api" "6.1.9" + "@storybook/channel-postmessage" "6.1.9" + "@storybook/channels" "6.1.9" + "@storybook/client-api" "6.1.9" + "@storybook/client-logger" "6.1.9" + "@storybook/components" "6.1.9" + "@storybook/core-events" "6.1.9" "@storybook/csf" "0.0.1" - "@storybook/node-logger" "6.1.2" - "@storybook/router" "6.1.2" + "@storybook/node-logger" "6.1.9" + "@storybook/router" "6.1.9" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.1.2" - "@storybook/ui" "6.1.2" + "@storybook/theming" "6.1.9" + "@storybook/ui" "6.1.9" "@types/glob-base" "^0.3.0" "@types/micromatch" "^4.0.1" "@types/node-fetch" "^2.5.4" @@ -5598,7 +5579,6 @@ style-loader "^1.2.1" telejson "^5.0.2" terser-webpack-plugin "^3.0.0" - trash "^6.1.1" ts-dedent "^2.0.0" unfetch "^4.1.0" url-loader "^4.0.0" @@ -5616,10 +5596,10 @@ dependencies: lodash "^4.17.15" -"@storybook/node-logger@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.1.2.tgz#fed92ebd537f9e5888e42cc46f393936fcd382ae" - integrity sha512-T/lIUVActNhvNhmuF8HxBT87e3N012HqnYsCo+blTpOqsntmP2m2D7Ipu46EwShMYiBUAr4JaGSt/yjhITcvag== +"@storybook/node-logger@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.1.9.tgz#c63a61f72209d76eeeffe9d151fec043864b9438" + integrity sha512-2gP9BSBXEOGIcUyzRdIkIJi1UEINUAIyuv9bfKODo4GfujRg7DLz/mpi/FdwmulGg/viXWSXa6ccb6ziIgY9RA== dependencies: "@types/npmlog" "^4.1.2" chalk "^4.0.0" @@ -5627,26 +5607,25 @@ npmlog "^4.1.2" pretty-hrtime "^1.0.3" -"@storybook/postinstall@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-6.1.2.tgz#2d07588b1a3946be9eea70a6d069a0db1be4fba3" - integrity sha512-/+1AM/5OaGK2Hg/JygxLgYnUVkiKcMQ3/5JCGbDd/px9SE8kL570tS3LvMS+t3jnMCGKXQW0B5iIVTndDG0AYQ== +"@storybook/postinstall@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-6.1.9.tgz#1c62cf46b7b8c50e939c8b14de9e7133368aab45" + integrity sha512-9aL0+ALWHKGJAnC8CdRBE7PlX87sKMEWZkDQJL5uT1eikfgfAmS1AgkpIbFD8mcxstcZmFKRwc1BD4mvwzqtQQ== dependencies: core-js "^3.0.1" -"@storybook/react@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/react/-/react-6.1.2.tgz#863662543a18c3a2ecea7a5646ec711c29163b05" - integrity sha512-q7Nj++ucjk9pVt/0jUlhoqzeypbzKdmRyN80hPAsSmvpzONlJtK82VA0iujBh9G6T+cT289nq2pf555tQfiAdQ== +"@storybook/react@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/react/-/react-6.1.9.tgz#063427015b3d0ce582b1b6b7826d40d963d265ce" + integrity sha512-HJWHQE+eCC7sz1vqvgmBMn2sA1uc0ByEj+NeSgyi45jBFI+Ke4a8hxx6k5XA7k9gLznqG8TPGg0z6EdQTJTLkQ== dependencies: "@babel/preset-flow" "^7.12.1" "@babel/preset-react" "^7.12.1" "@pmmmwh/react-refresh-webpack-plugin" "^0.4.2" - "@storybook/addons" "6.1.2" - "@storybook/core" "6.1.2" - "@storybook/node-logger" "6.1.2" + "@storybook/addons" "6.1.9" + "@storybook/core" "6.1.9" + "@storybook/node-logger" "6.1.9" "@storybook/semver" "^7.3.2" - "@svgr/webpack" "^5.4.0" "@types/webpack-env" "^1.15.3" babel-plugin-add-react-displayname "^0.0.5" babel-plugin-named-asset-import "^0.3.1" @@ -5662,10 +5641,10 @@ ts-dedent "^2.0.0" webpack "^4.44.2" -"@storybook/router@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.1.2.tgz#606790a89e5f25d9bd808de765e9292b609ad191" - integrity sha512-fNKWCEJlA0uCFBNbRgI9VD3a6hyelr528MEfdWcNvjffYXsF4DnMwewWYEPx01JUrWiAeZ3Qs5FYphuHn8BKEw== +"@storybook/router@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.1.9.tgz#c0b24dc3ab53d58541b81c7abea2f11d7fbbebf6" + integrity sha512-kIlmSFBnqI198oMCncFZR7MxoV5/kP6KS0paFcyu1XE1zO2ovV6eQZ8pPpOjSsD/ISu4Y44uE+ZDNsEehjj6GQ== dependencies: "@reach/router" "^1.3.3" "@types/reach__router" "^1.3.5" @@ -5682,13 +5661,13 @@ core-js "^3.6.5" find-up "^4.1.0" -"@storybook/source-loader@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-6.1.2.tgz#0982fcfa252c72f139da8bac7f478edbb3a7db13" - integrity sha512-rxfqecE4AaG+hyGUJRh4yXz/bqVmboo0X39AEObq7zwf3vz+6iw3j4MmepGHnyBotCVLw2AmKZ1405b4wO4fBg== +"@storybook/source-loader@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-6.1.9.tgz#10eebf12ecd7baa70e51f368c41b89b4b5084eb1" + integrity sha512-2h3TS1O9qJ0xmOOJrWxY9ODEFK0/RLxEmWcXngWAcH1ZrHAuz6UcEatO2bIgykrorf9xKqqk7/AYyE9KxW668A== dependencies: - "@storybook/addons" "6.1.2" - "@storybook/client-logger" "6.1.2" + "@storybook/addons" "6.1.9" + "@storybook/client-logger" "6.1.9" "@storybook/csf" "0.0.1" core-js "^3.0.1" estraverse "^4.2.0" @@ -5699,15 +5678,15 @@ regenerator-runtime "^0.13.7" source-map "^0.7.3" -"@storybook/theming@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.1.2.tgz#55fb60017acdf952450cc0cb9b530d36aa039178" - integrity sha512-MC89aDLIrng3zqpoTys0gylP76rlBYyEKUWtcZqrnFm6b+IqhmeP8e6KV/e5wmBfjuFqmDsKLLVMN6BwwZJlgg== +"@storybook/theming@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.1.9.tgz#8c584aa623f3d6e33b1e3b3de2ec1f41bdc5d9ab" + integrity sha512-orzMQkyEhAQEi0E9iwmUkzh5yPHoYGBz17t2aydDeT6oGKii6if8Mq2oPVycfVKZ84QO7GFAS9q1nVCRcuD8oA== dependencies: "@emotion/core" "^10.1.1" "@emotion/is-prop-valid" "^0.8.6" "@emotion/styled" "^10.0.23" - "@storybook/client-logger" "6.1.2" + "@storybook/client-logger" "6.1.9" core-js "^3.0.1" deep-object-diff "^1.1.0" emotion-theming "^10.0.19" @@ -5717,21 +5696,21 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" -"@storybook/ui@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.1.2.tgz#ee288141b70f908a8dcea646cb2ec52cf69a2483" - integrity sha512-ZBfL4x+E43DR+GnC/wjJ9QYP0sdXyZxxKWLQoDkNPIdDnNtFGyKFQyElo9GgtyGxIukssO0sW6fwWWpnErKTyw== +"@storybook/ui@6.1.9": + version "6.1.9" + resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.1.9.tgz#1ed3168d9fe5827285c13d8507dd1fd872830542" + integrity sha512-4MK5iTf7kI5DYVeWRiD6lkXdd0S6eiQJu9lvWqMOQJLOH5Bq77g0Ejo+38RTEQpV6we7hCPWWnRXQBjmJ2+19w== dependencies: "@emotion/core" "^10.1.1" - "@storybook/addons" "6.1.2" - "@storybook/api" "6.1.2" - "@storybook/channels" "6.1.2" - "@storybook/client-logger" "6.1.2" - "@storybook/components" "6.1.2" - "@storybook/core-events" "6.1.2" - "@storybook/router" "6.1.2" + "@storybook/addons" "6.1.9" + "@storybook/api" "6.1.9" + "@storybook/channels" "6.1.9" + "@storybook/client-logger" "6.1.9" + "@storybook/components" "6.1.9" + "@storybook/core-events" "6.1.9" + "@storybook/router" "6.1.9" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.1.2" + "@storybook/theming" "6.1.9" "@types/markdown-to-jsx" "^6.11.0" copy-to-clipboard "^3.0.8" core-js "^3.0.1" @@ -5753,114 +5732,6 @@ resolve-from "^5.0.0" store2 "^2.7.1" -"@stroncium/procfs@^1.0.0": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@stroncium/procfs/-/procfs-1.2.1.tgz#6b9be6fd20fb0a4c20e99a8695e083c699bb2b45" - integrity sha512-X1Iui3FUNZP18EUvysTHxt+Avu2nlVzyf90YM8OYgP6SGzTzzX/0JgObfO1AQQDzuZtNNz29bVh8h5R97JrjxA== - -"@svgr/babel-plugin-add-jsx-attribute@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz#81ef61947bb268eb9d50523446f9c638fb355906" - integrity sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg== - -"@svgr/babel-plugin-remove-jsx-attribute@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz#6b2c770c95c874654fd5e1d5ef475b78a0a962ef" - integrity sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg== - -"@svgr/babel-plugin-remove-jsx-empty-expression@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz#25621a8915ed7ad70da6cea3d0a6dbc2ea933efd" - integrity sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA== - -"@svgr/babel-plugin-replace-jsx-attribute-value@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz#0b221fc57f9fcd10e91fe219e2cd0dd03145a897" - integrity sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ== - -"@svgr/babel-plugin-svg-dynamic-title@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz#139b546dd0c3186b6e5db4fefc26cb0baea729d7" - integrity sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg== - -"@svgr/babel-plugin-svg-em-dimensions@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz#6543f69526632a133ce5cabab965deeaea2234a0" - integrity sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw== - -"@svgr/babel-plugin-transform-react-native-svg@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz#00bf9a7a73f1cad3948cdab1f8dfb774750f8c80" - integrity sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q== - -"@svgr/babel-plugin-transform-svg-component@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.4.0.tgz#a2212b4d018e6075a058bb7e220a66959ef7a03c" - integrity sha512-zLl4Fl3NvKxxjWNkqEcpdSOpQ3LGVH2BNFQ6vjaK6sFo2IrSznrhURIPI0HAphKiiIwNYjAfE0TNoQDSZv0U9A== - -"@svgr/babel-preset@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-5.4.0.tgz#da21854643e1c4ad2279239baa7d5a8b128c1f15" - integrity sha512-Gyx7cCxua04DBtyILTYdQxeO/pwfTBev6+eXTbVbxe4HTGhOUW6yo7PSbG2p6eJMl44j6XSequ0ZDP7bl0nu9A== - dependencies: - "@svgr/babel-plugin-add-jsx-attribute" "^5.4.0" - "@svgr/babel-plugin-remove-jsx-attribute" "^5.4.0" - "@svgr/babel-plugin-remove-jsx-empty-expression" "^5.0.1" - "@svgr/babel-plugin-replace-jsx-attribute-value" "^5.0.1" - "@svgr/babel-plugin-svg-dynamic-title" "^5.4.0" - "@svgr/babel-plugin-svg-em-dimensions" "^5.4.0" - "@svgr/babel-plugin-transform-react-native-svg" "^5.4.0" - "@svgr/babel-plugin-transform-svg-component" "^5.4.0" - -"@svgr/core@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/core/-/core-5.4.0.tgz#655378ee43679eb94fee3d4e1976e38252dff8e7" - integrity sha512-hWGm1DCCvd4IEn7VgDUHYiC597lUYhFau2lwJBYpQWDirYLkX4OsXu9IslPgJ9UpP7wsw3n2Ffv9sW7SXJVfqQ== - dependencies: - "@svgr/plugin-jsx" "^5.4.0" - camelcase "^6.0.0" - cosmiconfig "^6.0.0" - -"@svgr/hast-util-to-babel-ast@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.4.0.tgz#bb5d002e428f510aa5b53ec0a02377a95b367715" - integrity sha512-+U0TZZpPsP2V1WvVhqAOSTk+N+CjYHdZx+x9UBa1eeeZDXwH8pt0CrQf2+SvRl/h2CAPRFkm+Ey96+jKP8Bsgg== - dependencies: - "@babel/types" "^7.9.5" - -"@svgr/plugin-jsx@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-5.4.0.tgz#ab47504c55615833c6db70fca2d7e489f509787c" - integrity sha512-SGzO4JZQ2HvGRKDzRga9YFSqOqaNrgLlQVaGvpZ2Iht2gwRp/tq+18Pvv9kS9ZqOMYgyix2LLxZMY1LOe9NPqw== - dependencies: - "@babel/core" "^7.7.5" - "@svgr/babel-preset" "^5.4.0" - "@svgr/hast-util-to-babel-ast" "^5.4.0" - svg-parser "^2.0.2" - -"@svgr/plugin-svgo@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-5.4.0.tgz#45d9800b7099a6f7b4d85ebac89ab9abe8592f64" - integrity sha512-3Cgv3aYi1l6SHyzArV9C36yo4kgwVdF3zPQUC6/aCDUeXAofDYwE5kk3e3oT5ZO2a0N3lB+lLGvipBG6lnG8EA== - dependencies: - cosmiconfig "^6.0.0" - merge-deep "^3.0.2" - svgo "^1.2.2" - -"@svgr/webpack@^5.4.0": - version "5.4.0" - resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-5.4.0.tgz#b68bc86e29cf007292b96ced65f80971175632e0" - integrity sha512-LjepnS/BSAvelnOnnzr6Gg0GcpLmnZ9ThGFK5WJtm1xOqdBE/1IACZU7MMdVzjyUkfFqGz87eRE4hFaSLiUwYg== - dependencies: - "@babel/core" "^7.9.0" - "@babel/plugin-transform-react-constant-elements" "^7.9.0" - "@babel/preset-env" "^7.9.5" - "@babel/preset-react" "^7.9.4" - "@svgr/core" "^5.4.0" - "@svgr/plugin-jsx" "^5.4.0" - "@svgr/plugin-svgo" "^5.4.0" - loader-utils "^2.0.0" - "@testing-library/dom@^7.26.6": version "7.26.6" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.26.6.tgz#d558db63070a3acea5bea7e2497e631cd12541cc" @@ -10091,17 +9962,6 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" -clone-deep@^0.2.4: - version "0.2.4" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.2.4.tgz#4e73dd09e9fb971cc38670c5dced9c1896481cc6" - integrity sha1-TnPdCen7lxzDhnDF3O2cGJZIHMY= - dependencies: - for-own "^0.1.3" - is-plain-object "^2.0.1" - kind-of "^3.0.2" - lazy-cache "^1.0.3" - shallow-clone "^0.1.2" - clone-deep@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" @@ -10689,17 +10549,6 @@ cosmiconfig@^6.0.0: path-type "^4.0.0" yaml "^1.7.2" -cp-file@^6.1.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-6.2.0.tgz#40d5ea4a1def2a9acdd07ba5c0b0246ef73dc10d" - integrity sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA== - dependencies: - graceful-fs "^4.1.2" - make-dir "^2.0.0" - nested-error-stacks "^2.0.0" - pify "^4.0.1" - safe-buffer "^5.0.1" - cp-file@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-7.0.0.tgz#b9454cfd07fe3b974ab9ea0e5f29655791a9b8cd" @@ -10789,14 +10638,6 @@ create-react-context@<=0.2.2: fbjs "^0.8.0" gud "^1.0.0" -cross-spawn-async@^2.1.1: - version "2.2.5" - resolved "https://registry.yarnpkg.com/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz#845ff0c0834a3ded9d160daca6d390906bb288cc" - integrity sha1-hF/wwINKPe2dFg2sptOQkGuyiMw= - dependencies: - lru-cache "^4.0.0" - which "^1.2.8" - cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -10998,22 +10839,6 @@ css-tree@1.0.0-alpha.33: mdn-data "2.0.4" source-map "^0.5.3" -css-tree@1.0.0-alpha.37: - version "1.0.0-alpha.37" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" - integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg== - dependencies: - mdn-data "2.0.4" - source-map "^0.6.1" - -css-tree@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0.tgz#21993fa270d742642a90409a2c0cb3ac0298adf6" - integrity sha512-CdVYz/Yuqw0VdKhXPBIgi8DO3NicJVYZNWeX9XcIuSp9ZoFT5IcleVRW07O5rMjdcx1mb+MEJPknTTEW7DdsYw== - dependencies: - mdn-data "2.0.12" - source-map "^0.6.1" - css-tree@^1.0.0-alpha.28: version "1.0.0-alpha.36" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.36.tgz#48a848ecad1859c9d005bf900ce58a3465987954" @@ -11146,13 +10971,6 @@ csso@^3.5.1: dependencies: css-tree "1.0.0-alpha.29" -csso@^4.0.2: - version "4.1.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.1.0.tgz#1d31193efa99b87aa6bad6c0cef155e543d09e8b" - integrity sha512-h+6w/W1WqXaJA4tb1dk7r5tVbOm97MsKxzwnvOR04UQ6GILroryjMWu3pmCCtL2mLaEStQ0fZgeGiy99mo7iyg== - dependencies: - css-tree "^1.0.0" - cssom@^0.4.1, cssom@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" @@ -13155,17 +12973,6 @@ execa@4.0.0: signal-exit "^3.0.2" strip-final-newline "^2.0.0" -execa@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.2.2.tgz#e2ead472c2c31aad6f73f1ac956eef45e12320cb" - integrity sha1-4urUcsLDGq1vc/GslW7vReEjIMs= - dependencies: - cross-spawn-async "^2.1.1" - npm-run-path "^1.0.0" - object-assign "^4.0.1" - path-key "^1.0.0" - strip-eof "^1.0.0" - execa@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" @@ -13838,23 +13645,11 @@ for-each@~0.3.3: dependencies: is-callable "^1.1.3" -for-in@^0.1.3: - version "0.1.8" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" - integrity sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE= - -for-in@^1.0.1, for-in@^1.0.2: +for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= -for-own@^0.1.3: - version "0.1.5" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" - integrity sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4= - dependencies: - for-in "^1.0.1" - forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -15649,7 +15444,7 @@ is-boolean-object@^1.0.1: resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e" integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ== -is-buffer@^1.0.2, is-buffer@^1.1.5, is-buffer@~1.1.1: +is-buffer@^1.1.5, is-buffer@~1.1.1: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== @@ -15986,7 +15781,7 @@ is-path-inside@^2.1.0: dependencies: path-is-inside "^1.0.2" -is-path-inside@^3.0.1, is-path-inside@^3.0.2: +is-path-inside@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== @@ -16006,7 +15801,7 @@ is-plain-object@3.0.1: resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.1.tgz#662d92d24c0aa4302407b0d45d21f2251c85f85b" integrity sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g== -is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: +is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== @@ -16797,6 +16592,17 @@ jest-util@^24.0.0, jest-util@^24.9.0: slash "^2.0.0" source-map "^0.6.0" +jest-util@^25.5.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-25.5.0.tgz#31c63b5d6e901274d264a4fec849230aa3fa35b0" + integrity sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA== + dependencies: + "@jest/types" "^25.5.0" + chalk "^3.0.0" + graceful-fs "^4.2.4" + is-ci "^2.0.0" + make-dir "^3.0.0" + jest-util@^26.1.0, jest-util@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" @@ -17187,13 +16993,6 @@ killable@^1.0.1: resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== -kind-of@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5" - integrity sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU= - dependencies: - is-buffer "^1.0.2" - kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -17255,11 +17054,6 @@ lazy-ass@^1.6.0: resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM= -lazy-cache@^0.2.3: - version "0.2.7" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65" - integrity sha1-f+3fLctu23fRHvHRF6tf/fCrG2U= - lazy-cache@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" @@ -17799,7 +17593,7 @@ lowlight@^1.14.0: fault "^1.0.0" highlight.js "~10.4.0" -lru-cache@^4.0.0, lru-cache@^4.0.1: +lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== @@ -18026,11 +17820,6 @@ mdast-util-to-string@^1.0.0: resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz#27055500103f51637bd07d01da01eb1967a43527" integrity sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A== -mdn-data@2.0.12: - version "2.0.12" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.12.tgz#bbb658d08b38f574bbb88f7b83703defdcc46844" - integrity sha512-ULbAlgzVb8IqZ0Hsxm6hHSlQl3Jckst2YEQS7fODu9ilNWy2LvcoSY7TRFIktABP2mdppBioc66va90T+NUs8Q== - mdn-data@2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" @@ -18136,15 +17925,6 @@ merge-class-names@^1.1.1: resolved "https://registry.yarnpkg.com/merge-class-names/-/merge-class-names-1.2.0.tgz#cb30ecfc3bdbd96b6f76d0a98777907e5fbb3462" integrity sha512-ifHxhC8DojHT1rG3PHCaJYInUqPd0WO+PxsaYDMkgy7RzfyOFtnlpr/hbhki+m/3R/ujIRVnZkD/AHjgjb5uhg== -merge-deep@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/merge-deep/-/merge-deep-3.0.2.tgz#f39fa100a4f1bd34ff29f7d2bf4508fbb8d83ad2" - integrity sha512-T7qC8kg4Zoti1cFd8Cr0M+qaZfOwjlPDEdZIIPPB2JZctjaPM4fX+i7HOId69tAti2fvO6X5ldfYUONDODsrkA== - dependencies: - arr-union "^3.1.0" - clone-deep "^0.2.4" - kind-of "^3.0.2" - merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -18418,14 +18198,6 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mixin-object@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" - integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4= - dependencies: - for-in "^0.1.3" - is-extendable "^0.1.1" - mkdirp-promise@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/mkdirp-promise/-/mkdirp-promise-5.0.1.tgz#e9b8f68e552c68a9c1713b84883f7a1dd039b8a1" @@ -18552,15 +18324,6 @@ moo@^0.4.3: resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e" integrity sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw== -mount-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mount-point/-/mount-point-3.0.0.tgz#665cb9edebe80d110e658db56c31d0aef51a8f97" - integrity sha1-Zly57evoDREOZY21bDHQrvUaj5c= - dependencies: - "@sindresorhus/df" "^1.0.1" - pify "^2.3.0" - pinkie-promise "^2.0.1" - mousetrap-global-bind@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mousetrap-global-bind/-/mousetrap-global-bind-1.1.0.tgz#cd7de9222bd0646fa2e010d54c84a74c26a88edd" @@ -18583,15 +18346,6 @@ move-concurrently@^1.0.1: rimraf "^2.5.4" run-queue "^1.0.3" -move-file@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/move-file/-/move-file-1.2.0.tgz#789f92d276c62511d214b1b285aa16e015c2f2fc" - integrity sha512-USHrRmxzGowUWAGBbJPdFjHzEqtxDU03pLHY0Rfqgtnq+q8FOIs8wvkkf+Udmg77SJKs47y9sI0jJvQeYsmiCA== - dependencies: - cp-file "^6.1.0" - make-dir "^3.0.0" - path-exists "^3.0.0" - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -19087,13 +18841,6 @@ npm-pick-manifest@^3.0.0: npm-package-arg "^6.0.0" semver "^5.4.1" -npm-run-path@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-1.0.0.tgz#f5c32bf595fe81ae927daec52e82f8b000ac3c8f" - integrity sha1-9cMr9ZX+ga6Sfa7FLoL4sACsPI8= - dependencies: - path-key "^1.0.0" - npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" @@ -19671,7 +19418,7 @@ p-try@^1.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= -p-try@^2.0.0, p-try@^2.2.0: +p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== @@ -19905,11 +19652,6 @@ path-is-inside@^1.0.1, path-is-inside@^1.0.2: resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= -path-key@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-1.0.0.tgz#5d53d578019646c0d68800db4e146e6bdc2ac7af" - integrity sha1-XVPVeAGWRsDWiADbThRua9wqx68= - path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" @@ -20009,7 +19751,7 @@ pify@^4.0.1: resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== -pinkie-promise@^2.0.0, pinkie-promise@^2.0.1: +pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= @@ -23791,16 +23533,6 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" -shallow-clone@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060" - integrity sha1-WQnodLp3EG1zrEFM/sH/yofZcGA= - dependencies: - is-extendable "^0.1.1" - kind-of "^2.0.1" - lazy-cache "^0.2.3" - mixin-object "^2.0.1" - shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -24834,11 +24566,6 @@ supports-hyperlinks@^2.0.0: has-flag "^4.0.0" supports-color "^7.0.0" -svg-parser@^2.0.2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" - integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== - svgo@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.0.tgz#bae51ba95ded9a33a36b7c46ce9c359ae9154313" @@ -24858,25 +24585,6 @@ svgo@^1.0.0: unquote "~1.1.1" util.promisify "~1.0.0" -svgo@^1.2.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" - integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== - dependencies: - chalk "^2.4.1" - coa "^2.0.2" - css-select "^2.0.0" - css-select-base-adapter "^0.1.1" - css-tree "1.0.0-alpha.37" - csso "^4.0.2" - js-yaml "^3.13.1" - mkdirp "~0.5.1" - object.values "^1.1.0" - sax "~1.2.4" - stable "^0.1.8" - unquote "~1.1.1" - util.promisify "~1.0.0" - symbol-observable@^1.0.3, symbol-observable@^1.0.4, symbol-observable@^1.1.0, symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" @@ -25418,21 +25126,6 @@ tr46@^2.0.2: dependencies: punycode "^2.1.1" -trash@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/trash/-/trash-6.1.1.tgz#8fb863421b31f32571f2650b53534934d5e63025" - integrity sha512-4i56lCmz2RG6WZN018hf4L75L5HboaFuKkHx3wDG/ihevI99e0OgFyl8w6G4ioqBm62V4EJqCy5xw3vQSNXU8A== - dependencies: - "@stroncium/procfs" "^1.0.0" - globby "^7.1.1" - is-path-inside "^3.0.2" - make-dir "^3.0.0" - move-file "^1.1.0" - p-map "^3.0.0" - p-try "^2.2.0" - uuid "^3.3.2" - xdg-trashdir "^2.1.1" - traverse@^0.6.6: version "0.6.6" resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" @@ -25510,6 +25203,23 @@ ts-jest@26.4.4: semver "7.x" yargs-parser "20.x" +ts-jest@26.4.4: + version "26.4.4" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.4.4.tgz#61f13fb21ab400853c532270e52cc0ed7e502c49" + integrity sha512-3lFWKbLxJm34QxyVNNCgXX1u4o/RV0myvA2y2Bxm46iGIjKlaY0own9gIckbjZJPn+WaJEnfPPJ20HHGpoq4yg== + dependencies: + "@types/jest" "26.x" + bs-logger "0.x" + buffer-from "1.x" + fast-json-stable-stringify "2.x" + jest-util "^26.1.0" + json5 "2.x" + lodash.memoize "4.x" + make-error "1.x" + mkdirp "1.x" + semver "7.x" + yargs-parser "20.x" + ts-loader@6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-6.2.1.tgz#67939d5772e8a8c6bdaf6277ca023a4812da02ef" @@ -25532,15 +25242,15 @@ ts-loader@8.0.11: micromatch "^4.0.0" semver "^6.0.0" -ts-node@8.8.1: - version "8.8.1" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.8.1.tgz#7c4d3e9ed33aa703b64b28d7f9d194768be5064d" - integrity sha512-10DE9ONho06QORKAaCBpPiFCdW+tZJuY/84tyypGtl6r+/C7Asq0dhqbRZURuUlLQtZxxDvT8eoj8cGW0ha6Bg== +ts-node@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.0.0.tgz#e7699d2a110cc8c0d3b831715e417688683460b3" + integrity sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg== dependencies: arg "^4.1.0" diff "^4.0.1" make-error "^1.1.1" - source-map-support "^0.5.6" + source-map-support "^0.5.17" yn "3.1.1" ts-pnp@^1.1.6: @@ -25553,12 +25263,7 @@ tslib@1.10.0, tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== -tslib@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" - integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ== - -tslib@^2.0.0, tslib@^2.0.1: +tslib@2.0.3, tslib@^2.0.0, tslib@^2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== @@ -25681,6 +25386,11 @@ typescript@4.0.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2" integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== +typescript@4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.2.tgz#6369ef22516fe5e10304aae5a5c4862db55380e9" + integrity sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ== + typescript@~3.9.7: version "3.9.7" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" @@ -26637,7 +26347,7 @@ which-pm-runs@^1.0.0: resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= -which@1, which@1.3.1, which@^1.2.14, which@^1.2.8, which@^1.2.9, which@^1.3.1: +which@1, which@1.3.1, which@^1.2.14, which@^1.2.9, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -26865,29 +26575,11 @@ ws@^7.2.3: resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd" integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w== -xdg-basedir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2" - integrity sha1-7byQPMOF/ARSPZZqM1UEtVBNG9I= - dependencies: - os-homedir "^1.0.0" - xdg-basedir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= -xdg-trashdir@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/xdg-trashdir/-/xdg-trashdir-2.1.1.tgz#59a60aaf8e6f9240c1daed9a0944b2f514c27d8e" - integrity sha512-KcVhPaOu2ZurYNHSRTf1+ZHORkTZGCQ+u0JHN17QixRISJq4pXOnjt/lQcehvtHL5QAKhSzKgyjrcNnPdkPBHA== - dependencies: - "@sindresorhus/df" "^2.1.0" - mount-point "^3.0.0" - pify "^2.2.0" - user-home "^2.0.0" - xdg-basedir "^2.0.0" - xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" @@ -26917,9 +26609,9 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== "y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + version "4.0.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" + integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== y18n@^5.0.5: version "5.0.5" @@ -26968,6 +26660,11 @@ yargs-parser@20.x, yargs-parser@^20.2.2: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== +yargs-parser@20.x: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + yargs-parser@^11.1.1: version "11.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"