From 9e7ad70e5d8c8f45127f70e58176e866fa5928ab Mon Sep 17 00:00:00 2001 From: nikki-kiga <42276368+nikki-kiga@users.noreply.github.com> Date: Thu, 2 Sep 2021 17:04:02 -0700 Subject: [PATCH 001/132] FieldNamePicker: Add preview details for field type and values (#38768) * add field type and values preview to picker * add more details to description * add icon to Select and specify icons for FieldType * remove description --- .../grafana-ui/src/components/MatchersUI/utils.ts | 14 +++++++++++++- .../src/components/Select/SelectMenu.tsx | 3 +++ .../src/components/Select/getSelectStyles.ts | 3 +++ packages/grafana-ui/src/types/icon.ts | 2 ++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/MatchersUI/utils.ts b/packages/grafana-ui/src/components/MatchersUI/utils.ts index abd10f88549..d402e2cf5e8 100644 --- a/packages/grafana-ui/src/components/MatchersUI/utils.ts +++ b/packages/grafana-ui/src/components/MatchersUI/utils.ts @@ -1,5 +1,6 @@ import { useMemo } from 'react'; -import { DataFrame, Field, getFieldDisplayName, SelectableValue } from '@grafana/data'; +import { DataFrame, Field, FieldType, getFieldDisplayName, SelectableValue } from '@grafana/data'; +import { IconName } from '../..'; /** * @internal @@ -61,6 +62,15 @@ export function useFieldDisplayNames(data: DataFrame[], filter?: (field: Field) }, [data, filter]); } +const fieldTypeIcons: { [key in FieldType]: IconName } = { + time: 'clock-nine', + string: 'font', + number: 'calculator-alt', + boolean: 'toggle-on', + trace: 'info-circle', + other: 'brackets-curly', +}; + /** * @internal */ @@ -79,9 +89,11 @@ export function useSelectOptions( if (!found && name === currentName) { found = true; } + const field = displayNames.fields.get(name); options.push({ value: name, label: name, + icon: field ? fieldTypeIcons[field.type] : undefined, }); } for (const name of displayNames.raw) { diff --git a/packages/grafana-ui/src/components/Select/SelectMenu.tsx b/packages/grafana-ui/src/components/Select/SelectMenu.tsx index 008d871ba93..628c8660365 100644 --- a/packages/grafana-ui/src/components/Select/SelectMenu.tsx +++ b/packages/grafana-ui/src/components/Select/SelectMenu.tsx @@ -4,6 +4,8 @@ import { getSelectStyles } from './getSelectStyles'; import { cx } from '@emotion/css'; import { SelectableValue } from '@grafana/data'; import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar'; +import { Icon } from '../Icon/Icon'; +import { IconName } from '../../types'; interface SelectMenuProps { maxHeight: number; @@ -49,6 +51,7 @@ export const SelectMenuOptions = React.forwardRef + {data.icon && } {data.imgUrl && }
{renderOptionLabel ? renderOptionLabel(data) : children} diff --git a/packages/grafana-ui/src/components/Select/getSelectStyles.ts b/packages/grafana-ui/src/components/Select/getSelectStyles.ts index bc60888cc3f..8821b1cc74e 100644 --- a/packages/grafana-ui/src/components/Select/getSelectStyles.ts +++ b/packages/grafana-ui/src/components/Select/getSelectStyles.ts @@ -27,6 +27,9 @@ export const getSelectStyles = stylesFactory((theme: GrafanaTheme2) => { background: ${theme.colors.action.hover}; } `, + optionIcon: css` + margin-right: ${theme.spacing(1)}; + `, optionImage: css` label: grafana-select-option-image; width: 16px; diff --git a/packages/grafana-ui/src/types/icon.ts b/packages/grafana-ui/src/types/icon.ts index b2bb10fafd8..404020679fc 100644 --- a/packages/grafana-ui/src/types/icon.ts +++ b/packages/grafana-ui/src/types/icon.ts @@ -68,6 +68,7 @@ export const getAvailableIcons = () => 'file-copy-alt', 'filter', 'folder', + 'font', 'fire', 'folder-open', 'folder-plus', @@ -140,6 +141,7 @@ export const getAvailableIcons = () => 'table', 'tag-alt', 'times', + 'toggle-on', 'trash-alt', 'unlock', 'upload', From caef39b6a400068c6fd1b11d0a0fb7cd5187e1b2 Mon Sep 17 00:00:00 2001 From: Will <1873880+willrnch@users.noreply.github.com> Date: Fri, 3 Sep 2021 04:14:17 +0200 Subject: [PATCH 002/132] Chore: fix typo (#38838) --- packages/grafana-schema/src/schema/graph.gen.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-schema/src/schema/graph.gen.ts b/packages/grafana-schema/src/schema/graph.gen.ts index 89260ac9289..081fb369005 100644 --- a/packages/grafana-schema/src/schema/graph.gen.ts +++ b/packages/grafana-schema/src/schema/graph.gen.ts @@ -1,6 +1,6 @@ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // NOTE: This file will be auto generated from models.cue -// It is currenty hand written but will serve as the target for cuetsy +// It is currently hand written but will serve as the target for cuetsy //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ export enum AxisPlacement { From 2a2f10da7e0429835d6d4d8c86691d5b1476dc93 Mon Sep 17 00:00:00 2001 From: nikki-kiga <42276368+nikki-kiga@users.noreply.github.com> Date: Thu, 2 Sep 2021 19:18:06 -0700 Subject: [PATCH 003/132] Transformations: Use fieldMatchers for convertFieldType and add doc (#38769) --- .../panels/transformations/types-options.md | 31 ++++++++ .../transformers/convertFieldType.ts | 78 ++++++++++++------- .../ConvertFieldTypeTransformerEditor.tsx | 6 +- 3 files changed, 85 insertions(+), 30 deletions(-) diff --git a/docs/sources/panels/transformations/types-options.md b/docs/sources/panels/transformations/types-options.md index db14bee1251..3042eaf7e37 100644 --- a/docs/sources/panels/transformations/types-options.md +++ b/docs/sources/panels/transformations/types-options.md @@ -10,6 +10,7 @@ Grafana comes with the following transformations: - [Add field from calculation]({{< relref "./types-options.md#add-field-from-calculation" >}}) - [Concatenate fields]({{< relref "./types-options.md#concatenate-fields" >}}) - [Config from query results]({{< relref "./config-from-query.md" >}}) +- [Convert field type]({{< relref "./types-options.md#convert-field-type" >}}) - [Filter data by name]({{< relref "./types-options.md#filter-data-by-name" >}}) - [Filter data by query]({{< relref "./types-options.md#filter-data-by-query" >}}) - [Filter data by value]({{< relref "./types-options.md#filter-data-by-value" >}}) @@ -324,6 +325,36 @@ After you concatenate the fields, the data frame would be: | ---- | ------- | --- | ------ | | 15.4 | 1230233 | 3.2 | 5 | +## Convert field type + +This transformation changes the field type of the specified field. + +- **Field -** Select from available fields +- **as -** Select the FieldType to convert to + - **Numeric -** attempts to make the values numbers + - **String -** will make the values strings + - **Time -** attempts to parse the values as time + - Will show an option to specify a DateFormat as input by a string like yyyy-mm-dd or DD MM YYYY hh:mm:ss + - **Boolean -** will make the values booleans + +For example the following query could be modified by selecting the time field, as Time, and Date Format as YYYY. + +| Time | Mark | Value | +| ---------- | ----- | ----- | +| 2017-07-01 | above | 25 | +| 2018-08-02 | below | 22 | +| 2019-09-02 | below | 29 | +| 2020-10-04 | above | 22 | + +The result: + +| Time | Mark | Value | +| ------------------- | ----- | ----- | +| 2017-01-01 00:00:00 | above | 25 | +| 2018-01-01 00:00:00 | below | 22 | +| 2019-01-01 00:00:00 | below | 29 | +| 2020-01-01 00:00:00 | above | 22 | + ## Series to rows > **Note:** This transformation is available in Grafana 7.1+. diff --git a/packages/grafana-data/src/transformations/transformers/convertFieldType.ts b/packages/grafana-data/src/transformations/transformers/convertFieldType.ts index 277e1db3322..8cb8c95b454 100644 --- a/packages/grafana-data/src/transformations/transformers/convertFieldType.ts +++ b/packages/grafana-data/src/transformations/transformers/convertFieldType.ts @@ -5,20 +5,28 @@ import { DataTransformerID } from './ids'; import { DataFrame, Field, FieldType } from '../../types/dataFrame'; import { dateTimeParse } from '../../datetime'; import { ArrayVector } from '../../vector'; +import { fieldMatchers } from '../matchers'; +import { FieldMatcherID } from '../matchers/ids'; export interface ConvertFieldTypeTransformerOptions { conversions: ConvertFieldTypeOptions[]; } export interface ConvertFieldTypeOptions { + /** + * The field to convert field type + */ targetField?: string; + /** + * The field type to convert to + */ destinationType?: FieldType; + /** + * Date format to parse a string datetime + */ dateFormat?: string; } -/** - * @alpha - */ export const convertFieldTypeTransformer: SynchronousDataTransformerInfo = { id: DataTransformerID.convertFieldType, name: 'Convert field type', @@ -43,32 +51,44 @@ export const convertFieldTypeTransformer: SynchronousDataTransformerInfo ({ ...frame })); - frames.forEach((frame) => { - for (let fieldIdx = 0; fieldIdx < frame.fields.length; fieldIdx++) { - let field = frame.fields[fieldIdx]; - for (let cIdx = 0; cIdx < options.conversions.length; cIdx++) { - if (field.name === options.conversions[cIdx].targetField) { - //check in about matchers with Ryan - const conversion = options.conversions[cIdx]; - frame.fields[fieldIdx] = convertFieldType(field, conversion); - break; - } - } + for (const conversion of options.conversions) { + if (!conversion.targetField) { + continue; } - frameCopy.push(frame); - }); - return frameCopy; + const matches = fieldMatchers.get(FieldMatcherID.byName).get(conversion.targetField); + for (const frame of framesCopy) { + frame.fields = frame.fields.map((field) => { + if (matches(field, frame, framesCopy)) { + return convertFieldType(field, conversion); + } + return field; + }); + } + } + + return framesCopy; } +/** + * Convert a single field type to specifed field type. + * @param field - field to convert + * @param opts - field conversion options + * @returns converted field + * + * @internal + */ export function convertFieldType(field: Field, opts: ConvertFieldTypeOptions): Field { switch (opts.destinationType) { case FieldType.time: @@ -84,6 +104,9 @@ export function convertFieldType(field: Field, opts: ConvertFieldTypeOptions): F } } +/** + * @internal + */ export function fieldToTimeField(field: Field, dateFormat?: string): Field { let opts = dateFormat ? { format: dateFormat } : undefined; @@ -109,12 +132,8 @@ function fieldToNumberField(field: Field): Field { const numValues = field.values.toArray().slice(); for (let n = 0; n < numValues.length; n++) { - if (numValues[n]) { - let number = +numValues[n]; - numValues[n] = Number.isFinite(number) ? number : null; - } else { - numValues[n] = null; - } + const number = +numValues[n]; + numValues[n] = Number.isFinite(number) ? number : null; } return { @@ -128,7 +147,7 @@ function fieldToBooleanField(field: Field): Field { const booleanValues = field.values.toArray().slice(); for (let b = 0; b < booleanValues.length; b++) { - booleanValues[b] = Boolean(booleanValues[b]); + booleanValues[b] = Boolean(!!booleanValues[b]); } return { @@ -153,7 +172,12 @@ function fieldToStringField(field: Field): Field { } /** - * @alpha + * Checks the first value. Assumes any number should be time fieldtype. Otherwise attempts to make the fieldtype time. + * @param field - field to ensure is a time fieldtype + * @param dateFormat - date format used to parse a string datetime + * @returns field as time + * + * @public */ export function ensureTimeField(field: Field, dateFormat?: string): Field { const firstValueTypeIsNumber = typeof field.values.get(0) === 'number'; diff --git a/public/app/core/components/TransformersUI/ConvertFieldTypeTransformerEditor.tsx b/public/app/core/components/TransformersUI/ConvertFieldTypeTransformerEditor.tsx index 217e5e95f3c..1086ac74d18 100644 --- a/public/app/core/components/TransformersUI/ConvertFieldTypeTransformerEditor.tsx +++ b/public/app/core/components/TransformersUI/ConvertFieldTypeTransformerEditor.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { ChangeEvent, useCallback } from 'react'; import { DataTransformerID, FieldNamePickerConfigSettings, @@ -56,9 +56,9 @@ export const ConvertFieldTypeTransformerEditor: React.FC (value: SelectableValue) => { + (idx) => (e: ChangeEvent) => { const conversions = options.conversions; - conversions[idx] = { ...conversions[idx], dateFormat: value.value }; + conversions[idx] = { ...conversions[idx], dateFormat: e.currentTarget.value }; onChange({ ...options, conversions: conversions, From 68c7d054bfb0452c77b0890bf0d48f6c504368d1 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Fri, 3 Sep 2021 17:50:13 +0200 Subject: [PATCH 004/132] Update PID file location form /var/run to /run (#35739) Signed-off-by: MichaIng --- packaging/deb/default/grafana-server | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/deb/default/grafana-server b/packaging/deb/default/grafana-server index eb77e62d774..cd0580aa4ff 100644 --- a/packaging/deb/default/grafana-server +++ b/packaging/deb/default/grafana-server @@ -21,4 +21,4 @@ PLUGINS_DIR=/var/lib/grafana/plugins PROVISIONING_CFG_DIR=/etc/grafana/provisioning # Only used on systemd systems -PID_FILE_DIR=/var/run/grafana +PID_FILE_DIR=/run/grafana From d913e46e374f128c3ac9d01eade3c291cebeb8ab Mon Sep 17 00:00:00 2001 From: Kevin Minehart Date: Fri, 3 Sep 2021 11:24:21 -0500 Subject: [PATCH 005/132] Chore: move build command to importable package (#38726) * move build command to importable package & clean up --- build.go | 466 +------------------------------------------ pkg/build/cmd.go | 314 +++++++++++++++++++++++++++++ pkg/build/docs.go | 2 + pkg/build/exec.go | 34 ++++ pkg/build/fs.go | 101 ++++++++++ pkg/build/git.go | 17 ++ pkg/build/opts.go | 66 ++++++ pkg/build/version.go | 66 ++++++ 8 files changed, 602 insertions(+), 464 deletions(-) create mode 100644 pkg/build/cmd.go create mode 100644 pkg/build/docs.go create mode 100644 pkg/build/exec.go create mode 100644 pkg/build/fs.go create mode 100644 pkg/build/git.go create mode 100644 pkg/build/opts.go create mode 100644 pkg/build/version.go diff --git a/build.go b/build.go index 45e1bff35ae..50b8e4e2c35 100644 --- a/build.go +++ b/build.go @@ -3,476 +3,14 @@ package main import ( - "bytes" - "crypto/md5" - "crypto/sha256" - "encoding/json" - "flag" - "fmt" - "go/build" - "io" - "io/ioutil" "log" "os" - "os/exec" - "path" - "path/filepath" - "runtime" - "strconv" - "strings" - "time" -) -const ( - windows = "windows" - linux = "linux" -) - -var ( - //versionRe = regexp.MustCompile(`-[0-9]{1,3}-g[0-9a-f]{5,10}`) - goarch string - goos string - gocc string - cgo bool - libc string - pkgArch string - version string = "v1" - buildTags []string - // deb & rpm does not support semver so have to handle their version a little differently - linuxPackageVersion string = "v1" - linuxPackageIteration string = "" - race bool - workingDir string - includeBuildId bool = true - buildId string = "0" - serverBinary string = "grafana-server" - cliBinary string = "grafana-cli" - binaries []string = []string{serverBinary, cliBinary} - isDev bool = false - enterprise bool = false - skipRpmGen bool = false - skipDebGen bool = false - printGenVersion bool = false + "github.com/grafana/grafana/pkg/build" ) func main() { log.SetOutput(os.Stdout) log.SetFlags(0) - - var buildIdRaw string - var buildTagsRaw string - - flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH") - flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS") - flag.StringVar(&gocc, "cc", "", "CC") - flag.StringVar(&libc, "libc", "", "LIBC") - flag.StringVar(&buildTagsRaw, "build-tags", "", "Sets custom build tags") - flag.BoolVar(&cgo, "cgo-enabled", cgo, "Enable cgo") - flag.StringVar(&pkgArch, "pkg-arch", "", "PKG ARCH") - flag.BoolVar(&race, "race", race, "Use race detector") - flag.BoolVar(&includeBuildId, "includeBuildId", includeBuildId, "IncludeBuildId in package name") - flag.BoolVar(&enterprise, "enterprise", enterprise, "Build enterprise version of Grafana") - flag.StringVar(&buildIdRaw, "buildId", "0", "Build ID from CI system") - flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps") - flag.BoolVar(&skipRpmGen, "skipRpm", skipRpmGen, "skip rpm package generation (default: false)") - flag.BoolVar(&skipDebGen, "skipDeb", skipDebGen, "skip deb package generation (default: false)") - flag.BoolVar(&printGenVersion, "gen-version", printGenVersion, "generate Grafana version and output (default: false)") - flag.Parse() - - buildId = shortenBuildId(buildIdRaw) - - readVersionFromPackageJson() - - if pkgArch == "" { - pkgArch = goarch - } - - if printGenVersion { - printGeneratedVersion() - return - } - - if len(buildTagsRaw) > 0 { - buildTags = strings.Split(buildTagsRaw, ",") - } - - log.Printf("Version: %s, Linux Version: %s, Package Iteration: %s\n", version, linuxPackageVersion, linuxPackageIteration) - - if flag.NArg() == 0 { - log.Println("Usage: go run build.go build") - return - } - - workingDir, _ = os.Getwd() - - for _, cmd := range flag.Args() { - switch cmd { - case "setup": - setup() - - case "build-srv", "build-server": - clean() - doBuild("grafana-server", "./pkg/cmd/grafana-server", buildTags) - - case "build-cli": - clean() - doBuild("grafana-cli", "./pkg/cmd/grafana-cli", buildTags) - - case "build": - //clean() - for _, binary := range binaries { - doBuild(binary, "./pkg/cmd/"+binary, buildTags) - } - - case "build-frontend": - yarn("build") - - case "sha-dist": - shaFilesInDist() - - case "latest": - makeLatestDistCopies() - - case "clean": - clean() - - default: - log.Fatalf("Unknown command %q", cmd) - } - } -} - -func makeLatestDistCopies() { - files, err := ioutil.ReadDir("dist") - if err != nil { - log.Fatalf("failed to create latest copies. Cannot read from /dist") - } - - latestMapping := map[string]string{ - "_amd64.deb": "dist/grafana_latest_amd64.deb", - ".x86_64.rpm": "dist/grafana-latest-1.x86_64.rpm", - ".linux-amd64.tar.gz": "dist/grafana-latest.linux-x64.tar.gz", - ".linux-amd64-musl.tar.gz": "dist/grafana-latest.linux-x64-musl.tar.gz", - ".linux-armv7.tar.gz": "dist/grafana-latest.linux-armv7.tar.gz", - ".linux-armv7-musl.tar.gz": "dist/grafana-latest.linux-armv7-musl.tar.gz", - ".linux-armv6.tar.gz": "dist/grafana-latest.linux-armv6.tar.gz", - ".linux-arm64.tar.gz": "dist/grafana-latest.linux-arm64.tar.gz", - ".linux-arm64-musl.tar.gz": "dist/grafana-latest.linux-arm64-musl.tar.gz", - } - - for _, file := range files { - for extension, fullName := range latestMapping { - if strings.HasSuffix(file.Name(), extension) { - runError("cp", path.Join("dist", file.Name()), fullName) - } - } - } -} - -func readVersionFromPackageJson() { - reader, err := os.Open("package.json") - if err != nil { - log.Fatal("Failed to open package.json") - return - } - defer reader.Close() - - jsonObj := map[string]interface{}{} - jsonParser := json.NewDecoder(reader) - - if err := jsonParser.Decode(&jsonObj); err != nil { - log.Fatal("Failed to decode package.json") - } - - version = jsonObj["version"].(string) - linuxPackageVersion = version - linuxPackageIteration = "" - - // handle pre version stuff (deb / rpm does not support semver) - parts := strings.Split(version, "-") - - if len(parts) > 1 { - linuxPackageVersion = parts[0] - linuxPackageIteration = parts[1] - } - - // add timestamp to iteration - if includeBuildId { - if buildId != "0" { - linuxPackageIteration = fmt.Sprintf("%s%s", buildId, linuxPackageIteration) - } else { - linuxPackageIteration = fmt.Sprintf("%d%s", time.Now().Unix(), linuxPackageIteration) - } - } -} - -func yarn(params ...string) { - runPrint(`yarn run`, params...) -} - -func genPackageVersion() string { - if includeBuildId { - return fmt.Sprintf("%v-%v", linuxPackageVersion, linuxPackageIteration) - } else { - return version - } -} - -func setup() { - args := []string{"install", "-v"} - if goos == windows { - args = append(args, "-buildmode=exe") - } - args = append(args, "./pkg/cmd/grafana-server") - runPrint("go", args...) -} - -func printGeneratedVersion() { - fmt.Print(genPackageVersion()) -} - -func test(pkg string) { - setBuildEnv() - args := []string{"test", "-short", "-timeout", "60s"} - if goos == windows { - args = append(args, "-buildmode=exe") - } - args = append(args, pkg) - runPrint("go", args...) -} - -func doBuild(binaryName, pkg string, tags []string) { - libcPart := "" - if libc != "" { - libcPart = fmt.Sprintf("-%s", libc) - } - binary := fmt.Sprintf("./bin/%s-%s%s/%s", goos, goarch, libcPart, binaryName) - if isDev { - //don't include os/arch/libc in output path in dev environment - binary = fmt.Sprintf("./bin/%s", binaryName) - } - - if goos == windows { - binary += ".exe" - } - - if !isDev { - rmr(binary, binary+".md5") - } - args := []string{"build", "-ldflags", ldflags()} - if goos == windows { - // Work around a linking error on Windows: "export ordinal too large" - args = append(args, "-buildmode=exe") - } - if len(tags) > 0 { - args = append(args, "-tags", strings.Join(tags, ",")) - } - if race { - args = append(args, "-race") - } - - args = append(args, "-o", binary) - args = append(args, pkg) - - if !isDev { - setBuildEnv() - runPrint("go", "version") - libcPart := "" - if libc != "" { - libcPart = fmt.Sprintf("/%s", libc) - } - fmt.Printf("Targeting %s/%s%s\n", goos, goarch, libcPart) - } - - runPrint("go", args...) - - if !isDev { - // Create an md5 checksum of the binary, to be included in the archive for - // automatic upgrades. - err := md5File(binary) - if err != nil { - log.Fatal(err) - } - } -} - -func ldflags() string { - var b bytes.Buffer - b.WriteString("-w") - b.WriteString(fmt.Sprintf(" -X main.version=%s", version)) - b.WriteString(fmt.Sprintf(" -X main.commit=%s", getGitSha())) - b.WriteString(fmt.Sprintf(" -X main.buildstamp=%d", buildStamp())) - b.WriteString(fmt.Sprintf(" -X main.buildBranch=%s", getGitBranch())) - if v := os.Getenv("LDFLAGS"); v != "" { - b.WriteString(fmt.Sprintf(" -extldflags \"%s\"", v)) - } - return b.String() -} - -func rmr(paths ...string) { - for _, path := range paths { - log.Println("rm -r", path) - os.RemoveAll(path) - } -} - -func clean() { - if isDev { - return - } - - rmr("dist") - rmr("tmp") - rmr(filepath.Join(build.Default.GOPATH, fmt.Sprintf("pkg/%s_%s/github.com/grafana", goos, goarch))) -} - -func setBuildEnv() { - os.Setenv("GOOS", goos) - if goos == windows { - // require windows >=7 - os.Setenv("CGO_CFLAGS", "-D_WIN32_WINNT=0x0601") - } - if goarch != "amd64" || goos != linux { - // needed for all other archs - cgo = true - } - if strings.HasPrefix(goarch, "armv") { - os.Setenv("GOARCH", "arm") - os.Setenv("GOARM", goarch[4:]) - } else { - os.Setenv("GOARCH", goarch) - } - if cgo { - os.Setenv("CGO_ENABLED", "1") - } - if gocc != "" { - os.Setenv("CC", gocc) - } -} - -func getGitBranch() string { - v, err := runError("git", "rev-parse", "--abbrev-ref", "HEAD") - if err != nil { - return "main" - } - return string(v) -} - -func getGitSha() string { - v, err := runError("git", "rev-parse", "--short", "HEAD") - if err != nil { - return "unknown-dev" - } - return string(v) -} - -func buildStamp() int64 { - // use SOURCE_DATE_EPOCH if set. - if s, _ := strconv.ParseInt(os.Getenv("SOURCE_DATE_EPOCH"), 10, 64); s > 0 { - return s - } - - bs, err := runError("git", "show", "-s", "--format=%ct") - if err != nil { - return time.Now().Unix() - } - s, _ := strconv.ParseInt(string(bs), 10, 64) - return s -} - -func runError(cmd string, args ...string) ([]byte, error) { - ecmd := exec.Command(cmd, args...) - bs, err := ecmd.CombinedOutput() - if err != nil { - return nil, err - } - - return bytes.TrimSpace(bs), nil -} - -func runPrint(cmd string, args ...string) { - log.Println(cmd, strings.Join(args, " ")) - ecmd := exec.Command(cmd, args...) - ecmd.Stdout = os.Stdout - ecmd.Stderr = os.Stderr - err := ecmd.Run() - if err != nil { - log.Fatal(err) - } -} - -func md5File(file string) error { - fd, err := os.Open(file) - if err != nil { - return err - } - defer fd.Close() - - h := md5.New() - _, err = io.Copy(h, fd) - if err != nil { - return err - } - - out, err := os.Create(file + ".md5") - if err != nil { - return err - } - - _, err = fmt.Fprintf(out, "%x\n", h.Sum(nil)) - if err != nil { - return err - } - - return out.Close() -} - -func shaFilesInDist() { - filepath.Walk("./dist", func(path string, f os.FileInfo, err error) error { - if path == "./dist" { - return nil - } - - if !strings.Contains(path, ".sha256") { - err := shaFile(path) - if err != nil { - log.Printf("Failed to create sha file. error: %v\n", err) - } - } - return nil - }) -} - -func shaFile(file string) error { - fd, err := os.Open(file) - if err != nil { - return err - } - defer fd.Close() - - h := sha256.New() - _, err = io.Copy(h, fd) - if err != nil { - return err - } - - out, err := os.Create(file + ".sha256") - if err != nil { - return err - } - - _, err = fmt.Fprintf(out, "%x\n", h.Sum(nil)) - if err != nil { - return err - } - - return out.Close() -} - -func shortenBuildId(buildId string) string { - buildId = strings.Replace(buildId, "-", "", -1) - if len(buildId) < 9 { - return buildId - } - return buildId[0:8] + os.Exit(build.RunCmd()) } diff --git a/pkg/build/cmd.go b/pkg/build/cmd.go new file mode 100644 index 00000000000..b70dde154cb --- /dev/null +++ b/pkg/build/cmd.go @@ -0,0 +1,314 @@ +package build + +import ( + "bytes" + "flag" + "fmt" + "go/build" + "io/ioutil" + "log" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "time" +) + +const ( + GoOSWindows = "windows" + GoOSLinux = "linux" + + ServerBinary = "grafana-server" + CLIBinary = "grafana-cli" +) + +var binaries = []string{ServerBinary, CLIBinary} + +func logError(message string, err error) int { + log.Println(message, err) + + return 1 +} + +// RunCmd runs the build command and returns the exit code +func RunCmd() int { + opts := BuildOptsFromFlags() + + wd, err := os.Getwd() + if err != nil { + return logError("Error getting working directory", err) + } + + packageJSON, err := OpenPackageJSON(wd) + if err != nil { + return logError("Error opening package json", err) + } + + version, iteration := LinuxPackageVersion(packageJSON.Version, opts.buildID) + + if opts.printGenVersion { + fmt.Print(genPackageVersion(version, iteration)) + return 0 + } + + log.Printf("Version: %s, Linux Version: %s, Package Iteration: %s\n", version, version, iteration) + + if flag.NArg() == 0 { + log.Println("Usage: go run build.go build") + return 1 + } + + for _, cmd := range flag.Args() { + switch cmd { + case "setup": + setup(opts.goos) + + case "build-srv", "build-server": + if !opts.isDev { + clean(opts) + } + + if err := doBuild("grafana-server", "./pkg/cmd/grafana-server", opts); err != nil { + log.Println(err) + return 1 + } + + case "build-cli": + clean(opts) + if err := doBuild("grafana-cli", "./pkg/cmd/grafana-cli", opts); err != nil { + log.Println(err) + return 1 + } + + case "build": + //clean() + for _, binary := range binaries { + log.Println("building binaries", cmd) + // Can't use filepath.Join here because filepath.Join calls filepath.Clean, which removes the `./` from this path, which upsets `go build` + if err := doBuild(binary, fmt.Sprintf("./pkg/cmd/%s", binary), opts); err != nil { + log.Println(err) + return 1 + } + } + + case "build-frontend": + yarn("build") + + case "sha-dist": + if err := shaDir("dist"); err != nil { + return logError("error packaging dist directory", err) + } + + case "latest": + makeLatestDistCopies() + + case "clean": + clean(opts) + + default: + log.Println("Unknown command", cmd) + return 1 + } + } + + return 0 +} + +func makeLatestDistCopies() { + files, err := ioutil.ReadDir("dist") + if err != nil { + log.Fatalf("failed to create latest copies. Cannot read from /dist") + } + + latestMapping := map[string]string{ + "_amd64.deb": "dist/grafana_latest_amd64.deb", + ".x86_64.rpm": "dist/grafana-latest-1.x86_64.rpm", + ".linux-amd64.tar.gz": "dist/grafana-latest.linux-x64.tar.gz", + ".linux-amd64-musl.tar.gz": "dist/grafana-latest.linux-x64-musl.tar.gz", + ".linux-armv7.tar.gz": "dist/grafana-latest.linux-armv7.tar.gz", + ".linux-armv7-musl.tar.gz": "dist/grafana-latest.linux-armv7-musl.tar.gz", + ".linux-armv6.tar.gz": "dist/grafana-latest.linux-armv6.tar.gz", + ".linux-arm64.tar.gz": "dist/grafana-latest.linux-arm64.tar.gz", + ".linux-arm64-musl.tar.gz": "dist/grafana-latest.linux-arm64-musl.tar.gz", + } + + for _, file := range files { + for extension, fullName := range latestMapping { + if strings.HasSuffix(file.Name(), extension) { + if _, err := runError("cp", path.Join("dist", file.Name()), fullName); err != nil { + log.Println("error running cp command:", err) + } + } + } + } +} + +func yarn(params ...string) { + runPrint(`yarn run`, params...) +} + +func genPackageVersion(version string, iteration string) string { + if iteration != "" { + return fmt.Sprintf("%v-%v", version, iteration) + } else { + return version + } +} + +func setup(goos string) { + args := []string{"install", "-v"} + if goos == GoOSWindows { + args = append(args, "-buildmode=exe") + } + args = append(args, "./pkg/cmd/grafana-server") + runPrint("go", args...) +} + +func doBuild(binaryName, pkg string, opts BuildOpts) error { + log.Println("building", binaryName, pkg) + libcPart := "" + if opts.libc != "" { + libcPart = fmt.Sprintf("-%s", opts.libc) + } + binary := fmt.Sprintf("./bin/%s", binaryName) + + //don't include os/arch/libc in output path in dev environment + if !opts.isDev { + binary = fmt.Sprintf("./bin/%s-%s%s/%s", opts.goos, opts.goarch, libcPart, binaryName) + } + + if opts.goos == GoOSWindows { + binary += ".exe" + } + + if !opts.isDev { + rmr(binary, binary+".md5") + } + + lf, err := ldflags(opts) + if err != nil { + return err + } + + args := []string{"build", "-ldflags", lf} + + if opts.goos == GoOSWindows { + // Work around a linking error on Windows: "export ordinal too large" + args = append(args, "-buildmode=exe") + } + + if len(opts.buildTags) > 0 { + args = append(args, "-tags", strings.Join(opts.buildTags, ",")) + } + + if opts.race { + args = append(args, "-race") + } + + args = append(args, "-o", binary) + args = append(args, pkg) + + runPrint("go", args...) + + if opts.isDev { + return nil + } + + if err := setBuildEnv(opts); err != nil { + return err + } + runPrint("go", "version") + libcPart = "" + if opts.libc != "" { + libcPart = fmt.Sprintf("/%s", opts.libc) + } + fmt.Printf("Targeting %s/%s%s\n", opts.goos, opts.goarch, libcPart) + + // Create an md5 checksum of the binary, to be included in the archive for + // automatic upgrades. + return md5File(binary) +} + +func ldflags(opts BuildOpts) (string, error) { + buildStamp, err := buildStamp() + if err != nil { + return "", err + } + + var b bytes.Buffer + b.WriteString("-w") + b.WriteString(fmt.Sprintf(" -X main.version=%s", opts.version)) + b.WriteString(fmt.Sprintf(" -X main.commit=%s", getGitSha())) + b.WriteString(fmt.Sprintf(" -X main.buildstamp=%d", buildStamp)) + b.WriteString(fmt.Sprintf(" -X main.buildBranch=%s", getGitBranch())) + if v := os.Getenv("LDFLAGS"); v != "" { + b.WriteString(fmt.Sprintf(" -extldflags \"%s\"", v)) + } + + return b.String(), nil +} + +func setBuildEnv(opts BuildOpts) error { + if err := os.Setenv("GOOS", opts.goos); err != nil { + return err + } + + if opts.goos == GoOSWindows { + // require windows >=7 + if err := os.Setenv("CGO_CFLAGS", "-D_WIN32_WINNT=0x0601"); err != nil { + return err + } + } + + if opts.goarch != "amd64" || opts.goos != GoOSLinux { + // needed for all other archs + opts.cgo = true + } + + if strings.HasPrefix(opts.goarch, "armv") { + if err := os.Setenv("GOARCH", "arm"); err != nil { + return err + } + + if err := os.Setenv("GOARM", opts.goarch[4:]); err != nil { + return err + } + } else { + if err := os.Setenv("GOARCH", opts.goarch); err != nil { + return err + } + } + + if opts.cgo { + if err := os.Setenv("CGO_ENABLED", "1"); err != nil { + return err + } + } + + if opts.gocc == "" { + return nil + } + + return os.Setenv("CC", opts.gocc) +} + +func buildStamp() (int64, error) { + // use SOURCE_DATE_EPOCH if set. + if v, ok := os.LookupEnv("SOURCE_DATE_EPOCH"); ok { + return strconv.ParseInt(v, 10, 64) + } + + bs, err := runError("git", "show", "-s", "--format=%ct") + if err != nil { + return time.Now().Unix(), nil + } + + return strconv.ParseInt(string(bs), 10, 64) +} + +func clean(opts BuildOpts) { + rmr("dist") + rmr("tmp") + rmr(filepath.Join(build.Default.GOPATH, fmt.Sprintf("pkg/%s_%s/github.com/grafana", opts.goos, opts.goarch))) +} diff --git a/pkg/build/docs.go b/pkg/build/docs.go new file mode 100644 index 00000000000..ca9549e8799 --- /dev/null +++ b/pkg/build/docs.go @@ -0,0 +1,2 @@ +// Package build contains the command / functions for the Grafana build process used when running the "build" target in the makefile +package build diff --git a/pkg/build/exec.go b/pkg/build/exec.go new file mode 100644 index 00000000000..ad13eb5ba99 --- /dev/null +++ b/pkg/build/exec.go @@ -0,0 +1,34 @@ +package build + +import ( + "bytes" + "log" + "os" + "os/exec" + "strings" +) + +func runError(cmd string, args ...string) ([]byte, error) { + // Can ignore gosec G204 because this function is not used in Grafana, only in the build process. + //nolint:gosec + ecmd := exec.Command(cmd, args...) + bs, err := ecmd.CombinedOutput() + if err != nil { + return nil, err + } + + return bytes.TrimSpace(bs), nil +} + +func runPrint(cmd string, args ...string) { + log.Println(cmd, strings.Join(args, " ")) + // Can ignore gosec G204 because this function is not used in Grafana, only in the build process. + //nolint:gosec + ecmd := exec.Command(cmd, args...) + ecmd.Stdout = os.Stdout + ecmd.Stderr = os.Stderr + err := ecmd.Run() + if err != nil { + log.Fatal(err) + } +} diff --git a/pkg/build/fs.go b/pkg/build/fs.go new file mode 100644 index 00000000000..fe1eaba2577 --- /dev/null +++ b/pkg/build/fs.go @@ -0,0 +1,101 @@ +package build + +import ( + "crypto/md5" + "crypto/sha256" + "fmt" + "io" + "log" + "os" + "path/filepath" + "strings" +) + +func logAndClose(c io.Closer) { + if err := c.Close(); err != nil { + log.Println("error closing:", err) + } +} + +func shaDir(dir string) error { + return filepath.Walk(dir, func(path string, f os.FileInfo, err error) error { + if path == dir { + return nil + } + + if strings.Contains(path, ".sha256") { + return nil + } + if err := shaFile(path); err != nil { + log.Printf("Failed to create sha file. error: %v\n", err) + } + return nil + }) +} + +func shaFile(file string) error { + // Can ignore gosec G304 because this function is not used in Grafana, only in the build process. + //nolint:gosec + r, err := os.Open(file) + if err != nil { + return err + } + + defer logAndClose(r) + + h := sha256.New() + _, err = io.Copy(h, r) + if err != nil { + return err + } + + out, err := os.Create(file + ".sha256") + if err != nil { + return err + } + + _, err = fmt.Fprintf(out, "%x\n", h.Sum(nil)) + if err != nil { + return err + } + + return out.Close() +} + +func md5File(file string) error { + // Can ignore gosec G304 because this function is not used in Grafana, only in the build process. + //nolint:gosec + fd, err := os.Open(file) + if err != nil { + return err + } + defer logAndClose(fd) + + h := md5.New() + _, err = io.Copy(h, fd) + if err != nil { + return err + } + + out, err := os.Create(file + ".md5") + if err != nil { + return err + } + + _, err = fmt.Fprintf(out, "%x\n", h.Sum(nil)) + if err != nil { + return err + } + + return out.Close() +} + +// basically `rm -r`s the list of files provided +func rmr(paths ...string) { + for _, path := range paths { + log.Println("rm -r", path) + if err := os.RemoveAll(path); err != nil { + log.Println("error deleting folder", path, "error:", err) + } + } +} diff --git a/pkg/build/git.go b/pkg/build/git.go new file mode 100644 index 00000000000..6c86d74e004 --- /dev/null +++ b/pkg/build/git.go @@ -0,0 +1,17 @@ +package build + +func getGitBranch() string { + v, err := runError("git", "rev-parse", "--abbrev-ref", "HEAD") + if err != nil { + return "main" + } + return string(v) +} + +func getGitSha() string { + v, err := runError("git", "rev-parse", "--short", "HEAD") + if err != nil { + return "unknown-dev" + } + return string(v) +} diff --git a/pkg/build/opts.go b/pkg/build/opts.go new file mode 100644 index 00000000000..8e5486d22d1 --- /dev/null +++ b/pkg/build/opts.go @@ -0,0 +1,66 @@ +package build + +import ( + "flag" + "runtime" + "strings" +) + +// BuildOpts are options provided to the build step +type BuildOpts struct { + goarch string + goos string + gocc string + cgo bool + libc string + + pkgArch string + version string + buildTags []string + // deb & rpm does not support semver so have to handle their version a little differently + race bool + includeBuildID bool + buildID string + isDev bool + enterprise bool + skipRpmGen bool + skipDebGen bool + printGenVersion bool +} + +// BuildOptsFromFlags reads the cmd args to assemble a BuildOpts object. This function calls flag.Parse() +func BuildOptsFromFlags() BuildOpts { + opts := BuildOpts{} + + var buildIDRaw string + var buildTagsRaw string + + flag.StringVar(&opts.goarch, "goarch", runtime.GOARCH, "GOARCH") + flag.StringVar(&opts.goos, "goos", runtime.GOOS, "GOOS") + flag.StringVar(&opts.gocc, "cc", "", "CC") + flag.StringVar(&opts.libc, "libc", "", "LIBC") + flag.StringVar(&buildTagsRaw, "build-tags", "", "Sets custom build tags") + flag.BoolVar(&opts.cgo, "cgo-enabled", false, "Enable cgo") + flag.StringVar(&opts.pkgArch, "pkg-arch", "", "PKG ARCH") + flag.BoolVar(&opts.race, "race", false, "Use race detector") + flag.BoolVar(&opts.includeBuildID, "includeBuildID", true, "IncludeBuildID in package name") + flag.BoolVar(&opts.enterprise, "enterprise", false, "Build enterprise version of Grafana") + flag.StringVar(&buildIDRaw, "buildID", "0", "Build ID from CI system") + flag.BoolVar(&opts.isDev, "dev", false, "optimal for development, skips certain steps") + flag.BoolVar(&opts.skipRpmGen, "skipRpm", false, "skip rpm package generation (default: false)") + flag.BoolVar(&opts.skipDebGen, "skipDeb", false, "skip deb package generation (default: false)") + flag.BoolVar(&opts.printGenVersion, "gen-version", false, "generate Grafana version and output (default: false)") + flag.Parse() + + opts.buildID = shortenBuildID(buildIDRaw) + + if len(buildTagsRaw) > 0 { + opts.buildTags = strings.Split(buildTagsRaw, ",") + } + + if opts.pkgArch == "" { + opts.pkgArch = opts.goarch + } + + return opts +} diff --git a/pkg/build/version.go b/pkg/build/version.go new file mode 100644 index 00000000000..9ecb6b5b972 --- /dev/null +++ b/pkg/build/version.go @@ -0,0 +1,66 @@ +package build + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "time" +) + +type PackageJSON struct { + Version string `json:"version"` +} + +// Opens the package.json file in the provided directory and returns a struct that represents its contents +func OpenPackageJSON(dir string) (PackageJSON, error) { + reader, err := os.Open("package.json") + if err != nil { + return PackageJSON{}, err + } + + defer logAndClose(reader) + + jsonObj := PackageJSON{} + if err := json.NewDecoder(reader).Decode(&jsonObj); err != nil { + return PackageJSON{}, err + } + + return jsonObj, nil +} + +// LinuxPackageVersion extracts the linux package version and iteration out of the version string. The version string is likely extracted from the package JSON. +func LinuxPackageVersion(v string, buildID string) (string, string) { + var ( + version = v + iteration = "" + ) + + // handle pre version stuff (deb / rpm does not support semver) + parts := strings.Split(v, "-") + + if len(parts) > 1 { + version = parts[0] + iteration = parts[1] + } + + if buildID == "" { + return version, iteration + } + + // add timestamp to iteration + if buildID != "0" { + iteration = strings.Join([]string{buildID, iteration}, "") + return version, iteration + } + + return version, fmt.Sprintf("%d%s", time.Now().Unix(), iteration) +} + +func shortenBuildID(buildID string) string { + buildID = strings.Replace(buildID, "-", "", -1) + if len(buildID) < 9 { + return buildID + } + return buildID[0:8] +} From 3b0910e4705a22c24117ad6eca7c89dd993ec39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 6 Sep 2021 05:57:01 +0200 Subject: [PATCH 006/132] LibraryPanels: Prevents duplicate repeated panels from being created (#38804) * LibraryPanels: Prevents duplicate repeated panels from being created * Tests: adds tests * Chore: updates after PR feedback --- .../PanelEditor/state/actions.test.ts | 44 +++++++++- .../components/PanelEditor/state/actions.ts | 84 +++++++++++++------ 2 files changed, 100 insertions(+), 28 deletions(-) diff --git a/public/app/features/dashboard/components/PanelEditor/state/actions.test.ts b/public/app/features/dashboard/components/PanelEditor/state/actions.test.ts index 8669f5c9dda..d142187c601 100644 --- a/public/app/features/dashboard/components/PanelEditor/state/actions.test.ts +++ b/public/app/features/dashboard/components/PanelEditor/state/actions.test.ts @@ -1,6 +1,6 @@ import { thunkTester } from '../../../../../../test/core/thunk/thunkTester'; import { closeEditor, initialState, PanelEditorState } from './reducers'; -import { initPanelEditor, exitPanelEditor } from './actions'; +import { exitPanelEditor, initPanelEditor, skipPanelUpdate } from './actions'; import { cleanUpEditPanel, panelModelAndPluginReady } from '../../../state/reducers'; import { DashboardModel, PanelModel } from '../../../state'; import { getPanelPlugin } from 'app/features/plugins/__mocks__/pluginMocks'; @@ -125,4 +125,46 @@ describe('panelEditor actions', () => { expect(sourcePanel.getOptions()).toEqual({}); }); }); + + describe('skipPanelUpdate', () => { + describe('when called with panel with an library uid different from the modified panel', () => { + it('then it should return true', () => { + const meta: any = {}; + const modified: any = { libraryPanel: { uid: '123', name: 'Name', meta, version: 1 } }; + const panel: any = { libraryPanel: { uid: '456', name: 'Name', meta, version: 1 } }; + + expect(skipPanelUpdate(modified, panel)).toEqual(true); + }); + }); + + describe('when called with a panel that is the same as the modified panel', () => { + it('then it should return true', () => { + const meta: any = {}; + const modified: any = { editSourceId: 14, libraryPanel: { uid: '123', name: 'Name', meta, version: 1 } }; + const panel: any = { id: 14, libraryPanel: { uid: '123', name: 'Name', meta, version: 1 } }; + + expect(skipPanelUpdate(modified, panel)).toEqual(true); + }); + }); + + describe('when called with a panel that is repeated', () => { + it('then it should return true', () => { + const meta: any = {}; + const modified: any = { libraryPanel: { uid: '123', name: 'Name', meta, version: 1 } }; + const panel: any = { repeatPanelId: 14, libraryPanel: { uid: '123', name: 'Name', meta, version: 1 } }; + + expect(skipPanelUpdate(modified, panel)).toEqual(true); + }); + }); + + describe('when called with a panel that is a duplicate of the modified panel', () => { + it('then it should return false', () => { + const meta: any = {}; + const modified: any = { libraryPanel: { uid: '123', name: 'Name', meta, version: 1 } }; + const panel: any = { libraryPanel: { uid: '123', name: 'Name', meta, version: 1 } }; + + expect(skipPanelUpdate(modified, panel)).toEqual(false); + }); + }); + }); }); diff --git a/public/app/features/dashboard/components/PanelEditor/state/actions.ts b/public/app/features/dashboard/components/PanelEditor/state/actions.ts index 99373832015..9535e3c907e 100644 --- a/public/app/features/dashboard/components/PanelEditor/state/actions.ts +++ b/public/app/features/dashboard/components/PanelEditor/state/actions.ts @@ -4,9 +4,9 @@ import { closeEditor, PANEL_EDITOR_UI_STATE_STORAGE_KEY, PanelEditorUIState, + setDiscardChanges, setPanelEditorUIState, updateEditorInitState, - setDiscardChanges, } from './reducers'; import { cleanUpEditPanel, panelModelAndPluginReady } from '../../../state/reducers'; import store from 'app/core/store'; @@ -33,38 +33,68 @@ export function discardPanelChanges(): ThunkResult { }; } -function updateDuplicateLibraryPanels(modifiedPanel: PanelModel, dashboard: DashboardModel | null, dispatch: any) { - if (modifiedPanel.libraryPanel?.uid === undefined || !dashboard) { - return; - } - - const modifiedSaveModel = modifiedPanel.getSaveModel(); - for (const panel of dashboard.panels) { - if (panel.libraryPanel?.uid !== modifiedPanel.libraryPanel!.uid) { - continue; +export function updateDuplicateLibraryPanels( + modifiedPanel: PanelModel, + dashboard: DashboardModel | null +): ThunkResult { + return (dispatch) => { + if (modifiedPanel.libraryPanel?.uid === undefined || !dashboard) { + return; } - panel.restoreModel({ - ...modifiedSaveModel, - ...pick(panel, 'gridPos', 'id'), - }); + const modifiedSaveModel = modifiedPanel.getSaveModel(); + for (const panel of dashboard.panels) { + if (skipPanelUpdate(modifiedPanel, panel)) { + continue; + } - // Loaded plugin is not included in the persisted properties - // So is not handled by restoreModel - const pluginChanged = panel.plugin?.meta.id !== modifiedPanel.plugin?.meta.id; - panel.plugin = modifiedPanel.plugin; - panel.configRev++; + panel.restoreModel({ + ...modifiedSaveModel, + ...pick(panel, 'gridPos', 'id'), + }); - if (pluginChanged) { - dispatch(panelModelAndPluginReady({ panelId: panel.id, plugin: panel.plugin! })); + // Loaded plugin is not included in the persisted properties + // So is not handled by restoreModel + const pluginChanged = panel.plugin?.meta.id !== modifiedPanel.plugin?.meta.id; + panel.plugin = modifiedPanel.plugin; + panel.configRev++; + + if (pluginChanged) { + dispatch(panelModelAndPluginReady({ panelId: panel.id, plugin: panel.plugin! })); + } + + // Resend last query result on source panel query runner + // But do this after the panel edit editor exit process has completed + setTimeout(() => { + panel.getQueryRunner().useLastResultFrom(modifiedPanel.getQueryRunner()); + }, 20); } - // Resend last query result on source panel query runner - // But do this after the panel edit editor exit process has completed - setTimeout(() => { - panel.getQueryRunner().useLastResultFrom(modifiedPanel.getQueryRunner()); - }, 20); + if (modifiedPanel.repeat) { + // We skip any repeated library panels so we need to update them by calling processRepeats + // But do this after the panel edit editor exit process has completed + setTimeout(() => dashboard.processRepeats(), 20); + } + }; +} + +export function skipPanelUpdate(modifiedPanel: PanelModel, panelToUpdate: PanelModel): boolean { + // don't update library panels that aren't of the same type + if (panelToUpdate.libraryPanel?.uid !== modifiedPanel.libraryPanel!.uid) { + return true; } + + // don't update the modifiedPanel twice + if (panelToUpdate.id && panelToUpdate.id === modifiedPanel.editSourceId) { + return true; + } + + // don't update library panels that are repeated + if (panelToUpdate.repeatPanelId) { + return true; + } + + return false; } export function exitPanelEditor(): ThunkResult { @@ -78,7 +108,7 @@ export function exitPanelEditor(): ThunkResult { const sourcePanel = getSourcePanel(); const panelTypeChanged = sourcePanel.type !== panel.type; - updateDuplicateLibraryPanels(panel, dashboard, dispatch); + dispatch(updateDuplicateLibraryPanels(panel, dashboard)); // restore the source panel ID before we update source panel modifiedSaveModel.id = sourcePanel.id; From 774f0a0d016fa6f32e51259e38f8484a053e9aef Mon Sep 17 00:00:00 2001 From: Uchechukwu Obasi Date: Mon, 6 Sep 2021 07:03:02 +0100 Subject: [PATCH 007/132] e2e guide: improves data-testid code snippet to be clear and understandable (#38791) --- contribute/style-guides/e2e.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contribute/style-guides/e2e.md b/contribute/style-guides/e2e.md index 1e4961975ce..a603abaa2af 100644 --- a/contribute/style-guides/e2e.md +++ b/contribute/style-guides/e2e.md @@ -178,8 +178,8 @@ Prefix your selector string with "data-testid": ```typescript export const Components = { Login: { - openButton: 'data-testid-open', // this would look for a data-testid - closeButton: 'close-button', // this would look for an aria-label + openButton: 'open-button', // this would look for an aria-label + closeButton: 'data-testid modal-close-button', // this would look for a data-testid }, }; ``` @@ -187,5 +187,5 @@ export const Components = { and in your component, import the selectors and add the data test id: ``` -