diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 0000000000..390ec4216f --- /dev/null +++ b/.codespellrc @@ -0,0 +1,7 @@ +[codespell] +# js/ folder seems to contain vendored code +skip = .git,*.pdf,*.svg,*.min.js,*.js.map,js,requirements.txt +# docs/videos/healthcare-and-life-sciences/lysozyme-example/submit.sh has CONECT - might be "legit" +ignore-regex = HETATM -e CONECT|\([A-Z]\)[a-z]+ +# +ignore-words-list = namd,tempdate,te,ue,startd,passtime diff --git a/.github/workflows/pr-label-validation.yml b/.github/workflows/pr-label-validation.yml index 880591ff86..3d7667c615 100644 --- a/.github/workflows/pr-label-validation.yml +++ b/.github/workflows/pr-label-validation.yml @@ -43,6 +43,13 @@ jobs: count: 1 labels: "release-chore, release-key-new-features, release-new-modules, release-module-improvements, release-improvements, release-deprecations, release-version-updates, release-bugfix" message: "This PR is being prevented from merging because it is not labeled. Please add a label to this PR. Accepted labels: release-chore, release-key-new-features, release-new-modules, release-module-improvements, release-improvements, release-deprecations, release-version-updates, release-bugfix" + - id: do-not-merge + uses: mheap/github-action-required-labels@v5 + with: + mode: exactly + count: 0 + labels: "do-not-merge" + add_comment: false - id: print-labels run: | echo "Current PR labels:" diff --git a/.gitignore b/.gitignore index 29937edb09..d8853e6f8c 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,9 @@ packer-manifest.json #### Python Virtual Environments venv*/ +### Python cache files +**/__pycache__ + #### Exclude from gitingore !tools/validate_configs/golden_copies/expectations/*/*/*/defaults.auto.pkrvars.hcl !tools/validate_configs/golden_copies/expectations/*/*/terraform.tfvars diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eb274842ee..f7e9acd152 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,6 +39,7 @@ repos: entry: tools/autodoc/terraform_docs.sh language: script files: ^.*\.pkr\.hcl$ + exclude: (pkg\/.*$)|(tools/validate_configs/golden_copies/.*) pass_filenames: true require_serial: true - id: duplicate-diff @@ -57,6 +58,14 @@ repos: files: '.*\.tf$' pass_filenames: false require_serial: true + - id: addlicense + name: addlicense + language: system + entry: addlicense + args: ['-c', '"Google LLC"', '-l', 'apache'] + exclude: docs/videos/healthcare-and-life-sciences/lysozyme-example/submit.sh + pass_filenames: true + - repo: https://github.com/dnephin/pre-commit-golang rev: v0.5.1 hooks: @@ -64,15 +73,14 @@ repos: - id: go-vet - id: go-imports - id: go-cyclo - args: [-over=15] + args: [-over=13] - id: go-unit-tests - - id: go-build - id: go-mod-tidy -- repo: https://github.com/tekwizely/pre-commit-golang - rev: v1.0.0-rc.1 - hooks: - - id: go-critic - args: [-disable, "#experimental,sloppyTypeAssert"] +# Disabled temporarily due to https://github.com/go-critic/go-critic/issues/1388 +# - repo: https://github.com/tekwizely/pre-commit-golang +# rev: v1.0.0-rc.1 +# hooks: +# - id: go-critic - repo: https://github.com/Bahjat/pre-commit-golang rev: v1.0.2 hooks: @@ -105,4 +113,9 @@ repos: rev: v4.4.0 hooks: - id: end-of-file-fixer +- repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + exclude: requirements.txt$|/js/|go.sum$ exclude: tools/validate_configs/golden_copies/.* diff --git a/.tflint.hcl b/.tflint.hcl index e6ede24f8c..2acf4ee3cb 100644 --- a/.tflint.hcl +++ b/.tflint.hcl @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + plugin "google" { enabled = true version = "0.26.0" diff --git a/Makefile b/Makefile index 85c06d077a..4efab7a3ac 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ MIN_PACKER_VERSION=1.7.9 # for building images MIN_TERRAFORM_VERSION=1.2 # for deploying modules MIN_GOLANG_VERSION=1.18 # for building ghpc -.PHONY: install install-user tests format add-google-license install-dev-deps \ +.PHONY: install install-user tests format install-dev-deps \ warn-go-missing warn-terraform-missing warn-packer-missing \ warn-go-version warn-terraform-version warn-packer-version \ test-engine validate_configs validate_golden_copy packer-check \ @@ -65,15 +65,6 @@ install-dev-deps: warn-terraform-version warn-packer-version check-pre-commit ch go install golang.org/x/tools/cmd/goimports@latest go install honnef.co/go/tools/cmd/staticcheck@latest -ifeq (, $(shell which addlicense)) -add-google-license: - $(error "could not find addlicense in PATH, run: go install github.com/google/addlicense@latest") -else -add-google-license: - # lysozyme-example is under CC-BY-4.0 - addlicense -c "Google LLC" -l apache -ignore **/lysozyme-example/submit.sh . -endif - # RULES SUPPORTING THE ABOVE test-engine: warn-go-missing diff --git a/cmd/create.go b/cmd/create.go index 51314f6376..21ff70dbbe 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -21,9 +21,10 @@ import ( "errors" "fmt" "hpc-toolkit/pkg/config" + "hpc-toolkit/pkg/logging" "hpc-toolkit/pkg/modulewriter" "hpc-toolkit/pkg/validators" - "log" + "os" "path/filepath" "strings" @@ -78,51 +79,47 @@ var ( func runCreateCmd(cmd *cobra.Command, args []string) { dc := expandOrDie(args[0]) - deplName, err := dc.Config.DeploymentName() - checkErr(err) - deplDir := filepath.Join(outputDir, deplName) - checkErr(modulewriter.WriteDeployment(dc, deplDir, overwriteDeployment)) - - fmt.Println("To deploy your infrastructure please run:") - fmt.Println() - fmt.Printf(boldGreen("%s deploy %s\n"), execPath(), deplDir) - fmt.Println() + deplDir := filepath.Join(outputDir, dc.Config.DeploymentName()) + checkErr(checkOverwriteAllowed(deplDir, dc.Config, overwriteDeployment)) + checkErr(modulewriter.WriteDeployment(dc, deplDir)) + + logging.Info("To deploy your infrastructure please run:") + logging.Info("") + logging.Info(boldGreen("%s deploy %s"), execPath(), deplDir) + logging.Info("") printAdvancedInstructionsMessage(deplDir) } func printAdvancedInstructionsMessage(deplDir string) { - fmt.Println("Find instructions for cleanly destroying infrastructure and advanced manual") - fmt.Println("deployment instructions at:") - fmt.Println() - fmt.Printf("%s\n", modulewriter.InstructionsPath(deplDir)) + logging.Info("Find instructions for cleanly destroying infrastructure and advanced manual") + logging.Info("deployment instructions at:") + logging.Info("") + logging.Info(modulewriter.InstructionsPath(deplDir)) } func expandOrDie(path string) config.DeploymentConfig { dc, ctx, err := config.NewDeploymentConfig(path) if err != nil { - log.Fatal(renderError(err, ctx)) + logging.Fatal(renderError(err, ctx)) } // Set properties from CLI if err := setCLIVariables(&dc.Config, cliVariables); err != nil { - log.Fatalf("Failed to set the variables at CLI: %v", err) + logging.Fatal("Failed to set the variables at CLI: %v", err) } if err := setBackendConfig(&dc.Config, cliBEConfigVars); err != nil { - log.Fatalf("Failed to set the backend config at CLI: %v", err) - } - if err := setValidationLevel(&dc.Config, validationLevel); err != nil { - log.Fatal(err) - } - if err := skipValidators(&dc); err != nil { - log.Fatal(err) + logging.Fatal("Failed to set the backend config at CLI: %v", err) } + checkErr(setValidationLevel(&dc.Config, validationLevel)) + checkErr(skipValidators(&dc)) + if dc.Config.GhpcVersion != "" { - fmt.Printf("ghpc_version setting is ignored.") + logging.Info("ghpc_version setting is ignored.") } dc.Config.GhpcVersion = GitCommitInfo // Expand the blueprint if err := dc.ExpandConfig(); err != nil { - log.Fatal(renderError(err, ctx)) + logging.Fatal(renderError(err, ctx)) } validateMaybeDie(dc.Config, ctx) @@ -134,29 +131,29 @@ func validateMaybeDie(bp config.Blueprint, ctx config.YamlCtx) { if err == nil { return } - log.Println(renderError(err, ctx)) - - log.Println("One or more blueprint validators has failed. See messages above for suggested") - log.Println("actions. General troubleshooting guidance and instructions for configuring") - log.Println("validators are shown below.") - log.Println("") - log.Println("- https://goo.gle/hpc-toolkit-troubleshooting") - log.Println("- https://goo.gle/hpc-toolkit-validation") - log.Println("") - log.Println("Validators can be silenced or treated as warnings or errors:") - log.Println("") - log.Println("- https://goo.gle/hpc-toolkit-validation-levels") - log.Println("") + logging.Error(renderError(err, ctx)) + + logging.Error("One or more blueprint validators has failed. See messages above for suggested") + logging.Error("actions. General troubleshooting guidance and instructions for configuring") + logging.Error("validators are shown below.") + logging.Error("") + logging.Error("- https://goo.gle/hpc-toolkit-troubleshooting") + logging.Error("- https://goo.gle/hpc-toolkit-validation") + logging.Error("") + logging.Error("Validators can be silenced or treated as warnings or errors:") + logging.Error("") + logging.Error("- https://goo.gle/hpc-toolkit-validation-levels") + logging.Error("") switch bp.ValidationLevel { case config.ValidationWarning: { - log.Println(boldYellow("Validation failures were treated as a warning, continuing to create blueprint.")) - log.Println("") + logging.Error(boldYellow("Validation failures were treated as a warning, continuing to create blueprint.")) + logging.Error("") } case config.ValidationError: { - log.Fatal(boldRed("validation failed due to the issues listed above")) + logging.Fatal(boldRed("validation failed due to the issues listed above")) } } @@ -289,3 +286,46 @@ func filterYaml(cmd *cobra.Command, args []string, toComplete string) ([]string, } return []string{"yaml", "yml"}, cobra.ShellCompDirectiveFilterFileExt } + +// Determines if overwrite is allowed +func checkOverwriteAllowed(depDir string, bp config.Blueprint, overwriteFlag bool) error { + if _, err := os.Stat(depDir); os.IsNotExist(err) { + return nil // all good, no previous deployment + } + + if _, err := os.Stat(modulewriter.HiddenGhpcDir(depDir)); os.IsNotExist(err) { + // hidden ghpc dir does not exist + return fmt.Errorf("folder %q already exists, and it is not a valid GHPC deployment folder", depDir) + } + + // try to get previous deployment + expPath := filepath.Join(modulewriter.ArtifactsDir(depDir), modulewriter.ExpandedBlueprintName) + if _, err := os.Stat(expPath); os.IsNotExist(err) { + return fmt.Errorf("expanded blueprint file %q is missing, this could be a result of changing GHPC version between consecutive deployments", expPath) + } + prev, _, err := config.NewDeploymentConfig(expPath) + if err != nil { + return err + } + + if prev.Config.GhpcVersion != bp.GhpcVersion { + logging.Info("WARNING: ghpc_version has changed from %q to %q, using different versions of GHPC to update a live deployment is not officially supported. Proceed at your own risk", prev.Config.GhpcVersion, bp.GhpcVersion) + } + + if !overwriteFlag { + return fmt.Errorf("deployment folder %q already exists, use -w to overwrite", depDir) + } + + newGroups := map[config.GroupName]bool{} + for _, g := range bp.DeploymentGroups { + newGroups[g.Name] = true + } + + for _, g := range prev.Config.DeploymentGroups { + if !newGroups[g.Name] { + return fmt.Errorf("you are attempting to remove a deployment group %q, which is not supported", g.Name) + } + } + + return nil +} diff --git a/cmd/create_test.go b/cmd/create_test.go index a5127b1de6..94454fe26a 100644 --- a/cmd/create_test.go +++ b/cmd/create_test.go @@ -17,6 +17,9 @@ package cmd import ( "errors" "hpc-toolkit/pkg/config" + "hpc-toolkit/pkg/modulewriter" + "os" + "path/filepath" "github.com/zclconf/go-cty/cty" . "gopkg.in/check.v1" @@ -164,3 +167,76 @@ func (s *MySuite) TestValidateMaybeDie(c *C) { ctx, _ := config.NewYamlCtx([]byte{}) validateMaybeDie(bp, ctx) // smoke test } + +func (s *MySuite) TestIsOverwriteAllowed_Absent(c *C) { + testDir := c.MkDir() + depDir := filepath.Join(testDir, "casper") + + bp := config.Blueprint{} + c.Check(checkOverwriteAllowed(depDir, bp, false /*overwriteFlag*/), IsNil) + c.Check(checkOverwriteAllowed(depDir, bp, true /*overwriteFlag*/), IsNil) +} + +func (s *MySuite) TestIsOverwriteAllowed_NotGHPC(c *C) { + depDir := c.MkDir() // empty deployment folder considered malformed + + bp := config.Blueprint{} + c.Check(checkOverwriteAllowed(depDir, bp, false /*overwriteFlag*/), ErrorMatches, ".* not a valid GHPC deployment folder") + c.Check(checkOverwriteAllowed(depDir, bp, true /*overwriteFlag*/), ErrorMatches, ".* not a valid GHPC deployment folder") +} + +func (s *MySuite) TestIsOverwriteAllowed_NoExpanded(c *C) { + depDir := c.MkDir() // empty deployment folder considered malformed + if err := os.MkdirAll(modulewriter.HiddenGhpcDir(depDir), 0755); err != nil { + c.Fatal(err) + } + + bp := config.Blueprint{} + c.Check(checkOverwriteAllowed(depDir, bp, false /*overwriteFlag*/), ErrorMatches, ".* changing GHPC version.*") + c.Check(checkOverwriteAllowed(depDir, bp, true /*overwriteFlag*/), ErrorMatches, ".* changing GHPC version.*") +} + +func (s *MySuite) TestIsOverwriteAllowed_Malformed(c *C) { + depDir := c.MkDir() // empty deployment folder considered malformed + if err := os.MkdirAll(modulewriter.ArtifactsDir(depDir), 0755); err != nil { + c.Fatal(err) + } + expPath := filepath.Join(modulewriter.ArtifactsDir(depDir), "expanded_blueprint.yaml") + if err := os.WriteFile(expPath, []byte("humus"), 0644); err != nil { + c.Fatal(err) + } + + bp := config.Blueprint{} + c.Check(checkOverwriteAllowed(depDir, bp, false /*overwriteFlag*/), NotNil) + c.Check(checkOverwriteAllowed(depDir, bp, true /*overwriteFlag*/), NotNil) +} + +func (s *MySuite) TestIsOverwriteAllowed_Present(c *C) { + depDir := c.MkDir() + artDir := modulewriter.ArtifactsDir(depDir) + if err := os.MkdirAll(artDir, 0755); err != nil { + c.Fatal(err) + } + + prev := config.DeploymentConfig{ + Config: config.Blueprint{ + GhpcVersion: "TaleOdBygoneYears", + DeploymentGroups: []config.DeploymentGroup{ + {Name: "isildur"}}}} + if err := prev.ExportBlueprint(filepath.Join(artDir, "expanded_blueprint.yaml")); err != nil { + c.Fatal(err) + } + + super := config.Blueprint{ + DeploymentGroups: []config.DeploymentGroup{ + {Name: "isildur"}, + {Name: "elendil"}}} + c.Check(checkOverwriteAllowed(depDir, super, false /*overwriteFlag*/), ErrorMatches, ".* already exists, use -w to overwrite") + c.Check(checkOverwriteAllowed(depDir, super, true /*overwriteFlag*/), IsNil) + + sub := config.Blueprint{ + DeploymentGroups: []config.DeploymentGroup{ + {Name: "aragorn"}}} + c.Check(checkOverwriteAllowed(depDir, sub, false /*overwriteFlag*/), ErrorMatches, `.* already exists, use -w to overwrite`) + c.Check(checkOverwriteAllowed(depDir, sub, true /*overwriteFlag*/), ErrorMatches, `.*remove a deployment group "isildur".*`) +} diff --git a/cmd/deploy.go b/cmd/deploy.go index d4d89f77db..51c41dc1d0 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -18,9 +18,9 @@ package cmd import ( "fmt" "hpc-toolkit/pkg/config" + "hpc-toolkit/pkg/logging" "hpc-toolkit/pkg/modulewriter" "hpc-toolkit/pkg/shell" - "log" "path/filepath" "github.com/spf13/cobra" @@ -74,7 +74,7 @@ func getApplyBehavior(autoApprove bool) shell.ApplyBehavior { } func runDeployCmd(cmd *cobra.Command, args []string) { - expandedBlueprintFile := filepath.Join(artifactsDir, expandedBlueprintFilename) + expandedBlueprintFile := filepath.Join(artifactsDir, modulewriter.ExpandedBlueprintName) dc, _, err := config.NewDeploymentConfig(expandedBlueprintFile) checkErr(err) groups := dc.Config.DeploymentGroups @@ -98,7 +98,7 @@ func runDeployCmd(cmd *cobra.Command, args []string) { checkErr(fmt.Errorf("group %s is an unsupported kind %s", groupDir, group.Kind().String())) } } - fmt.Println("\n###############################") + logging.Info("\n###############################") printAdvancedInstructionsMessage(deploymentRoot) } @@ -131,15 +131,15 @@ func deployPackerGroup(moduleDir string) error { } buildImage := applyBehavior == shell.AutomaticApply || shell.ApplyChangesChoice(c) if buildImage { - log.Printf("initializing packer module at %s", moduleDir) + logging.Info("initializing packer module at %s", moduleDir) if err := shell.ExecPackerCmd(moduleDir, false, "init", "."); err != nil { return err } - log.Printf("validating packer module at %s", moduleDir) + logging.Info("validating packer module at %s", moduleDir) if err := shell.ExecPackerCmd(moduleDir, false, "validate", "."); err != nil { return err } - log.Printf("building image using packer module at %s", moduleDir) + logging.Info("building image using packer module at %s", moduleDir) if err := shell.ExecPackerCmd(moduleDir, true, "build", "."); err != nil { return err } diff --git a/cmd/destroy.go b/cmd/destroy.go index 5e0b6d7e05..32aef0f6d3 100644 --- a/cmd/destroy.go +++ b/cmd/destroy.go @@ -63,7 +63,7 @@ func parseDestroyArgs(cmd *cobra.Command, args []string) error { } func runDestroyCmd(cmd *cobra.Command, args []string) error { - expandedBlueprintFile := filepath.Join(artifactsDir, expandedBlueprintFilename) + expandedBlueprintFile := filepath.Join(artifactsDir, modulewriter.ExpandedBlueprintName) dc, _, err := config.NewDeploymentConfig(expandedBlueprintFile) if err != nil { return err diff --git a/cmd/expand.go b/cmd/expand.go index 2e328cd1a5..b58ca726a4 100644 --- a/cmd/expand.go +++ b/cmd/expand.go @@ -16,7 +16,7 @@ package cmd import ( - "fmt" + "hpc-toolkit/pkg/logging" "github.com/spf13/cobra" ) @@ -50,5 +50,5 @@ var ( func runExpandCmd(cmd *cobra.Command, args []string) { dc := expandOrDie(args[0]) checkErr(dc.ExportBlueprint(outputFilename)) - fmt.Printf(boldGreen("Expanded Environment Definition created successfully, saved as %s.\n"), outputFilename) + logging.Info(boldGreen("Expanded Environment Definition created successfully, saved as %s."), outputFilename) } diff --git a/cmd/export.go b/cmd/export.go index 77a0706e17..c87649d0dd 100644 --- a/cmd/export.go +++ b/cmd/export.go @@ -32,10 +32,6 @@ func init() { rootCmd.AddCommand(exportCmd) } -var defaultArtifactsDir = filepath.Join(modulewriter.HiddenGhpcDirName, modulewriter.ArtifactsDirName) - -const expandedBlueprintFilename string = "expanded_blueprint.yaml" - var ( artifactsDir string exportCmd = &cobra.Command{ @@ -73,7 +69,7 @@ func parseExportImportArgs(cmd *cobra.Command, args []string) { func getArtifactsDir(deploymentRoot string) string { if artifactsDir == "" { - return filepath.Clean(filepath.Join(deploymentRoot, defaultArtifactsDir)) + return modulewriter.ArtifactsDir(deploymentRoot) } return artifactsDir } @@ -86,7 +82,7 @@ func runExportCmd(cmd *cobra.Command, args []string) error { return err } - expandedBlueprintFile := filepath.Join(artifactsDir, expandedBlueprintFilename) + expandedBlueprintFile := filepath.Join(artifactsDir, modulewriter.ExpandedBlueprintName) dc, _, err := config.NewDeploymentConfig(expandedBlueprintFile) if err != nil { return err diff --git a/cmd/export_test.go b/cmd/export_test.go index bc4c399cdc..dadf613301 100644 --- a/cmd/export_test.go +++ b/cmd/export_test.go @@ -18,40 +18,24 @@ package cmd import ( "os" + "path/filepath" . "gopkg.in/check.v1" ) func (s *MySuite) TestIsDir(c *C) { - dir, err := os.MkdirTemp("", "test-*") - if err != nil { - c.Fatal(err) - } - defer os.RemoveAll(dir) + dir := c.MkDir() + c.Assert(checkDir(nil, []string{dir}), IsNil) - err = checkDir(nil, []string{dir}) - c.Assert(err, IsNil) + p := filepath.Join(dir, "does-not-exist") + c.Assert(checkDir(nil, []string{p}), NotNil) - os.RemoveAll(dir) - err = checkDir(nil, []string{dir}) - c.Assert(err, NotNil) - - f, err := os.CreateTemp("", "test-*") - if err != nil { - c.Fatal(err) - } - defer os.Remove(f.Name()) - err = checkDir(nil, []string{f.Name()}) - c.Assert(err, NotNil) + f, err := os.CreateTemp(dir, "test-*") + c.Assert(err, IsNil) + c.Assert(checkDir(nil, []string{f.Name()}), NotNil) } func (s *MySuite) TestRunExport(c *C) { - dir, err := os.MkdirTemp("", "test-*") - if err != nil { - c.Fatal(err) - } - defer os.RemoveAll(dir) - - err = runExportCmd(nil, []string{dir}) - c.Assert(err, NotNil) + dir := c.MkDir() + c.Assert(runExportCmd(nil, []string{dir}), NotNil) } diff --git a/cmd/import.go b/cmd/import.go index cf93fa65a6..f51f88affd 100644 --- a/cmd/import.go +++ b/cmd/import.go @@ -17,6 +17,7 @@ package cmd import ( "hpc-toolkit/pkg/config" + "hpc-toolkit/pkg/modulewriter" "hpc-toolkit/pkg/shell" "path/filepath" @@ -50,7 +51,7 @@ func runImportCmd(cmd *cobra.Command, args []string) error { return err } - expandedBlueprintFile := filepath.Join(artifactsDir, expandedBlueprintFilename) + expandedBlueprintFile := filepath.Join(artifactsDir, modulewriter.ExpandedBlueprintName) dc, _, err := config.NewDeploymentConfig(expandedBlueprintFile) if err != nil { return err diff --git a/cmd/root.go b/cmd/root.go index b176282a63..9583af298f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -21,7 +21,7 @@ import ( "errors" "fmt" "hpc-toolkit/pkg/config" - "log" + "hpc-toolkit/pkg/logging" "os" "os/exec" "path/filepath" @@ -49,10 +49,10 @@ var ( HPC deployments on the Google Cloud Platform.`, Run: func(cmd *cobra.Command, args []string) { if err := cmd.Help(); err != nil { - log.Fatalf("cmd.Help function failed: %s", err) + logging.Fatal("cmd.Help function failed: %s", err) } }, - Version: "v1.26.1", + Version: "v1.27.0", Annotations: annotation, } ) @@ -66,12 +66,9 @@ func init() { // Execute the root command func Execute() error { - // Don't prefix messages with data & time to improve readability. - // See https://pkg.go.dev/log#pkg-constants - log.SetFlags(0) mismatch, branch, hash, dir := checkGitHashMismatch() if mismatch { - fmt.Fprintf(os.Stderr, "WARNING: ghpc binary was built from a different commit (%s/%s) than the current git branch in %s (%s/%s). You can rebuild the binary by running 'make'\n", + logging.Error("WARNING: ghpc binary was built from a different commit (%s/%s) than the current git branch in %s (%s/%s). You can rebuild the binary by running 'make'", GitBranch, GitCommitHash[0:7], dir, branch, hash[0:7]) } @@ -198,7 +195,7 @@ func execPath() string { // "simplification" of `ghpc` to `./ghpc` return nice } - // Code bellow assumes that `args0` contains path to file, not a + // Code below assumes that `args0` contains path to file, not a // executable name from PATH. { // Find shortest & nicest form of args0 @@ -249,11 +246,11 @@ func execPath() string { return args0 } -// checkErr is similar to cobra.CheckErr, but with renderError and log.Fatal +// checkErr is similar to cobra.CheckErr, but with renderError and logging.Fatal // NOTE: this function uses empty YamlCtx, so if you have one, use renderError directly. func checkErr(err error) { if err != nil { msg := fmt.Sprintf("%s: %s", boldRed("Error"), renderError(err, config.YamlCtx{})) - log.Fatal(msg) + logging.Fatal(msg) } } diff --git a/community/examples/AMD/hpc-amd-slurm.yaml b/community/examples/AMD/hpc-amd-slurm.yaml index 0735ccd438..4f68b4de41 100644 --- a/community/examples/AMD/hpc-amd-slurm.yaml +++ b/community/examples/AMD/hpc-amd-slurm.yaml @@ -169,7 +169,7 @@ deployment_groups: disable_public_ips: true instance_image: # these images must match the images used by Slurm modules below because - # we are building OpenMPI with PMI support in libaries contained in + # we are building OpenMPI with PMI support in libraries contained in # Slurm installation family: slurm-gcp-5-9-hpc-centos-7 project: schedmd-slurm-public diff --git a/community/examples/flux-framework/README.md b/community/examples/flux-framework/README.md index 665dd58da8..5039b67fe5 100644 --- a/community/examples/flux-framework/README.md +++ b/community/examples/flux-framework/README.md @@ -12,7 +12,7 @@ The cluster includes > **_NOTE:_** prior to running this HPC Toolkit example the [Flux Framework GCP Images](https://github.com/GoogleCloudPlatform/scientific-computing-examples/tree/main/fluxfw-gcp/img#flux-framework-gcp-images) > must be created in your project. -### Intial Setup for flux-framework Cluster +### Initial Setup for flux-framework Cluster Before provisioning any infrastructure in this project you should follow the Toolkit guidance to enable [APIs][apis] and establish minimum resource diff --git a/community/examples/fsi-montecarlo-on-batch.yaml b/community/examples/fsi-montecarlo-on-batch.yaml new file mode 100644 index 0000000000..0fc472e1ec --- /dev/null +++ b/community/examples/fsi-montecarlo-on-batch.yaml @@ -0,0 +1,116 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +blueprint_name: fsi-montecarlo-on-batch + +vars: + project_id: ## Set GCP Project ID Here ## + deployment_name: fsimontecarlo + region: us-central1 + zone: us-central1-a +deployment_groups: +- group: setup + modules: + + - id: enable_apis + source: community/modules/project/service-enablement + settings: + gcp_service_list: [ + "bigquery.googleapis.com", + "cloudresourcemanager.googleapis.com", + "container.googleapis.com", + "logging.googleapis.com", + "notebooks.googleapis.com", + "batch.googleapis.com", + "pubsub.googleapis.com", + "compute.googleapis.com" + ] +- group: primary + modules: + + - id: fsi_bucket + source: community/modules/file-system/cloud-storage-bucket + settings: + name_prefix: fsi_bucket + random_suffix: true + force_destroy: true + local_mount: /home/jupyter/fsi + + - id: pubsub_topic + source: community/modules/pubsub/topic + + - id: bq-dataset + source: community/modules/database/bigquery-dataset + settings: + + - id: bq-table + source: community/modules/database/bigquery-table + use: [bq-dataset] + settings: + table_schema: + ' + [ + { + "name": "subscription_name", "type": "STRING" + }, + { + "name": "message_id", "type": "STRING" + }, + { + "name": "publish_time", "type": "TIMESTAMP" + }, + { + "name": "simulation_results", "type": "RECORD", "mode": "REPEATED", + "fields": [ + { + "name" : "price", + "type" : "NUMERIC" + } + ] + }, + { + "name": "ticker", "type": "STRING" + } + ,{ + "name": "epoch_time", "type": "INT64" + } + ,{ + "name": "iteration", "type": "INT64" + } + ,{ + "name": "start_date", "type": "STRING" + } + ,{ + "name": "end_date", "type": "STRING" + } + ,{ + "name": "attributes", "type": "STRING" + } + ] + ' + + - id: fsi_notebook + source: community/modules/compute/notebook + use: [fsi_bucket] + settings: + machine_type: n1-standard-4 + + - id: fsi_tutorial_files + source: community/modules/files/fsi-montecarlo-on-batch + use: [bq-dataset, bq-table, fsi_bucket, pubsub_topic] + + - id: bq_subscription + source: community/modules/pubsub/bigquery-sub + use: [bq-table, pubsub_topic] diff --git a/community/examples/hpc-slurm-local-ssd.yaml b/community/examples/hpc-slurm-local-ssd.yaml index 39b71aa9ea..c8b18d1f8f 100644 --- a/community/examples/hpc-slurm-local-ssd.yaml +++ b/community/examples/hpc-slurm-local-ssd.yaml @@ -17,7 +17,7 @@ blueprint_name: hpc-slurm-local-ssd vars: - project_id: ## Set GCP Project ID Here ## + project_id: ## Set GCP Project ID Here ## deployment_name: hpc-localssd region: us-central1 zone: us-central1-a @@ -56,7 +56,7 @@ deployment_groups: auto_delete: true boot: false bandwidth_tier: gvnic_enabled - machine_type: n1-standard-16 + machine_type: c2-standard-4 node_count_dynamic_max: 5 node_count_static: 0 @@ -70,6 +70,35 @@ deployment_groups: is_default: true partition_name: ssdcomp region: us-central1 + startup_script: | + #!/bin/bash + set -e -o pipefail + + # this script assumes it is running on a RedHat-derivative OS + yum install -y mdadm + + RAID_DEVICE=/dev/md0 + DST_MNT=/mnt/localssd + DISK_LABEL=LOCALSSD + OPTIONS=discard,defaults + + # if mount is successful, do nothing + if mount --source LABEL="$DISK_LABEL" --target="$DST_MNT" -o "$OPTIONS"; then + exit 0 + fi + + # Create new RAID, format ext4 and mount + # TODO: handle case of zero or 1 local SSD disk + # TODO: handle case when /dev/md0 exists but was not mountable + DEVICES=`nvme list | grep nvme_ | grep -v nvme_card-pd | awk '{print $1}' | paste -sd ' '` + NB_DEVICES=`nvme list | grep nvme_ | grep -v nvme_card-pd | awk '{print $1}' | wc -l` + mdadm --create "$RAID_DEVICE" --level=0 --raid-devices=$NB_DEVICES $DEVICES + mkfs.ext4 -F "$RAID_DEVICE" + tune2fs "$RAID_DEVICE" -r 131072 + e2label "$RAID_DEVICE" "$DISK_LABEL" + mkdir -p "$DST_MNT" + mount --source LABEL="$DISK_LABEL" --target="$DST_MNT" -o "$OPTIONS" + chmod 1777 "$DST_MNT" - id: slurm_controller source: community/modules/scheduler/schedmd-slurm-gcp-v5-controller @@ -84,27 +113,6 @@ deployment_groups: suspend_rate: 0 suspend_timeout: 300 no_comma_params: false - compute_startup_script: | - #!/bin/bash - export LOG_FILE=/tmp/custom_startup.log - export DST_MNT="/mount/localssd" # TODO: set this appropriately - if [ -d $DST_MNT ]; then - echo "DST_MNT already exists. Canceling." >> $LOG_FILE - exit 1 - fi - sudo yum install mdadm -y - lsblk >> $LOG_FILE - export DEVICES=`lsblk -d -n -oNAME,RO | grep 'nvme.*0$' | awk {'print "/dev/" $1'}` - export NB_DEVICES=`lsblk -d -n -oNAME,RO | grep 'nvme.*0$' | wc | awk {'print $1'}` - sudo mdadm --create /dev/md0 --level=0 --raid-devices=$NB_DEVICES $DEVICES - sudo mkfs.ext4 -F /dev/md0 - sudo mkdir -p $DST_MNT - sudo mount /dev/md0 $DST_MNT - sudo chmod a+w $DST_MNT - echo UUID=`sudo blkid -s UUID -o value /dev/md0` $DST_MNT ext4 discard,defaults,nofail 0 2 | sudo tee -a /etc/fstab - cat /etc/fstab >> $LOG_FILE - echo "DONE" >> $LOG_FILE - cat $LOG_FILE machine_type: n1-standard-4 - id: slurm_login diff --git a/community/examples/hpc-slurm-ramble-gromacs.yaml b/community/examples/hpc-slurm-ramble-gromacs.yaml index db8675d95a..15e6577c95 100644 --- a/community/examples/hpc-slurm-ramble-gromacs.yaml +++ b/community/examples/hpc-slurm-ramble-gromacs.yaml @@ -21,6 +21,7 @@ vars: deployment_name: hpc-slurm-ramble-gromacs region: us-central1 zone: us-central1-c + system_user_name: spack-ramble # Documentation for each of the modules used below can be found at # https://github.com/GoogleCloudPlatform/hpc-toolkit/blob/main/modules/README.md diff --git a/community/examples/intel/README.md b/community/examples/intel/README.md index 77c33e1bd4..7172673363 100644 --- a/community/examples/intel/README.md +++ b/community/examples/intel/README.md @@ -272,7 +272,7 @@ Both daos-server instances should show a state of *Joined*. #### About the DAOS Command Line Tools -The DAOS Management tool `dmg` is used by System Administrators to manange the DAOS storage [system](https://docs.daos.io/v2.2/overview/architecture/#daos-system) and DAOS [pools](https://docs.daos.io/v2.2/overview/storage/#daos-pool). Therefore, `sudo` must be used when running `dmg`. +The DAOS Management tool `dmg` is used by System Administrators to manage the DAOS storage [system](https://docs.daos.io/v2.2/overview/architecture/#daos-system) and DAOS [pools](https://docs.daos.io/v2.2/overview/storage/#daos-pool). Therefore, `sudo` must be used when running `dmg`. The DAOS CLI `daos` is used by both users and System Administrators to create and manage [containers](https://docs.daos.io/v2.2/overview/storage/#daos-container). It is not necessary to use `sudo` with the `daos` command. diff --git a/community/examples/quantum-circuit-simulator.yaml b/community/examples/quantum-circuit-simulator.yaml index de4939b0bb..2876ae5b16 100644 --- a/community/examples/quantum-circuit-simulator.yaml +++ b/community/examples/quantum-circuit-simulator.yaml @@ -41,6 +41,8 @@ deployment_groups: content: | #!/bin/bash # This script implements https://quantumai.google/qsim/tutorials/gcp_gpu + # Disable any user interactive prompt during upgrade script. + export DEBIAN_FRONTEND=noninteractive set -e -o pipefail curl -O https://raw.githubusercontent.com/GoogleCloudPlatform/compute-gpu-installation/main/linux/install_gpu_driver.py python3 install_gpu_driver.py diff --git a/community/front-end/ofe/deploy.sh b/community/front-end/ofe/deploy.sh index cc6ef1e662..3666b1b2b8 100755 --- a/community/front-end/ofe/deploy.sh +++ b/community/front-end/ofe/deploy.sh @@ -53,6 +53,8 @@ PRJ_API['iam.googleapis.com']='Identity and Access Management (IAM) API' PRJ_API['oslogin.googleapis.com']='Cloud OS Login API' PRJ_API['cloudbilling.googleapis.com']='Cloud Billing API' PRJ_API['aiplatform.googleapis.com']='Vertex AI API' +PRJ_API['bigqueryconnection.googleapis.com']='BigQuery Connection API' +PRJ_API['sqladmin.googleapis.com']='Cloud SQL Admin API' # Location for output credential file = pwd/credential.json # @@ -128,6 +130,9 @@ HELP1 subnet_name: Name of the subnet to use for the deployment dns_hostname: Hostname to assign to the deployment's IP address ip_address: Static IP address to use for the deployment + deployment_mode: The mode used to deploy the FrontEnd, which must be either 'git' or 'tarball' + repo_fork: The GitHub owner of the forked repo that is used for the deployment, if the 'git' deployment mode is used + repo_branch: The git branch of the forked repo that is used for the deployment, if the 'git' deployment mode is used To set the Django superuser password securely, you can set the DJANGO_SUPERUSER_PASSWORD environment variable with the password you want to use, like this: @@ -156,6 +161,9 @@ HELP1 django_superuser_username: sysadm django_superuser_password: Passw0rd! (optional if DJANGO_SUPERUSER_PASSWORD is passed) django_superuser_email: sysadmin@example.com + deployment_mode: git (optional) + repo_fork: GoogleCloudPlatform (optional) + repo_branch: develop (optional) HELP2 } @@ -178,7 +186,7 @@ error() { # Capture user entry. # - Has an option to hide the response, which is useful for passwords. # - Accepts a default that is used when no user entry. -# - Note: this function is used in command substition, i.e. foo=$(ask "bar") +# - Note: this function is used in command substitution, i.e. foo=$(ask "bar") # so no echo commands can be used # # Usage: @@ -439,7 +447,7 @@ create_service_account() { getcred=1 ;; *) - verbose "assuming re-use of account" + verbose "assuming reuse of account" echo "" echo " Using existing service account: ${service_account}" case $(ask " Do you want to regenerate a credential? [y/N] ") in @@ -500,18 +508,11 @@ deploy() { if [ "${deployment_mode}" == "tarball" ]; then basedir=$(git rev-parse --show-toplevel) - sdir=${SCRIPT_DIR#"${basedir}"} tdir=/tmp/hpc-toolkit cp -R "${basedir}" ${tdir}/ ( cd ${tdir} - # - # Shuffle contents to put paths where they are expected to be - # TODO: remove hardwired paths in TKFE source code - # - mkdir -p community/front-end - mv ${tdir}"${sdir}"/* community/front-end/ tar -zcf "${SCRIPT_DIR}"/tf/deployment.tar.gz \ --exclude=.terraform \ @@ -520,6 +521,7 @@ deploy() { --directory=/tmp \ ./hpc-toolkit 2>/dev/null ) + rm -rf ${tdir} fi @@ -555,21 +557,14 @@ TFVARS echo "static_ip = \"${ip_address}\"" >>terraform.tfvars fi - #### git deployment not yet available - commented out, reinstate later - #### - #### - will need to make sure paths are modified - # - # if [ "${deployment}" == "git" ]; then - # echo "Will clone hpc-toolkit from github.com/${REPO_FORK:-GoogleCloudPlatform}.git branch ${REPO_BRANCH:-main}." - # echo "Set REPO_BRANCH and REPO_FORK environment variables to override" - # - # cat >>terraform.tfvars <<+ - # repo_branch = "${REPO_BRANCH:-main}" - # repo_fork = "${REPO_FORK:-GoogleCloudPlatform}" - # deployment_key = "${deploy_key}" - #+ - # fi - #### + if [ "${deployment_mode}" == "git" ]; then + echo "Will clone hpc-toolkit from github.com/${repo_fork}/hpc-toolkit.git ${repo_branch} branch." + + cat <<-END >>terraform.tfvars + repo_fork = "${repo_fork}" + repo_branch = "${repo_branch}" + END + fi echo "" # echo "terraform.tfvars file has been created in the 'tf' directory." @@ -850,42 +845,28 @@ SERVICEACC esac - # - # For now, we have restricted deployment to only be via tarball - # - # TODO - Reinstate option to deploy from git, once close to formal release - # and location and access to open repository is available. - # - # Will need to make sure that names, etc., are correct and don't - # break deployment and startup scripts (e.g. 'root' directory name - # must be "hpc-toolkit") - # - #echo "" - #echo "Please select deployment method of server software:" - #echo " 1) Use a copy of the code from this computer" - #echo " 2) Clone the git repo when server deploys" - #deploy_choice=$(ask " Please choose one of the above options", "1") - #if [ "${deploy_choice}" == "1" ]; then - # deployment_mode="tarball" - #elif [ "${deploy_choice}" == "2" ]; then - # deployment_mode="git" - # deploy_key=$(ask "For Git clones, please specify the path to the deployment key") - # if [ ! -r "${deploy_key}" ]; then - # error "Deployment key file cannot be read." - # exit 1 - # fi - # deploy_key=$(realpath "${deploy_key}") - #else - # error "Invalid selection" - # exit 1 - #fi - deployment_mode="tarball" + echo "" + echo "Please select deployment method of server software:" + echo " 1) Clone the git repo when server deploys" + echo " 2) Use a copy of the code from this computer" + deploy_choice=$(ask " Please choose one of the above options", "1") + if [ "${deploy_choice}" == "1" ]; then + deployment_mode="git" + repo_fork=$(ask "Please specify the forked repo owner (or just press Enter)", "GoogleCloudPlatform") + repo_branch=$(ask "Please specify the forked repo branch (or just press Enter)", "main") + elif [ "${deploy_choice}" == "2" ]; then + deployment_mode="tarball" + else + error "Invalid selection" + exit 1 + fi # -- Summarise entered parameters back to user # echo "" echo "*** Deployment summary: ***" echo "" + echo " Deploymnet mode: ${deployment_mode}" echo " Deployment name: ${deployment_name}" echo " GCP project ID: ${project_id}" echo " GCP zone: ${zone}" @@ -935,6 +916,9 @@ deploy_from_config() { ip_address=${yaml_array[ip_address]} django_superuser_username=${yaml_array[django_superuser_username]} django_superuser_email=${yaml_array[django_superuser_email]} + deployment_mode=${yaml_array[deployment_mode]:-tarball} + repo_fork=${yaml_array[repo_fork]:-GoogleCloudPlatform} + repo_branch=${yaml_array[repo_branch]:-main} # Set password from environment variable if it exists, otherwise from YAML file if [[ -n ${DJANGO_SUPERUSER_PASSWORD+x} ]]; then @@ -954,6 +938,7 @@ deploy_from_config() { echo "" echo "*** Deployment summary: ***" echo "" + echo " Deploymnet mode: ${deployment_mode}" echo " Deployment name: ${deployment_name}" echo " GCP project ID: ${project_id}" echo " GCP zone: ${zone}" @@ -977,7 +962,6 @@ deploy_from_config() { echo " Admin email: ${django_superuser_email}" echo "" - deployment_mode="tarball" deploy } diff --git a/community/front-end/ofe/docs/ClusterCommandControl.md b/community/front-end/ofe/docs/ClusterCommandControl.md index 40dd3a8614..a036ee91a6 100644 --- a/community/front-end/ofe/docs/ClusterCommandControl.md +++ b/community/front-end/ofe/docs/ClusterCommandControl.md @@ -1,5 +1,5 @@ # Command and Control of Clusters -Previous incarnations of this Frontend relied on the frontend webserver instance being able to SSH directly to clusters in order to performance command and control (C2) operations. When clusters were created, an admin user was set that would accept a public ssh key for which the webserver owned the private key. This was largely straightfoward, and worked quite well. The clusters were also able to make HTTP API queries to the webserver. +Previous incarnations of this Frontend relied on the frontend webserver instance being able to SSH directly to clusters in order to performance command and control (C2) operations. When clusters were created, an admin user was set that would accept a public ssh key for which the webserver owned the private key. This was largely straightforward, and worked quite well. The clusters were also able to make HTTP API queries to the webserver. This works well in the case where webserver and clusters all have public IP addresses, and are able to receive inbound requests, but it breaks down in the case where a user may wish to have the compute clusters not be directly exposed to the public internet. diff --git a/community/front-end/ofe/docs/WorkbenchUser.md b/community/front-end/ofe/docs/WorkbenchUser.md index 5a172ff07f..6da3457bad 100644 --- a/community/front-end/ofe/docs/WorkbenchUser.md +++ b/community/front-end/ofe/docs/WorkbenchUser.md @@ -91,7 +91,7 @@ on the workbench page. It is important to remember that all data stored on the workbench instance will be deleted unless it has been saved in another place such as a shared -filesystem or transferred elsewhere in another way. Once the destory button is +filesystem or transferred elsewhere in another way. Once the destroy button is clicked a confirmation page will be displayed. ![Destroy confirmation](images/Workbench_userguide/destroy_confirm.png) diff --git a/community/front-end/ofe/docs/admin_guide.md b/community/front-end/ofe/docs/admin_guide.md index 9534af528c..e89596e87b 100644 --- a/community/front-end/ofe/docs/admin_guide.md +++ b/community/front-end/ofe/docs/admin_guide.md @@ -12,7 +12,7 @@ applications. and manage user access. Normal HPC users should refer to the [User Guide](user_guide.md) for guidance on how to prepare and run jobs on clusters that have been set up by administrators. -Basic administrator knowledge of the Google Cloud Plaform is needed in order to +Basic administrator knowledge of the Google Cloud Platform is needed in order to create projects and user accounts, but all other low-level administration tasks are handled by the portal. @@ -75,7 +75,7 @@ All further deployment actions must be performed from this directory. #### Google Cloud Platform -Your organisation must already have access to the Google Cloud Plaform (GCP) +Your organisation must already have access to the Google Cloud Platform (GCP) and be able to create projects and users. A project and a user account with enabled APIs and roles/permissions need to be created. The user account must also be authenticated on the client machine to allow it to provision GCP @@ -198,6 +198,9 @@ To use this configuration file for automated deployment, follow these steps: django_superuser_username: sysadm django_superuser_password: Passw0rd! # (optional if DJANGO_SUPERUSER_PASSWORD is passed) django_superuser_email: sysadmin@example.com + deployment_mode: git # (optional) + repo_fork: GoogleCloudPlatform # (optional) + repo_branch: develop # (optional) ``` 1. Save the file in the same directory as the deploy.sh script. @@ -299,7 +302,7 @@ be reachable by the VPC subnets intended to be used for clusters. An internal address can be used if the cluster shares the same VPC with the imported filesystem. Alternatively, system administrators can set up hybrid -connectivity (such as extablishing network peering) beforing mounting the +connectivity (such as extablishing network peering) before mounting the external filesystem located elsewhere on GCP. ## Cluster Management @@ -352,7 +355,7 @@ A typical workflow for creating a new cluster is as follows: cluster can be specified. 1. In the *Create a new cluster* form, give the new cluster a name. Cloud resource names are subject to naming constraints and will be validated by the - system. In general, lower-case alpha-numeric names with hyphens are + system. In general, lower-case alphanumeric names with hyphens are accepted. 1. From the *Subnet* dropdown list, select the subnet within which the cluster resides. @@ -554,5 +557,5 @@ back-end logic is handled, which can also help with certain issues. `terraform destroy` there for clean up cloud resources. - Certain database records might get corrupted and need to be removed for failed clusters or network/filesystem components. This can be done from the - Django Admin site, although adminstrators need to exercise caution while + Django Admin site, although administrators need to exercise caution while modifying the raw data in Django database. diff --git a/community/front-end/ofe/docs/developer_guide.md b/community/front-end/ofe/docs/developer_guide.md index 2abc503257..5690740236 100644 --- a/community/front-end/ofe/docs/developer_guide.md +++ b/community/front-end/ofe/docs/developer_guide.md @@ -141,7 +141,7 @@ The home directory of the *gcluster* account is at `/opt/gcluster`. For a new de - `supvisor.log` -  Django application server log. Python `print` from Django source files will appear in this file for debugging purposes. - `django.log` - additional debugging information generated by the Python - logging module is writen here. + logging module is written here. ### Run-time data diff --git a/community/front-end/ofe/infrastructure_files/gcs_bucket/clusters/ansible_setup/roles/c2_daemon/files/ghpcfe_c2daemon.py b/community/front-end/ofe/infrastructure_files/gcs_bucket/clusters/ansible_setup/roles/c2_daemon/files/ghpcfe_c2daemon.py index 0e1941e2f5..f01dc8a0ca 100644 --- a/community/front-end/ofe/infrastructure_files/gcs_bucket/clusters/ansible_setup/roles/c2_daemon/files/ghpcfe_c2daemon.py +++ b/community/front-end/ofe/infrastructure_files/gcs_bucket/clusters/ansible_setup/roles/c2_daemon/files/ghpcfe_c2daemon.py @@ -692,7 +692,7 @@ def _make_run_script(job_dir, uid, gid, orig_run_script): ) elif script_url.scheme in ["http", "https"]: if recursive_fetch: - logger.error("Not Implemented recursive HTTP/HTTPS fetchs") + logger.error("Not Implemented recursive HTTP/HTTPS fetches") return None fetch = f"curl --silent -O '{text}'" diff --git a/community/front-end/ofe/infrastructure_files/gcs_bucket/clusters/ansible_setup/roles/common/tasks/main.yaml b/community/front-end/ofe/infrastructure_files/gcs_bucket/clusters/ansible_setup/roles/common/tasks/main.yaml index 42e112cd94..7c8501e0b7 100644 --- a/community/front-end/ofe/infrastructure_files/gcs_bucket/clusters/ansible_setup/roles/common/tasks/main.yaml +++ b/community/front-end/ofe/infrastructure_files/gcs_bucket/clusters/ansible_setup/roles/common/tasks/main.yaml @@ -13,7 +13,7 @@ # limitations under the License. --- -- name: Add Enviornment Modules +- name: Add Environment Modules ansible.builtin.yum: name: - environment-modules diff --git a/community/front-end/ofe/infrastructure_files/gcs_bucket/clusters/ansible_setup/roles/spack_install/tasks/main.yaml b/community/front-end/ofe/infrastructure_files/gcs_bucket/clusters/ansible_setup/roles/spack_install/tasks/main.yaml index 823aa52127..33e1830af0 100644 --- a/community/front-end/ofe/infrastructure_files/gcs_bucket/clusters/ansible_setup/roles/spack_install/tasks/main.yaml +++ b/community/front-end/ofe/infrastructure_files/gcs_bucket/clusters/ansible_setup/roles/spack_install/tasks/main.yaml @@ -25,7 +25,7 @@ ansible.builtin.git: repo: https://github.com/spack/spack.git dest: "{{ spack_dir }}" - version: v0.19.1 + version: v0.21.0 depth: 1 - name: Apply Global Spack configurations diff --git a/community/front-end/ofe/infrastructure_files/gcs_bucket/webserver/startup.sh b/community/front-end/ofe/infrastructure_files/gcs_bucket/webserver/startup.sh index 9a3a590023..3342148389 100644 --- a/community/front-end/ofe/infrastructure_files/gcs_bucket/webserver/startup.sh +++ b/community/front-end/ofe/infrastructure_files/gcs_bucket/webserver/startup.sh @@ -49,14 +49,32 @@ dnf install -y epel-release dnf update -y --security dnf config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo dnf install --best -y google-cloud-sdk nano make gcc python38-devel unzip git \ - rsync nginx bind-utils policycoreutils-python-utils \ - terraform packer supervisor python3-certbot-nginx \ - grafana + rsync wget nginx bind-utils policycoreutils-python-utils \ + terraform packer supervisor python3-certbot-nginx curl --silent --show-error --location https://github.com/mikefarah/yq/releases/download/v4.13.4/yq_linux_amd64 --output /usr/local/bin/yq chmod +x /usr/local/bin/yq curl --silent --show-error --location https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz --output /tmp/shellcheck.tar.xz tar xfa /tmp/shellcheck.tar.xz --strip=1 --directory /usr/local/bin +# Install Grafana +curl -sSL -o gpg.key https://rpm.grafana.com/gpg.key +rpm --import gpg.key + +tee /etc/yum.repos.d/grafana.repo <>/etc/bashrc printf "\n####################\n#### Creating firewall & SELinux rules\n####################\n" printf "Adding rule for port 22 (ssh): " @@ -129,23 +136,7 @@ fi useradd -r -m -d /opt/gcluster gcluster if [ "${deploy_mode}" == "git" ]; then - printf "Adding deployment keys..\n" - mkdir -p /opt/gcluster/.ssh - - echo "$DEPLOY_KEY1" >/opt/gcluster/.ssh/gcluster-deploykey - sed -i -e :a -e '/^\n*$/{$d;N;ba' -e '}' /opt/gcluster/.ssh/gcluster-deploykey - cat >>/opt/gcluster/.ssh/config <<+ - -host github.com - hostname github.com - IdentityFile ~/.ssh/gcluster-deploykey - StrictHostKeyChecking=accept-new -+ - chmod 700 /opt/gcluster/.ssh - chmod 600 /opt/gcluster/.ssh/* - chown gcluster -R /opt/gcluster/.ssh - - fetch_hpc_toolkit="git clone -b \"${repo_branch}\" git@github.com:${repo_fork}/hpc-toolkit.git" + fetch_hpc_toolkit="git clone -b \"${repo_branch}\" https://github.com/${repo_fork}/hpc-toolkit.git" elif [ "${deploy_mode}" == "tarball" ]; then printf "\n####################\n#### Download web application files\n####################\n" @@ -163,13 +154,26 @@ chown gcluster:gcluster -R /opt/gcluster sudo su - gcluster -c /bin/bash <>/etc/bashrc + +sudo su - gcluster -c /bin/bash < /dev/null popd @@ -187,7 +191,7 @@ sudo su - gcluster -c /bin/bash < configuration.yaml @@ -223,18 +227,20 @@ EOF # Tweak Grafana settings # -sed -i \ - -e '/^\[server]/,/^\[/{s/serve_from_sub_path = false/serve_from_sub_path = true/}' \ - -e '/^\[server]/,/^\[/{s/root_url = \(.*\)\/$/root_url = \1\/grafana\//}' \ - -e '/^\[auth.proxy]/,/^\[/{s/enabled = false/enabled = true/}' \ - -e '/^\[auth.proxy]/,/^\[/{s/whitelist =.*/whitelist = 127.0.0.1/}' \ - -e '/^\[auth.proxy]/,/^\[/{s/header_property =.*/header_property = email/}' \ - /etc/grafana/grafana.ini +cat </etc/grafana/grafana.ini +[server] +serve_from_sub_path = true +root_url = %(protocol)s://%(domain)s:%(http_port)s/grafana/ +[auth.proxy] +enabled = true +whitelist = 127.0.0.1 +header_property = email +EOL printf "Creating supervisord service..." echo "[program:gcluster-uvicorn-background] process_name=%(program_name)s_%(process_num)02d -directory=/opt/gcluster/hpc-toolkit/community/front-end/website +directory=/opt/gcluster/hpc-toolkit/community/front-end/ofe/website command=/opt/gcluster/django-env/bin/uvicorn website.asgi:application --reload --host 127.0.0.1 --port 8001 autostart=true autorestart=true @@ -251,8 +257,8 @@ After=supervisord.service grafana-server.service [Service] Type=forking -ExecStart=/usr/sbin/nginx -p /opt/gcluster/run/ -c /opt/gcluster/hpc-toolkit/community/front-end/website/nginx.conf -ExecStop=/usr/sbin/nginx -p /opt/gcluster/run/ -c /opt/gcluster/hpc-toolkit/community/front-end/website/nginx.conf -s stop +ExecStart=/usr/sbin/nginx -p /opt/gcluster/run/ -c /opt/gcluster/hpc-toolkit/community/front-end/ofe/website/nginx.conf +ExecStop=/usr/sbin/nginx -p /opt/gcluster/run/ -c /opt/gcluster/hpc-toolkit/community/front-end/ofe/website/nginx.conf -s stop PIDFile=/opt/gcluster/run/nginx.pid Restart=no @@ -270,7 +276,7 @@ systemctl status gcluster.service # sudo su - gcluster -c /bin/bash <>"${tmpcron}" # .. if something more forceful/complete is needed: - # echo "0 12 * * * /usr/bin/certbot certonly --force-renew --quiet" --nginx --nginx-server-root=/opt/gcluster/hpc-toolkit/community/front-end/website --cert-name "${SERVER_HOSTNAME}" -m "${DJANGO_EMAIL}" >>"${tmpcron}" + # echo "0 12 * * * /usr/bin/certbot certonly --force-renew --quiet" --nginx --nginx-server-root=/opt/gcluster/hpc-toolkit/community/front-end/ofe/website --cert-name "${SERVER_HOSTNAME}" -m "${DJANGO_EMAIL}" >>"${tmpcron}" crontab -u root "${tmpcron}" rm "${tmpcron}" diff --git a/community/front-end/ofe/requirements.txt b/community/front-end/ofe/requirements.txt index 87935411a9..9624308827 100644 --- a/community/front-end/ofe/requirements.txt +++ b/community/front-end/ofe/requirements.txt @@ -1,7 +1,9 @@ +altgraph==0.17.4 archspec==0.2.1 argcomplete==3.1.1 asgiref==3.7.2 astroid==2.15.5 +attrs==23.1.0 # This should be supported by zoneinfo in Python 3.9+ backports.zoneinfo==0.2.1;python_version<"3.9" cachetools==5.3.1 @@ -10,7 +12,7 @@ cffi==1.15.1 cfgv==3.3.1 charset-normalizer==3.1.0 click==8.1.3 -cryptography==41.0.4 +cryptography==41.0.6 decorator==5.1.1 defusedxml==0.7.1 dill==0.3.6 @@ -42,13 +44,21 @@ h11==0.14.0 httplib2==0.22.0 identify==2.5.24 idna==3.4 +importlib-resources==6.1.1 isort==5.12.0 +Jinja2==3.1.2 +jsonschema==4.20.0 +jsonschema-specifications==2023.11.1 lazy-object-proxy==1.9.0 libcst==1.0.1 +macholib==1.16.3 +MarkupSafe==2.1.3 mccabe==0.7.0 mypy-extensions==1.0.0 nodeenv==1.8.0 oauthlib==3.2.2 +path==16.7.1 +pkgutil_resolve_name==1.3.10 platformdirs==3.8.0 pre-commit==3.3.3 proto-plus==1.22.3 @@ -64,10 +74,14 @@ pyparsing==3.1.0 python3-openid==3.2.0 pytz==2023.3 PyYAML==6.0 +referencing==0.31.0 requests==2.31.0 requests-oauthlib==1.3.1 retry==0.9.2 +rpds-py==0.13.1 rsa==4.9 +ruamel.yaml==0.18.5 +ruamel.yaml.clib==0.2.8 semantic-version==2.10.0 setuptools-rust==1.6.0 six==1.16.0 @@ -84,3 +98,4 @@ virtualenv==20.23.1 wrapt==1.15.0 xmltodict==0.13.0 yq==3.2.2 +zipp==3.17.0 diff --git a/community/front-end/ofe/script/service_account.sh b/community/front-end/ofe/script/service_account.sh index 53b1a6a2d9..41f88dc4f7 100755 --- a/community/front-end/ofe/script/service_account.sh +++ b/community/front-end/ofe/script/service_account.sh @@ -60,7 +60,11 @@ SA_ROLES=('aiplatform.admin' 'notebooks.admin' 'resourcemanager.projectIamAdmin' 'monitoring.viewer' - 'pubsub.admin') + 'pubsub.admin' + 'cloudsql.admin' + 'bigquery.admin' + 'secretmanager.admin' + 'servicenetworking.networksAdmin') # # @@ -160,7 +164,7 @@ create_service_account() { set -e # Add all required roles to new service account - # - can assume we can do this if account creation above worke + # - can assume we can do this if account creation above works # sa_fullname=$(sa_expand "${project}" "${account}") for role in "${SA_ROLES[@]}"; do diff --git a/community/front-end/ofe/tf/main.tf b/community/front-end/ofe/tf/main.tf index 5954117432..bec26eb1e7 100644 --- a/community/front-end/ofe/tf/main.tf +++ b/community/front-end/ofe/tf/main.tf @@ -66,9 +66,10 @@ module "control_bucket" { source = "terraform-google-modules/cloud-storage/google" version = "~> 4.0" - project_id = var.project_id - names = ["storage"] - prefix = var.deployment_name + project_id = var.project_id + names = ["storage"] + prefix = var.deployment_name + randomize_suffix = true force_destroy = { storage = true } diff --git a/community/front-end/ofe/website/ghpcfe/cluster_manager/c2.py b/community/front-end/ofe/website/ghpcfe/cluster_manager/c2.py index 75adf169a0..cff2396f8e 100644 --- a/community/front-end/ofe/website/ghpcfe/cluster_manager/c2.py +++ b/community/front-end/ofe/website/ghpcfe/cluster_manager/c2.py @@ -45,7 +45,7 @@ # If no 'target' (aka, coming from the clusters: # * source={cluster_id} - Who sent it? -# Command with reponse callback +# Command with response callback # # Commands that require a response should encode a unique key as a message # field ('ackid'). diff --git a/community/front-end/ofe/website/ghpcfe/cluster_manager/cloud_info.py b/community/front-end/ofe/website/ghpcfe/cluster_manager/cloud_info.py index 05f603fb3a..bf563c54d7 100644 --- a/community/front-end/ofe/website/ghpcfe/cluster_manager/cloud_info.py +++ b/community/front-end/ofe/website/ghpcfe/cluster_manager/cloud_info.py @@ -37,13 +37,17 @@ "n2": defaultdict(lambda: "cascadelake"), "n2d": defaultdict(lambda: "zen2"), "n1": defaultdict(lambda: "x86_64"), + "c3": defaultdict(lambda: "sapphirerapids"), + "c3d": defaultdict(lambda: "zen2"), # Compute Optimized "c2": defaultdict(lambda: "cascadelake"), "c2d": defaultdict( lambda: "zen2" # TODO: Should be zen3, but CentOS7 doesn't have ), # a new enough kernel to recognize as such. "t2d": defaultdict(lambda: "zen2"), # TODO: Should also be zen3 + "h3": defaultdict(lambda: "sapphirerapids"), # Memory Optimized + "m2": defaultdict(lambda: "icelake"), "m2": defaultdict(lambda: "cascadelake"), "m1": defaultdict( lambda: "broadwell", @@ -57,7 +61,6 @@ def _get_arch_for_node_type_gcp(instance): try: - logger.info(instance.split("-")) family, group, _ = instance.split("-", maxsplit=2) return gcp_machine_table[family][group] except ValueError: @@ -246,7 +249,7 @@ def get_region_zone_info(cloud_provider, credentials): if cloud_provider == "GCP": return _get_gcp_region_zone_info(credentials, ttl_hash=_get_ttl_hash()) else: - raise Exception("Unsupport Cloud Provider") + raise Exception("Unsupported Cloud Provider") def _get_gcp_subnets(credentials): @@ -274,7 +277,7 @@ def get_subnets(cloud_provider, credentials): if cloud_provider == "GCP": return _get_gcp_subnets(credentials) else: - raise Exception("Unsupport Cloud Provider") + raise Exception("Unsupported Cloud Provider") _gcp_services_list = None @@ -354,12 +357,16 @@ def get_cpu_price(num_cores, instance_type, skus): instance_description_mapper = { "e2": "E2 Instance Core", "n2d": "N2D AMD Instance Core", + "h3": "Compute optimized Core", + "c3": "Compute optimized Core", "c2": "Compute optimized Core", "c2d": "C2D AMD Instance Core", + "c3d": "C3D AMD Instance Core", "t2d": "T2D AMD Instance Core", "a2": "A2 Instance Core", "m1": "Memory-optimized Instance Core", # ?? "m2": "Memory Optimized Upgrade Premium for Memory-optimized Instance Core", # pylint: disable=line-too-long + "m3": "Memory-optimized Instance Core", "n2": "N2 Instance Core", "n1": "Custom Instance Core", # ?? } @@ -400,10 +407,15 @@ def get_mem_price(num_gb, instance_type, skus): "e2": "E2 Instance Ram", "n2d": "N2D AMD Instance Ram", "c2": "Compute optimized Ram", + "c3": "Compute optimized Ram", + "h3": "Compute optimized Ram", "c2d": "C2D AMD Instance Ram", + "c3d": "C3D AMD Instance Ram", "t2d": "T2D AMD Instance Ram", "a2": "A2 Instance Ram", - "m1": "Memory-optimized Instance Ram", # ?? + "m1": "Memory-optimized Instance Ram", + "m2": "Memory-optimized Instance Ram", + "m3": "Memory-optimized Instance Ram", # ?? "n2": "N2 Instance Ram", "n1": "Custom Instance Ram", # ?? } @@ -585,7 +597,7 @@ def get_gcp_workbench_region_zone_info( def get_gcp_filestores(credentials): - """Returns an array of Filestore instance informations + """Returns an array of Filestore instance information E.g. [ {'createTime': ..., diff --git a/community/front-end/ofe/website/ghpcfe/cluster_manager/clusterinfo.py b/community/front-end/ofe/website/ghpcfe/cluster_manager/clusterinfo.py index 31ed09cf31..401e37f4d8 100644 --- a/community/front-end/ofe/website/ghpcfe/cluster_manager/clusterinfo.py +++ b/community/front-end/ofe/website/ghpcfe/cluster_manager/clusterinfo.py @@ -54,7 +54,7 @@ class ClusterInfo: def __init__(self, cluster): self.config = utils.load_config() - self.ghpc_path = self.config["baseDir"].parent.parent / "ghpc" + self.ghpc_path = "/opt/gcluster/hpc-toolkit/ghpc" self.cluster = cluster self.cluster_dir = ( @@ -81,19 +81,23 @@ def prepare(self, credentials): The required credentials can be obtained from the cloud provider's dashboard or by following the documentation for obtaining authentication credentials. """ - self._create_cluster_dir() - self._set_credentials(credentials) - self.update() + #self._create_cluster_dir() + #self._set_credentials(credentials) + #self.update() def update(self): self._prepare_ghpc_yaml() self._prepare_bootstrap_gcs() - def start_cluster(self): + def start_cluster(self, credentials): self.cluster.cloud_state = "nm" self.cluster.status = "c" self.cluster.save() + self._create_cluster_dir() + self._set_credentials(credentials) + self.update() + try: self._run_ghpc() self._initialize_terraform() @@ -111,6 +115,23 @@ def start_cluster(self): self.cluster.save() raise + def reconfigure_cluster(self): + try: + self._run_ghpc() + self._initialize_terraform() + self._apply_terraform() + self.cluster.status = "r" + self.cluster.cloud_state = "m" + self.cluster.save() + + # Not a lot we can do if terraform fails, it's on the admin user to + # investigate and fix the errors shown in the log + except Exception: # pylint: disable=broad-except + self.cluster.status = "e" + self.cluster.cloud_state = "nm" + self.cluster.save() + raise + def stop_cluster(self): self._destroy_terraform() @@ -118,7 +139,10 @@ def get_cluster_access_key(self): return self.cluster.get_access_key() def _create_cluster_dir(self): - self.cluster_dir.mkdir(parents=True) + try: + self.cluster_dir.mkdir(parents=True) + except FileExistsError: + pass # Do nothing if the directory already exists def _get_credentials_file(self): return self.cluster_dir / "cloud_credentials" @@ -137,24 +161,29 @@ def _set_credentials(self, creds=None): def _create_ssh_key(self, target_dir): # ssh-keygen -t rsa -f /.ssh/id_rsa -N "" sshdir = target_dir / ".ssh" - sshdir.mkdir(mode=0o711) - - priv_key_file = sshdir / "id_rsa" - - subprocess.run( - [ - "ssh-keygen", - "-t", - "rsa", - "-f", - priv_key_file.as_posix(), - "-N", - "", - "-C", - "citc@mgmt", - ], - check=True, - ) + + if not sshdir.exists(): + sshdir.mkdir(mode=0o711) + + priv_key_file = sshdir / "id_rsa" + + subprocess.run( + [ + "ssh-keygen", + "-t", + "rsa", + "-f", + priv_key_file.as_posix(), + "-N", + "", + "-C", + "citc@mgmt", + ], + check=True, + ) + else: + # Directory already exists, no need to create it again + pass def _prepare_ghpc_filesystems(self): yaml = [] @@ -197,6 +226,21 @@ def _prepare_ghpc_partitions(self, part_uses): project: {self.cluster.project_id}""" else: instance_image_yaml = "" + + if part.additional_disk_count > 0 and part.additional_disk_count is not None: + additional_disks_yaml = f"""additional_disks: +""" + for disk in range(part.additional_disk_count): + additional_disks_yaml += f""" - device_name: disk{disk} + disk_name: null + disk_size_gb: {part.additional_disk_size} + disk_type: {part.additional_disk_type} + disk_labels: {{}} + auto_delete: {part.additional_disk_auto_delete} + boot: false\n""" + else: + additional_disks_yaml = "" + yaml.append( f""" - source: community/modules/compute/schedmd-slurm-gcp-v5-partition @@ -219,7 +263,10 @@ def _prepare_ghpc_partitions(self, part_uses): machine_type: {part.machine_type} node_count_dynamic_max: {part.dynamic_node_count} node_count_static: {part.static_node_count} + disk_size_gb: {part.boot_disk_size} + disk_type: {part.boot_disk_type} {instance_image_yaml} + {additional_disks_yaml} """ ) @@ -292,7 +339,22 @@ def _prepare_ghpc_yaml(self): """ else: controller_image_yaml = "" - + + if self.cluster.use_cloudsql: + controller_uses = self._yaml_refs_to_uses( + ["hpc_network"] + partitions_references + filesystems_references + ["slurm-sql"] + ) + slurmdbd_cloudsql_yaml = f""" - source: community/modules/database/slurm-cloudsql-federation + kind: terraform + id: slurm-sql + use: [hpc_network] + settings: + sql_instance_name: sql-{self.cluster.cloud_id} + tier: "db-g1-small" + """ + else: + slurmdbd_cloudsql_yaml = "" + with yaml_file.open("w") as f: f.write( f""" @@ -303,6 +365,11 @@ def _prepare_ghpc_yaml(self): deployment_name: {self.cluster.cloud_id} region: {self.cluster.cloud_region} zone: {self.cluster.cloud_zone} + enable_reconfigure: True + enable_cleanup_compute: False + enable_cleanup_subscriptions: True + enable_bigquery_load: {self.cluster.use_bigquery} + instance_image_custom: True labels: created_by: {SITE_NAME} @@ -330,8 +397,7 @@ def _prepare_ghpc_yaml(self): - monitoring.metricWriter - logging.logWriter - storage.objectAdmin - - pubsub.publisher - - pubsub.subscriber + - pubsub.admin - compute.securityAdmin - iam.serviceAccountAdmin - resourcemanager.projectIamAdmin @@ -339,12 +405,12 @@ def _prepare_ghpc_yaml(self): {partitions_yaml} +{slurmdbd_cloudsql_yaml} + - source: community/modules/scheduler/schedmd-slurm-gcp-v5-controller kind: terraform id: slurm_controller settings: - enable_cleanup_compute: True - enable_cleanup_subscriptions: True cloud_parameters: resume_rate: 0 resume_timeout: 500 @@ -370,8 +436,6 @@ def _prepare_ghpc_yaml(self): compute_startup_script: | #!/bin/bash gsutil cp gs://{startup_bucket}/clusters/{self.cluster.id}/bootstrap_compute.sh - | bash -#TODO: enable_cleanup_compute: True -#TODO: enable_cleanup_subscriptions: True use: {controller_uses} @@ -467,7 +531,7 @@ def _run_ghpc(self): with log_out_fn.open("wb") as log_out: with log_err_fn.open("wb") as log_err: subprocess.run( - [self.ghpc_path.as_posix(), "create", "cluster.yaml"], + [self.ghpc_path, "create", "cluster.yaml","-w"], cwd=target_dir, stdout=log_out, stderr=log_err, @@ -527,7 +591,19 @@ def model_from_tf(tf): except (KeyError, IndexError): pass - return ComputeInstance(**ci_kwargs) + # Check if a model with the same attributes exists + try: + existing_instance = ComputeInstance.objects.get( + internal_ip=ci_kwargs["internal_ip"] + ) + # If the instance already exists, update its attributes + for key, value in ci_kwargs.items(): + setattr(existing_instance, key, value) + existing_instance.save() + return existing_instance # Return the existing instance + except ComputeInstance.DoesNotExist: + # If the instance doesn't exist, create a new one + return ComputeInstance(**ci_kwargs) return [model_from_tf(instance) for instance in tf_nodes] @@ -603,8 +679,13 @@ def _apply_terraform(self): state = json.load(statefp) # Apply Perms to the service accounts - service_accounts = self._get_service_accounts(state) - self._apply_service_account_permissions(service_accounts) + try: + service_accounts = self._get_service_accounts(state) + self._apply_service_account_permissions(service_accounts) + except Exception as e: + # Be nicer to the user and continue creating cluster + logger.warning(f"An error occurred while applying permissions to service accounts: {e}") + # Cluster is now being initialized self.cluster.internal_name = self.cluster.name @@ -623,7 +704,7 @@ def _apply_terraform(self): ) if len(mgmt_nodes) != 1: logger.warning( - "Found %d contoller nodes, there should be only 1", + "Found %d controller nodes, there should be only 1", len(mgmt_nodes), ) if len(mgmt_nodes): @@ -665,13 +746,12 @@ def _apply_terraform(self): except subprocess.CalledProcessError as err: # We can error during provisioning, in which case Terraform - # doesn't tear things down. Run a `destroy`, just in case + # doesn't tear things down. logger.error("Terraform apply failed", exc_info=err) if err.stdout: logger.info("TF stdout:\n%s\n", err.stdout.decode("utf-8")) if err.stderr: logger.info("TF stderr:\n%s\n", err.stderr.decode("utf-8")) - self._destroy_terraform() raise def _destroy_terraform(self): diff --git a/community/front-end/ofe/website/ghpcfe/cluster_manager/filesystem.py b/community/front-end/ofe/website/ghpcfe/cluster_manager/filesystem.py index 33d09312bd..b77c454b12 100644 --- a/community/front-end/ofe/website/ghpcfe/cluster_manager/filesystem.py +++ b/community/front-end/ofe/website/ghpcfe/cluster_manager/filesystem.py @@ -90,7 +90,7 @@ def create_filesystem(fs: Filesystem) -> None: def _run_ghpc(target_dir: Path) -> None: - ghpc_path = utils.load_config()["baseDir"].parent.parent / "ghpc" + ghpc_path = "/opt/gcluster/hpc-toolkit/ghpc" try: logger.info("Invoking ghpc create") @@ -99,7 +99,7 @@ def _run_ghpc(target_dir: Path) -> None: with log_out_fn.open("wb") as log_out: with log_err_fn.open("wb") as log_err: subprocess.run( - [ghpc_path.as_posix(), "create", "filesystem.yaml"], + [ghpc_path, "create", "filesystem.yaml"], cwd=target_dir, stdout=log_out, stderr=log_err, diff --git a/community/front-end/ofe/website/ghpcfe/cluster_manager/image.py b/community/front-end/ofe/website/ghpcfe/cluster_manager/image.py index 49fde4dc1d..032746a4f9 100644 --- a/community/front-end/ofe/website/ghpcfe/cluster_manager/image.py +++ b/community/front-end/ofe/website/ghpcfe/cluster_manager/image.py @@ -13,7 +13,7 @@ # limitations under the License. ''' -This is a backend part of custom image creation fuctionality. +This is a backend part of custom image creation functionality. Frontend views will talk with functions here to perform real actions. ''' @@ -33,7 +33,7 @@ class ImageBackend: def __init__(self, image): self.config = utils.load_config() - self.ghpc_path = self.config["baseDir"].parents[1] / "ghpc" + self.ghpc_path = "/opt/gcluster/hpc-toolkit/ghpc" self.image = image self.image_dir = ( @@ -177,7 +177,7 @@ def _run_ghpc(self): with log_out_fn.open("wb") as log_out: with log_err_fn.open("wb") as log_err: subprocess.run( - [self.ghpc_path.as_posix(), "create", "image.yaml"], + [self.ghpc_path, "create", "image.yaml"], cwd=target_dir, stdout=log_out, stderr=log_err, diff --git a/community/front-end/ofe/website/ghpcfe/cluster_manager/spack.py b/community/front-end/ofe/website/ghpcfe/cluster_manager/spack.py index dcdcbdf780..f075c4b283 100644 --- a/community/front-end/ofe/website/ghpcfe/cluster_manager/spack.py +++ b/community/front-end/ofe/website/ghpcfe/cluster_manager/spack.py @@ -36,7 +36,7 @@ def get_package_list(): def get_package_info(names): - pkgs = [spack.repo.get(name) for name in names] + pkgs = [spack.repo.PATH.get_pkg_class(name) for name in names] return ( { "name": pkg.name, diff --git a/community/front-end/ofe/website/ghpcfe/cluster_manager/utils.py b/community/front-end/ofe/website/ghpcfe/cluster_manager/utils.py index 990b7f76d2..d2ebdc7549 100644 --- a/community/front-end/ofe/website/ghpcfe/cluster_manager/utils.py +++ b/community/front-end/ofe/website/ghpcfe/cluster_manager/utils.py @@ -168,7 +168,7 @@ def rsync_dir( rsync_cmd.extend([src_dir, tgt_dir]) new_env = os.environ.copy() - # Don't have terraform try to re-use any existing SSH agent + # Don't have terraform try to reuse any existing SSH agent # It has its own keys if "SSH_AUTH_SOCK" in new_env: del new_env["SSH_AUTH_SOCK"] @@ -205,7 +205,7 @@ def run_terraform(target_dir, command, arguments=None, extra_env=None): log_err_fn = Path(target_dir) / f"terraform_{command}_log.stderr" new_env = os.environ.copy() - # Don't have terraform try to re-use any existing SSH agent + # Don't have terraform try to reuse any existing SSH agent # It has its own keys if "SSH_AUTH_SOCK" in new_env: del new_env["SSH_AUTH_SOCK"] diff --git a/community/front-end/ofe/website/ghpcfe/cluster_manager/vpc.py b/community/front-end/ofe/website/ghpcfe/cluster_manager/vpc.py index f50766bf56..addf72e8bb 100644 --- a/community/front-end/ofe/website/ghpcfe/cluster_manager/vpc.py +++ b/community/front-end/ofe/website/ghpcfe/cluster_manager/vpc.py @@ -157,7 +157,7 @@ def generate_vpc_tf_datablock(vpc: VirtualNetwork, target_dir: Path) -> Path: key = "name" else: raise NotImplementedError( - f"Cloud Provider {vpc.cloud_provider} not yet implmeneted" + f"Cloud Provider {vpc.cloud_provider} not yet implemented" ) with output_file.open("w") as fp: fp.write( @@ -180,7 +180,7 @@ def generate_subnet_tf_datablock( key = "name" else: raise NotImplementedError( - f"Cloud Provider {subnet.cloud_provider} not yet implmeneted" + f"Cloud Provider {subnet.cloud_provider} not yet implemented" ) with output_file.open("w") as fp: fp.write( diff --git a/community/front-end/ofe/website/ghpcfe/cluster_manager/workbenchinfo.py b/community/front-end/ofe/website/ghpcfe/cluster_manager/workbenchinfo.py index 1c95943ba5..55db08eab6 100644 --- a/community/front-end/ofe/website/ghpcfe/cluster_manager/workbenchinfo.py +++ b/community/front-end/ofe/website/ghpcfe/cluster_manager/workbenchinfo.py @@ -176,7 +176,7 @@ def copy_startup_script(self): with startup_script.open("w") as f: f.write( f"""#!/bin/bash -echo "starting starup script at `date`" | tee -a /tmp/startup.log +echo "starting startup script at `date`" | tee -a /tmp/startup.log echo "Getting username..." | tee -a /tmp/startup.log {startup_script_vars} diff --git a/community/front-end/ofe/website/ghpcfe/forms.py b/community/front-end/ofe/website/ghpcfe/forms.py index 9b6d4f68aa..ad8ba6e721 100644 --- a/community/front-end/ofe/website/ghpcfe/forms.py +++ b/community/front-end/ofe/website/ghpcfe/forms.py @@ -115,25 +115,8 @@ def _get_creds(self, kwargs): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - credential = self._get_creds(kwargs) - self.fields["subnet"].queryset = VirtualSubnet.objects.filter( - cloud_credential=credential - ).filter(Q(cloud_state="i") | Q(cloud_state="m")) - - if self.instance.cloud_state not in ["nm"]: - # Need to disable things - for field in self.fields.keys(): - self.field[field].disabled = True - - self.fields["cloud_zone"].widget.choices = [ - ( - self.instance.cloud_zone, - self.instance.cloud_zone - ) - ] - - # For machine types, will use JS to get valid types dependant on + # For machine types, will use JS to get valid types dependent on # cloud zone. So bypass cleaning and choices def prep_dynamic_select(field, value): self.fields[field].widget.choices = [ @@ -158,15 +141,26 @@ def prep_dynamic_select(field, value): self.instance.login_node_disk_type ) + # If cluster is running make some of form field ready only. + if self.instance.status == "r": + logger.info("Cluster is running making some fields ready only") + # Define a list of field names you want to set as readonly + fields_to_make_readonly = ['cloud_credential', 'name', 'subnet', 'cloud_region', 'cloud_zone'] + + # Loop through the fields and set the 'readonly' attribute + for field_name in fields_to_make_readonly: + self.fields[field_name].widget = forms.TextInput(attrs={'class': 'form-control'}) + self.fields[field_name].widget.attrs['readonly'] = True class Meta: model = Cluster fields = ( + "cloud_credential", "name", "subnet", + "cloud_region", "cloud_zone", - "cloud_credential", "authorised_users", "spackdir", "controller_instance_type", @@ -178,15 +172,20 @@ class Meta: "login_node_disk_size", "login_node_image", "controller_node_image", + "use_cloudsql", + "use_bigquery", ) widgets = { "name": forms.TextInput(attrs={"class": "form-control"}), "cloud_credential": forms.Select( - attrs={"class": "form-control", "disabled": True} + attrs={"class": "form-control"} ), "subnet": forms.Select(attrs={"class": "form-control"}), + "cloud_region": forms.Select(attrs={"class": "form-control", "readonly": "readonly"}), "cloud_zone": forms.Select(attrs={"class": "form-control"}), + "authorised_users": forms.SelectMultiple(attrs={"class": "form-control"}), + "spackdir": forms.TextInput(attrs={"class": "form-control"}), "controller_instance_type": forms.Select( attrs={"class": "form-control machine_type_select"} ), @@ -216,6 +215,8 @@ class Meta: "id": "controller-node-image", "name": "controller_node_image", "value": "",}), + "use_cloudsql": forms.CheckboxInput(attrs={"class": "required checkbox"}), + "use_bigquery": forms.CheckboxInput(attrs={"class": "required checkbox"}), } @@ -233,7 +234,7 @@ def __init__(self, *args, **kwargs): class ClusterPartitionForm(forms.ModelForm): - """Form for Cluster Paritions""" + """Form for Cluster Partitions""" machine_type = forms.ChoiceField(widget=forms.Select()) GPU_type = forms.ChoiceField(widget=forms.Select()) # pylint: disable=invalid-name @@ -251,6 +252,12 @@ class Meta: "enable_node_reuse", "GPU_type", "GPU_per_node", + "boot_disk_type", + "boot_disk_size", + "additional_disk_type", + "additional_disk_count", + "additional_disk_size", + "additional_disk_auto_delete" ) def __init__(self, *args, **kwargs): @@ -262,24 +269,39 @@ def __init__(self, *args, **kwargs): self.fields[field].widget.attrs.update( {"title": self.fields[field].help_text} ) + + self.fields["boot_disk_type"].widget = forms.Select(attrs={"class": "form-control disk_type_select"}) + self.fields["additional_disk_type"].widget = forms.Select(attrs={"class": "form-control disk_type_select"}) self.fields["machine_type"].widget.attrs[ "class" ] += " machine_type_select" - # NOTE: This is a just a hack... - # We need to set choices such that the current value is valid, - # so that the current 'value' is valid, and gets selected. - self.fields["machine_type"].widget.choices = [ - (self.instance.machine_type, self.instance.machine_type) - ] - self.fields["GPU_type"].widget.choices = [ - (self.instance.GPU_type, self.instance.GPU_type) - ] - # NOTE: Hack to bypass cleaning 'machine_type' & GPU_type here, - # and do so in form_valid - self.fields["machine_type"].clean = lambda value: value - self.fields["GPU_type"].clean = lambda value: value + def prep_dynamic_select(field, value): + self.fields[field].widget.choices = [ + ( value, value ) + ] + self.fields[field].clean = lambda value: value + + prep_dynamic_select( + "boot_disk_type", + self.instance.boot_disk_type + ) + + prep_dynamic_select( + "additional_disk_type", + self.instance.additional_disk_type + ) + + prep_dynamic_select( + "machine_type", + self.instance.machine_type + ) + + prep_dynamic_select( + "GPU_type", + self.instance.GPU_type + ) def clean(self): cleaned_data = super().clean() diff --git a/community/front-end/ofe/website/ghpcfe/grafana.py b/community/front-end/ofe/website/ghpcfe/grafana.py index 6ea9962e2a..94e4d05fd0 100644 --- a/community/front-end/ofe/website/ghpcfe/grafana.py +++ b/community/front-end/ofe/website/ghpcfe/grafana.py @@ -327,7 +327,7 @@ def create_cluster_dashboard(cluster): "uid": None, "title": f"Cluster {cluster.name}", "panels": panels, - "verison": 0, + "version": 0, }, "filderId": 0, "overwrite": True, diff --git a/community/front-end/ofe/website/ghpcfe/management/commands/custom_setup_command.py b/community/front-end/ofe/website/ghpcfe/management/commands/custom_setup_command.py index b40776ab43..bc08c507c3 100644 --- a/community/front-end/ofe/website/ghpcfe/management/commands/custom_setup_command.py +++ b/community/front-end/ofe/website/ghpcfe/management/commands/custom_setup_command.py @@ -82,4 +82,4 @@ def handle(self, *args, **kwargs): socialapp.save() socialapp.sites.add(site) except Exception as err: - raise CommandError("Initalization failed.") from err + raise CommandError("Initialization failed.") from err diff --git a/community/front-end/ofe/website/ghpcfe/models.py b/community/front-end/ofe/website/ghpcfe/models.py index c5b544e234..9e88b2e800 100644 --- a/community/front-end/ofe/website/ghpcfe/models.py +++ b/community/front-end/ofe/website/ghpcfe/models.py @@ -39,6 +39,7 @@ ("nm", "New"), # Just defined, managed ("cm", "Creating"), # In the process of creating, managed ("m", "Managed/Running"), # Created, operational, managed + ("re", "Reconfiguring"), # Created, operational, managed ("dm", "Destroying"), # In the process of deleting, managed ("xm", "Destroyed"), # Deleted, managed ("um", "Unknown"), # Unknown, following error @@ -328,7 +329,7 @@ class VirtualNetwork(CloudResource): validators=[ RFC1035Validator( 63, - "VPC Name must be RFC1035 Compliant (lower case, alpha-numeric " + "VPC Name must be RFC1035 Compliant (lower case, alphanumeric " "with hyphens)", ) ], @@ -361,7 +362,7 @@ class VirtualSubnet(CloudResource): RFC1035Validator( 63, "Subnet Name must be RFC1035 Compliant (lower case, " - " alpha-numeric with hyphens)", + " alphanumeric with hyphens)", ) ], ) @@ -546,7 +547,7 @@ def __str__(self): ) description = models.TextField( max_length=4000, - help_text="(Optional) description of this stratup script", + help_text="(Optional) description of this startup script", blank=True, null=True, ) @@ -601,7 +602,7 @@ class Image(CloudResource): source_image_family = models.CharField( max_length=60, - help_text="Enter a soure image family", + help_text="Enter a source image family", blank=False, default="schedmd-v5-slurm-22-05-8-rocky-linux-8", ) @@ -613,7 +614,7 @@ class Image(CloudResource): enable_os_login = models.CharField( max_length=5, - help_text="Enable OS Login durring the image creation?", + help_text="Enable OS Login during the image creation?", choices=(("TRUE", "TRUE"),("FALSE", "FALSE")), default="TRUE", ) @@ -661,7 +662,7 @@ class Cluster(CloudResource): RFC1035Validator( 17, "Cluster Name must be RFC1035 Compliant (lower case, " - "alpha-numeric with hyphens)", + "alphanumeric with hyphens)", ) ], ) @@ -689,13 +690,14 @@ class Cluster(CloudResource): ("c", "Cluster is being created"), ("i", "Cluster is being initialised"), ("r", "Cluster is ready for jobs"), + ("re", "Cluster is reconfiguring"), ("s", "Cluster is stopped (can be restarted)"), ("t", "Cluster is terminating"), ("e", "Cluster deployment has failed"), ("d", "Cluster has been deleted"), ) status = models.CharField( - max_length=1, + max_length=2, choices=CLUSTER_STATUS, default="n", help_text="Status of this cluster", @@ -788,6 +790,18 @@ class Cluster(CloudResource): default=None, on_delete=models.SET_NULL, ) + use_cloudsql = models.BooleanField( + default=False, + help_text=( + "Would you like to use Cloud SQL for Slurm accounting database?" + ), + ) + use_bigquery = models.BooleanField( + default=False, + help_text=( + "Would you like to send Slurm accounting data to BigQuery?" + ), + ) def get_access_key(self): return Token.objects.get(user=self.owner) @@ -853,7 +867,7 @@ class ComputeInstance(CloudResource): class ClusterPartition(models.Model): - """Compute partition on a clustero""" + """Compute partition on a cluster""" # Define the regex pattern validator name_validator = RegexValidator( @@ -896,7 +910,7 @@ class ClusterPartition(models.Model): default=0, ) enable_placement = models.BooleanField( - default=True, + default=False, help_text=( "Enable Placement Groups (currently only valid for C2, C2D and C3" "instances)" @@ -917,6 +931,17 @@ class ClusterPartition(models.Model): help_text="The number of vCPU per node of the partition", default=1, ) + boot_disk_type = models.CharField( + max_length=30, + help_text="GCP Persistent Disk type", + default="pd-standard", + ) + boot_disk_size = models.PositiveIntegerField( + validators=[MinValueValidator(49)], + help_text="Boot disk size (in GB)", + default=50, + blank=True, + ) GPU_per_node = models.PositiveIntegerField( # pylint: disable=invalid-name validators=[MinValueValidator(0)], help_text="The number of GPU per node of the partition", @@ -925,10 +950,36 @@ class ClusterPartition(models.Model): GPU_type = models.CharField( # pylint: disable=invalid-name max_length=64, blank=True, default="", help_text="GPU device type" ) + additional_disk_count = models.PositiveIntegerField( + help_text="How many additional disks?", + default=0, + blank=True, + ) + additional_disk_type = models.CharField( + max_length=30, + blank=True, + help_text="Additional Disk type", + default="pd-standard", + ) + additional_disk_size = models.PositiveIntegerField( + help_text="Disk size (in GB)", + default=375, + blank=True, + ) + additional_disk_auto_delete = models.BooleanField( + default=True, + help_text=( + "Automatically delete additional disk when node is deleted?" + ), + ) def __str__(self): return self.name + def clean(self): + if self.enable_placement and self.enable_node_reuse: + raise ValidationError("You cannot enable both Placement Groups and Node Reuse simultaneously.") + class ApplicationInstallationLocation(models.Model): """User managed application support""" @@ -1456,7 +1507,7 @@ class Workbench(CloudResource): RFC1035Validator( 63, "Workbench Name must be RFC1035 Compliant (lower-case " - "alpha-numeric with hyphens)", + "alphanumeric with hyphens)", ) ], ) diff --git a/community/front-end/ofe/website/ghpcfe/signals.py b/community/front-end/ofe/website/ghpcfe/signals.py index fc14cbc040..a76de84c36 100644 --- a/community/front-end/ofe/website/ghpcfe/signals.py +++ b/community/front-end/ofe/website/ghpcfe/signals.py @@ -32,7 +32,8 @@ def sync_vnet_subnet_state(sender, **kwargs): @receiver(post_delete, sender=Cluster) def delete_cluster_extras(sender, **kwargs): cluster = kwargs["instance"] - cluster.shared_fs.delete() + if cluster.shared_fs: + cluster.shared_fs.delete() if cluster.controller_node: cluster.controller_node.delete() diff --git a/community/front-end/ofe/website/ghpcfe/static/css/jquery-ui.css b/community/front-end/ofe/website/ghpcfe/static/css/jquery-ui.css index 9eacce7fe4..80e8b5e840 100644 --- a/community/front-end/ofe/website/ghpcfe/static/css/jquery-ui.css +++ b/community/front-end/ofe/website/ghpcfe/static/css/jquery-ui.css @@ -14,10 +14,10 @@ * limitations under the License. */ -/*! jQuery UI - v1.12.1 - 2016-09-14 +/*! jQuery UI - v1.13.2 - 2022-07-14 * http://jqueryui.com * Includes: core.css, accordion.css, autocomplete.css, menu.css, button.css, controlgroup.css, checkboxradio.css, datepicker.css, dialog.css, draggable.css, resizable.css, progressbar.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css -* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Arial%2CHelvetica%2Csans-serif&fsDefault=1em&fwDefault=normal&cornerRadius=3px&bgColorHeader=e9e9e9&bgTextureHeader=flat&borderColorHeader=dddddd&fcHeader=333333&iconColorHeader=444444&bgColorContent=ffffff&bgTextureContent=flat&borderColorContent=dddddd&fcContent=333333&iconColorContent=444444&bgColorDefault=f6f6f6&bgTextureDefault=flat&borderColorDefault=c5c5c5&fcDefault=454545&iconColorDefault=777777&bgColorHover=ededed&bgTextureHover=flat&borderColorHover=cccccc&fcHover=2b2b2b&iconColorHover=555555&bgColorActive=007fff&bgTextureActive=flat&borderColorActive=003eff&fcActive=ffffff&iconColorActive=ffffff&bgColorHighlight=fffa90&bgTextureHighlight=flat&borderColorHighlight=dad55e&fcHighlight=777620&iconColorHighlight=777620&bgColorError=fddfdf&bgTextureError=flat&borderColorError=f1a899&fcError=5f3f3f&iconColorError=cc0000&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=666666&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=5px&offsetTopShadow=0px&offsetLeftShadow=0px&cornerRadiusShadow=8px +* To view and modify this theme, visit http://jqueryui.com/themeroller/?bgShadowXPos=&bgOverlayXPos=&bgErrorXPos=&bgHighlightXPos=&bgContentXPos=&bgHeaderXPos=&bgActiveXPos=&bgHoverXPos=&bgDefaultXPos=&bgShadowYPos=&bgOverlayYPos=&bgErrorYPos=&bgHighlightYPos=&bgContentYPos=&bgHeaderYPos=&bgActiveYPos=&bgHoverYPos=&bgDefaultYPos=&bgShadowRepeat=&bgOverlayRepeat=&bgErrorRepeat=&bgHighlightRepeat=&bgContentRepeat=&bgHeaderRepeat=&bgActiveRepeat=&bgHoverRepeat=&bgDefaultRepeat=&iconsHover=url(%22images%2Fui-icons_555555_256x240.png%22)&iconsHighlight=url(%22images%2Fui-icons_777620_256x240.png%22)&iconsHeader=url(%22images%2Fui-icons_444444_256x240.png%22)&iconsError=url(%22images%2Fui-icons_cc0000_256x240.png%22)&iconsDefault=url(%22images%2Fui-icons_777777_256x240.png%22)&iconsContent=url(%22images%2Fui-icons_444444_256x240.png%22)&iconsActive=url(%22images%2Fui-icons_ffffff_256x240.png%22)&bgImgUrlShadow=&bgImgUrlOverlay=&bgImgUrlHover=&bgImgUrlHighlight=&bgImgUrlHeader=&bgImgUrlError=&bgImgUrlDefault=&bgImgUrlContent=&bgImgUrlActive=&opacityFilterShadow=Alpha(Opacity%3D30)&opacityFilterOverlay=Alpha(Opacity%3D30)&opacityShadowPerc=30&opacityOverlayPerc=30&iconColorHover=%23555555&iconColorHighlight=%23777620&iconColorHeader=%23444444&iconColorError=%23cc0000&iconColorDefault=%23777777&iconColorContent=%23444444&iconColorActive=%23ffffff&bgImgOpacityShadow=0&bgImgOpacityOverlay=0&bgImgOpacityError=95&bgImgOpacityHighlight=55&bgImgOpacityContent=75&bgImgOpacityHeader=75&bgImgOpacityActive=65&bgImgOpacityHover=75&bgImgOpacityDefault=75&bgTextureShadow=flat&bgTextureOverlay=flat&bgTextureError=flat&bgTextureHighlight=flat&bgTextureContent=flat&bgTextureHeader=flat&bgTextureActive=flat&bgTextureHover=flat&bgTextureDefault=flat&cornerRadius=3px&fwDefault=normal&ffDefault=Arial%2CHelvetica%2Csans-serif&fsDefault=1em&cornerRadiusShadow=8px&thicknessShadow=5px&offsetLeftShadow=0px&offsetTopShadow=0px&opacityShadow=.3&bgColorShadow=%23666666&opacityOverlay=.3&bgColorOverlay=%23aaaaaa&fcError=%235f3f3f&borderColorError=%23f1a899&bgColorError=%23fddfdf&fcHighlight=%23777620&borderColorHighlight=%23dad55e&bgColorHighlight=%23fffa90&fcContent=%23333333&borderColorContent=%23dddddd&bgColorContent=%23ffffff&fcHeader=%23333333&borderColorHeader=%23dddddd&bgColorHeader=%23e9e9e9&fcActive=%23ffffff&borderColorActive=%23003eff&bgColorActive=%23007fff&fcHover=%232b2b2b&borderColorHover=%23cccccc&bgColorHover=%23ededed&fcDefault=%23454545&borderColorDefault=%23c5c5c5&bgColorDefault=%23f6f6f6 * Copyright jQuery Foundation and other contributors; Licensed MIT */ /* Layout helpers @@ -61,7 +61,7 @@ left: 0; position: absolute; opacity: 0; - filter:Alpha(Opacity=0); /* support: IE8 */ + -ms-filter: "alpha(opacity=0)"; /* support: IE8 */ } .ui-front { @@ -680,7 +680,7 @@ button.ui-button::-moz-focus-inner { .ui-progressbar .ui-progressbar-overlay { background: url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw=="); height: 100%; - filter: alpha(opacity=25); /* support: IE8 */ + -ms-filter: "alpha(opacity=25)"; /* support: IE8 */ opacity: 0.25; } .ui-progressbar-indeterminate .ui-progressbar-value { @@ -744,7 +744,7 @@ button.ui-button::-moz-focus-inner { z-index: 2; width: 1.2em; height: 1.2em; - cursor: default; + cursor: pointer; -ms-touch-action: none; touch-action: none; } @@ -896,6 +896,7 @@ button.ui-button::-moz-focus-inner { body .ui-tooltip { border-width: 2px; } + /* Component containers ----------------------------------*/ .ui-widget { @@ -1056,18 +1057,18 @@ a.ui-button:active, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; - filter:Alpha(Opacity=70); /* support: IE8 */ + -ms-filter: "alpha(opacity=70)"; /* support: IE8 */ font-weight: normal; } .ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; - filter:Alpha(Opacity=35); /* support: IE8 */ + -ms-filter: "alpha(opacity=35)"; /* support: IE8 */ background-image: none; } .ui-state-disabled .ui-icon { - filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */ + -ms-filter: "alpha(opacity=35)"; /* support: IE8 - See #6059 */ } /* Icons @@ -1108,7 +1109,10 @@ a.ui-button:active, } /* positioning */ -.ui-icon-blank { background-position: 16px 16px; } +/* Three classes needed to override `.ui-button:hover .ui-icon` */ +.ui-icon-blank.ui-icon-blank.ui-icon-blank { + background-image: none; +} .ui-icon-caret-1-n { background-position: 0 0; } .ui-icon-caret-1-ne { background-position: -16px 0; } .ui-icon-caret-1-e { background-position: -32px 0; } @@ -1318,8 +1322,8 @@ a.ui-button:active, /* Overlays */ .ui-widget-overlay { background: #aaaaaa; - opacity: .3; - filter: Alpha(Opacity=30); /* support: IE8 */ + opacity: .003; + -ms-filter: Alpha(Opacity=.3); /* support: IE8 */ } .ui-widget-shadow { -webkit-box-shadow: 0px 0px 5px #666666; diff --git a/community/front-end/ofe/website/ghpcfe/static/js/jquery-ui.js b/community/front-end/ofe/website/ghpcfe/static/js/jquery-ui.js index c94965d64f..c9c0c4ade7 100644 --- a/community/front-end/ofe/website/ghpcfe/static/js/jquery-ui.js +++ b/community/front-end/ofe/website/ghpcfe/static/js/jquery-ui.js @@ -14,30 +14,33 @@ * limitations under the License. */ -/*! jQuery UI - v1.12.1 - 2016-09-14 +/*! jQuery UI - v1.13.2 - 2022-07-14 * http://jqueryui.com -* Includes: widget.js, position.js, data.js, disable-selection.js, effect.js, effects/effect-blind.js, effects/effect-bounce.js, effects/effect-clip.js, effects/effect-drop.js, effects/effect-explode.js, effects/effect-fade.js, effects/effect-fold.js, effects/effect-highlight.js, effects/effect-puff.js, effects/effect-pulsate.js, effects/effect-scale.js, effects/effect-shake.js, effects/effect-size.js, effects/effect-slide.js, effects/effect-transfer.js, focusable.js, form-reset-mixin.js, jquery-1-7.js, keycode.js, labels.js, scroll-parent.js, tabbable.js, unique-id.js, widgets/accordion.js, widgets/autocomplete.js, widgets/button.js, widgets/checkboxradio.js, widgets/controlgroup.js, widgets/datepicker.js, widgets/dialog.js, widgets/draggable.js, widgets/droppable.js, widgets/menu.js, widgets/mouse.js, widgets/progressbar.js, widgets/resizable.js, widgets/selectable.js, widgets/selectmenu.js, widgets/slider.js, widgets/sortable.js, widgets/spinner.js, widgets/tabs.js, widgets/tooltip.js +* Includes: widget.js, position.js, data.js, disable-selection.js, effect.js, effects/effect-blind.js, effects/effect-bounce.js, effects/effect-clip.js, effects/effect-drop.js, effects/effect-explode.js, effects/effect-fade.js, effects/effect-fold.js, effects/effect-highlight.js, effects/effect-puff.js, effects/effect-pulsate.js, effects/effect-scale.js, effects/effect-shake.js, effects/effect-size.js, effects/effect-slide.js, effects/effect-transfer.js, focusable.js, form-reset-mixin.js, jquery-patch.js, keycode.js, labels.js, scroll-parent.js, tabbable.js, unique-id.js, widgets/accordion.js, widgets/autocomplete.js, widgets/button.js, widgets/checkboxradio.js, widgets/controlgroup.js, widgets/datepicker.js, widgets/dialog.js, widgets/draggable.js, widgets/droppable.js, widgets/menu.js, widgets/mouse.js, widgets/progressbar.js, widgets/resizable.js, widgets/selectable.js, widgets/selectmenu.js, widgets/slider.js, widgets/sortable.js, widgets/spinner.js, widgets/tabs.js, widgets/tooltip.js * Copyright jQuery Foundation and other contributors; Licensed MIT */ -(function( factory ) { +( function( factory ) { + "use strict"; + if ( typeof define === "function" && define.amd ) { // AMD. Register as an anonymous module. - define([ "jquery" ], factory ); + define( [ "jquery" ], factory ); } else { // Browser globals factory( jQuery ); } -}(function( $ ) { +} )( function( $ ) { +"use strict"; $.ui = $.ui || {}; -var version = $.ui.version = "1.12.1"; +var version = $.ui.version = "1.13.2"; /*! - * jQuery UI Widget 1.12.1 + * jQuery UI Widget 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -52,24 +55,20 @@ var version = $.ui.version = "1.12.1"; //>>demos: http://jqueryui.com/widget/ - var widgetUuid = 0; +var widgetHasOwnProperty = Array.prototype.hasOwnProperty; var widgetSlice = Array.prototype.slice; $.cleanData = ( function( orig ) { return function( elems ) { var events, elem, i; for ( i = 0; ( elem = elems[ i ] ) != null; i++ ) { - try { - - // Only trigger remove when necessary to save time - events = $._data( elem, "events" ); - if ( events && events.remove ) { - $( elem ).triggerHandler( "remove" ); - } - // Http://bugs.jquery.com/ticket/8235 - } catch ( e ) {} + // Only trigger remove when necessary to save time + events = $._data( elem, "events" ); + if ( events && events.remove ) { + $( elem ).triggerHandler( "remove" ); + } } orig( elems ); }; @@ -91,12 +90,12 @@ $.widget = function( name, base, prototype ) { base = $.Widget; } - if ( $.isArray( prototype ) ) { + if ( Array.isArray( prototype ) ) { prototype = $.extend.apply( null, [ {} ].concat( prototype ) ); } // Create selector for plugin - $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { + $.expr.pseudos[ fullName.toLowerCase() ] = function( elem ) { return !!$.data( elem, fullName ); }; @@ -105,7 +104,7 @@ $.widget = function( name, base, prototype ) { constructor = $[ namespace ][ name ] = function( options, element ) { // Allow instantiation without "new" keyword - if ( !this._createWidget ) { + if ( !this || !this._createWidget ) { return new constructor( options, element ); } @@ -136,7 +135,7 @@ $.widget = function( name, base, prototype ) { // inheriting from basePrototype.options = $.widget.extend( {}, basePrototype.options ); $.each( prototype, function( prop, value ) { - if ( !$.isFunction( value ) ) { + if ( typeof value !== "function" ) { proxiedPrototype[ prop ] = value; return; } @@ -215,7 +214,7 @@ $.widget.extend = function( target ) { for ( ; inputIndex < inputLength; inputIndex++ ) { for ( key in input[ inputIndex ] ) { value = input[ inputIndex ][ key ]; - if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { + if ( widgetHasOwnProperty.call( input[ inputIndex ], key ) && value !== undefined ) { // Clone objects if ( $.isPlainObject( value ) ) { @@ -264,7 +263,8 @@ $.widget.bridge = function( name, object ) { "attempted to call method '" + options + "'" ); } - if ( !$.isFunction( instance[ options ] ) || options.charAt( 0 ) === "_" ) { + if ( typeof instance[ options ] !== "function" || + options.charAt( 0 ) === "_" ) { return $.error( "no such method '" + options + "' for " + name + " widget instance" ); } @@ -525,12 +525,34 @@ $.Widget.prototype = { classes: this.options.classes || {} }, options ); + function bindRemoveEvent() { + var nodesToBind = []; + + options.element.each( function( _, element ) { + var isTracked = $.map( that.classesElementLookup, function( elements ) { + return elements; + } ) + .some( function( elements ) { + return elements.is( element ); + } ); + + if ( !isTracked ) { + nodesToBind.push( element ); + } + } ); + + that._on( $( nodesToBind ), { + remove: "_untrackClassesElement" + } ); + } + function processClassString( classes, checkOption ) { var current, i; for ( i = 0; i < classes.length; i++ ) { current = that.classesElementLookup[ classes[ i ] ] || $(); if ( options.add ) { - current = $( $.unique( current.get().concat( options.element.get() ) ) ); + bindRemoveEvent(); + current = $( $.uniqueSort( current.get().concat( options.element.get() ) ) ); } else { current = $( current.not( options.element ).get() ); } @@ -542,10 +564,6 @@ $.Widget.prototype = { } } - this._on( options.element, { - "remove": "_untrackClassesElement" - } ); - if ( options.keys ) { processClassString( options.keys.match( /\S+/g ) || [], true ); } @@ -563,6 +581,8 @@ $.Widget.prototype = { that.classesElementLookup[ key ] = $( value.not( event.target ).get() ); } } ); + + this._off( $( event.target ) ); }, _removeClass: function( element, keys, extra ) { @@ -643,7 +663,7 @@ $.Widget.prototype = { _off: function( element, eventName ) { eventName = ( eventName || "" ).split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace; - element.off( eventName ).off( eventName ); + element.off( eventName ); // Clear the stack to avoid memory leaks (#10056) this.bindings = $( this.bindings.not( element ).get() ); @@ -709,7 +729,7 @@ $.Widget.prototype = { } this.element.trigger( event, data ); - return !( $.isFunction( callback ) && + return !( typeof callback === "function" && callback.apply( this.element[ 0 ], [ event ].concat( data ) ) === false || event.isDefaultPrevented() ); } @@ -731,6 +751,8 @@ $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { options = options || {}; if ( typeof options === "number" ) { options = { duration: options }; + } else if ( options === true ) { + options = {}; } hasOptions = !$.isEmptyObject( options ); @@ -760,7 +782,7 @@ var widget = $.widget; /*! - * jQuery UI Position 1.12.1 + * jQuery UI Position 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -799,6 +821,10 @@ function parseCss( element, property ) { return parseInt( $.css( element, property ), 10 ) || 0; } +function isWindow( obj ) { + return obj != null && obj === obj.window; +} + function getDimensions( elem ) { var raw = elem[ 0 ]; if ( raw.nodeType === 9 ) { @@ -808,7 +834,7 @@ function getDimensions( elem ) { offset: { top: 0, left: 0 } }; } - if ( $.isWindow( raw ) ) { + if ( isWindow( raw ) ) { return { width: elem.width(), height: elem.height(), @@ -835,9 +861,9 @@ $.position = { return cachedScrollbarWidth; } var w1, w2, - div = $( "
" + - "
" ), + div = $( "
" + + "
" ), innerDiv = div.children()[ 0 ]; $( "body" ).append( div ); @@ -870,12 +896,12 @@ $.position = { }, getWithinInfo: function( element ) { var withinElement = $( element || window ), - isWindow = $.isWindow( withinElement[ 0 ] ), + isElemWindow = isWindow( withinElement[ 0 ] ), isDocument = !!withinElement[ 0 ] && withinElement[ 0 ].nodeType === 9, - hasOffset = !isWindow && !isDocument; + hasOffset = !isElemWindow && !isDocument; return { element: withinElement, - isWindow: isWindow, + isWindow: isElemWindow, isDocument: isDocument, offset: hasOffset ? $( element ).offset() : { left: 0, top: 0 }, scrollLeft: withinElement.scrollLeft(), @@ -895,7 +921,12 @@ $.fn.position = function( options ) { options = $.extend( {}, options ); var atOffset, targetWidth, targetHeight, targetOffset, basePosition, dimensions, - target = $( options.of ), + + // Make sure string options are treated as CSS selectors + target = typeof options.of === "string" ? + $( document ).find( options.of ) : + $( options.of ), + within = $.position.getWithinInfo( options.within ), scrollInfo = $.position.getScrollInfo( within ), collision = ( options.collision || "flip" ).split( " " ), @@ -1248,7 +1279,7 @@ var position = $.ui.position; /*! - * jQuery UI :data 1.12.1 + * jQuery UI :data 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -1262,7 +1293,7 @@ var position = $.ui.position; //>>docs: http://api.jqueryui.com/data-selector/ -var data = $.extend( $.expr[ ":" ], { +var data = $.extend( $.expr.pseudos, { data: $.expr.createPseudo ? $.expr.createPseudo( function( dataName ) { return function( elem ) { @@ -1277,7 +1308,7 @@ var data = $.extend( $.expr[ ":" ], { } ); /*! - * jQuery UI Disable Selection 1.12.1 + * jQuery UI Disable Selection 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -1292,7 +1323,6 @@ var data = $.extend( $.expr[ ":" ], { // This file is deprecated - var disableSelection = $.fn.extend( { disableSelection: ( function() { var eventType = "onselectstart" in document.createElement( "div" ) ? @@ -1312,56 +1342,37 @@ var disableSelection = $.fn.extend( { } ); -/*! - * jQuery UI Effects 1.12.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ - -//>>label: Effects Core -//>>group: Effects -// jscs:disable maximumLineLength -//>>description: Extends the internal jQuery effects. Includes morphing and easing. Required by all other effects. -// jscs:enable maximumLineLength -//>>docs: http://api.jqueryui.com/category/effects-core/ -//>>demos: http://jqueryui.com/effect/ - - -var dataSpace = "ui-effects-", - dataSpaceStyle = "ui-effects-style", - dataSpaceAnimated = "ui-effects-animated", +// Create a local jQuery because jQuery Color relies on it and the +// global may not exist with AMD and a custom build (#10199). +// This module is a noop if used as a regular AMD module. +// eslint-disable-next-line no-unused-vars +var jQuery = $; - // Create a local jQuery because jQuery Color relies on it and the - // global may not exist with AMD and a custom build (#10199) - jQuery = $; - -$.effects = { - effect: {} -}; /*! - * jQuery Color Animations v2.1.2 + * jQuery Color Animations v2.2.0 * https://github.com/jquery/jquery-color * - * Copyright 2014 jQuery Foundation and other contributors + * Copyright OpenJS Foundation and other contributors * Released under the MIT license. * http://jquery.org/license * - * Date: Wed Jan 16 08:47:09 2013 -0600 + * Date: Sun May 10 09:02:36 2020 +0200 */ -( function( jQuery, undefined ) { + + var stepHooks = "backgroundColor borderBottomColor borderLeftColor borderRightColor " + "borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor", - // Plusequals test for += 100 -= 100 + class2type = {}, + toString = class2type.toString, + + // plusequals test for += 100 -= 100 rplusequals = /^([\-+])=\s*(\d+\.?\d*)/, - // A set of RE's that can match strings and generate color tuples. + // a set of RE's that can match strings and generate color tuples. stringParsers = [ { re: /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/, parse: function( execResult ) { @@ -1384,24 +1395,31 @@ $.effects = { } }, { - // This regex ignores A-F because it's compared against an already lowercased string - re: /#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/, + // this regex ignores A-F because it's compared against an already lowercased string + re: /#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})?/, parse: function( execResult ) { return [ parseInt( execResult[ 1 ], 16 ), parseInt( execResult[ 2 ], 16 ), - parseInt( execResult[ 3 ], 16 ) + parseInt( execResult[ 3 ], 16 ), + execResult[ 4 ] ? + ( parseInt( execResult[ 4 ], 16 ) / 255 ).toFixed( 2 ) : + 1 ]; } }, { - // This regex ignores A-F because it's compared against an already lowercased string - re: /#([a-f0-9])([a-f0-9])([a-f0-9])/, + // this regex ignores A-F because it's compared against an already lowercased string + re: /#([a-f0-9])([a-f0-9])([a-f0-9])([a-f0-9])?/, parse: function( execResult ) { return [ parseInt( execResult[ 1 ] + execResult[ 1 ], 16 ), parseInt( execResult[ 2 ] + execResult[ 2 ], 16 ), - parseInt( execResult[ 3 ] + execResult[ 3 ], 16 ) + parseInt( execResult[ 3 ] + execResult[ 3 ], 16 ), + execResult[ 4 ] ? + ( parseInt( execResult[ 4 ] + execResult[ 4 ], 16 ) / 255 ) + .toFixed( 2 ) : + 1 ]; } }, { @@ -1417,7 +1435,7 @@ $.effects = { } } ], - // JQuery.Color( ) + // jQuery.Color( ) color = jQuery.Color = function( color, green, blue, alpha ) { return new jQuery.Color.fn.parse( color, green, blue, alpha ); }, @@ -1471,20 +1489,20 @@ $.effects = { }, support = color.support = {}, - // Element for support tests + // element for support tests supportElem = jQuery( "

" )[ 0 ], - // Colors = jQuery.Color.names + // colors = jQuery.Color.names colors, - // Local aliases of functions called often + // local aliases of functions called often each = jQuery.each; -// Determine rgba support immediately +// determine rgba support immediately supportElem.style.cssText = "background-color:rgba(1,1,1,.5)"; support.rgba = supportElem.style.backgroundColor.indexOf( "rgba" ) > -1; -// Define cache name and alpha properties +// define cache name and alpha properties // for rgba and hsla spaces each( spaces, function( spaceName, space ) { space.cache = "_" + spaceName; @@ -1495,6 +1513,22 @@ each( spaces, function( spaceName, space ) { }; } ); +// Populate the class2type map +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), + function( _i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); + } ); + +function getType( obj ) { + if ( obj == null ) { + return obj + ""; + } + + return typeof obj === "object" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; +} + function clamp( value, prop, allowEmpty ) { var type = propTypes[ prop.type ] || {}; @@ -1513,13 +1547,13 @@ function clamp( value, prop, allowEmpty ) { if ( type.mod ) { - // We add mod before modding to make sure that negatives values + // we add mod before modding to make sure that negatives values // get converted properly: -10 -> 350 return ( value + type.mod ) % type.mod; } - // For now all property types without mod have min and max - return 0 > value ? 0 : type.max < value ? type.max : value; + // for now all property types without mod have min and max + return Math.min( type.max, Math.max( 0, value ) ); } function stringParse( string ) { @@ -1528,7 +1562,7 @@ function stringParse( string ) { string = string.toLowerCase(); - each( stringParsers, function( i, parser ) { + each( stringParsers, function( _i, parser ) { var parsed, match = parser.re.exec( string ), values = match && parser.parse( match ), @@ -1537,12 +1571,12 @@ function stringParse( string ) { if ( values ) { parsed = inst[ spaceName ]( values ); - // If this was an rgba parse the assignment might happen twice + // if this was an rgba parse the assignment might happen twice // oh well.... inst[ spaces[ spaceName ].cache ] = parsed[ spaces[ spaceName ].cache ]; rgba = inst._rgba = parsed._rgba; - // Exit each( stringParsers ) here because we matched + // exit each( stringParsers ) here because we matched return false; } } ); @@ -1550,7 +1584,7 @@ function stringParse( string ) { // Found a stringParser that handled it if ( rgba.length ) { - // If this came from a parsed string, force "transparent" when alpha is 0 + // if this came from a parsed string, force "transparent" when alpha is 0 // chrome, (and maybe others) return "transparent" as rgba(0,0,0,0) if ( rgba.join() === "0,0,0,0" ) { jQuery.extend( rgba, colors.transparent ); @@ -1558,7 +1592,7 @@ function stringParse( string ) { return inst; } - // Named colors + // named colors return colors[ string ]; } @@ -1574,10 +1608,10 @@ color.fn = jQuery.extend( color.prototype, { } var inst = this, - type = jQuery.type( red ), + type = getType( red ), rgba = this._rgba = []; - // More than 1 argument specified - assume ( red, green, blue, alpha ) + // more than 1 argument specified - assume ( red, green, blue, alpha ) if ( green !== undefined ) { red = [ red, green, blue, alpha ]; type = "array"; @@ -1588,7 +1622,7 @@ color.fn = jQuery.extend( color.prototype, { } if ( type === "array" ) { - each( spaces.rgba.props, function( key, prop ) { + each( spaces.rgba.props, function( _key, prop ) { rgba[ prop.idx ] = clamp( red[ prop.idx ], prop ); } ); return this; @@ -1596,20 +1630,20 @@ color.fn = jQuery.extend( color.prototype, { if ( type === "object" ) { if ( red instanceof color ) { - each( spaces, function( spaceName, space ) { + each( spaces, function( _spaceName, space ) { if ( red[ space.cache ] ) { inst[ space.cache ] = red[ space.cache ].slice(); } } ); } else { - each( spaces, function( spaceName, space ) { + each( spaces, function( _spaceName, space ) { var cache = space.cache; each( space.props, function( key, prop ) { - // If the cache doesn't exist, and we know how to convert + // if the cache doesn't exist, and we know how to convert if ( !inst[ cache ] && space.to ) { - // If the value was null, we don't need to copy it + // if the value was null, we don't need to copy it // if the key was alpha, we don't need to copy it either if ( key === "alpha" || red[ key ] == null ) { return; @@ -1617,17 +1651,19 @@ color.fn = jQuery.extend( color.prototype, { inst[ cache ] = space.to( inst._rgba ); } - // This is the only case where we allow nulls for ALL properties. + // this is the only case where we allow nulls for ALL properties. // call clamp with alwaysAllowEmpty inst[ cache ][ prop.idx ] = clamp( red[ key ], prop, true ); } ); - // Everything defined but alpha? - if ( inst[ cache ] && - jQuery.inArray( null, inst[ cache ].slice( 0, 3 ) ) < 0 ) { + // everything defined but alpha? + if ( inst[ cache ] && jQuery.inArray( null, inst[ cache ].slice( 0, 3 ) ) < 0 ) { + + // use the default of 1 + if ( inst[ cache ][ 3 ] == null ) { + inst[ cache ][ 3 ] = 1; + } - // Use the default of 1 - inst[ cache ][ 3 ] = 1; if ( space.from ) { inst._rgba = space.from( inst[ cache ] ); } @@ -1677,18 +1713,18 @@ color.fn = jQuery.extend( color.prototype, { result = start.slice(); end = end[ space.cache ]; - each( space.props, function( key, prop ) { + each( space.props, function( _key, prop ) { var index = prop.idx, startValue = start[ index ], endValue = end[ index ], type = propTypes[ prop.type ] || {}; - // If null, don't override start value + // if null, don't override start value if ( endValue === null ) { return; } - // If null - use end + // if null - use end if ( startValue === null ) { result[ index ] = endValue; } else { @@ -1706,7 +1742,7 @@ color.fn = jQuery.extend( color.prototype, { }, blend: function( opaque ) { - // If we are already opaque - return ourself + // if we are already opaque - return ourself if ( this._rgba[ 3 ] === 1 ) { return this; } @@ -1722,7 +1758,10 @@ color.fn = jQuery.extend( color.prototype, { toRgbaString: function() { var prefix = "rgba(", rgba = jQuery.map( this._rgba, function( v, i ) { - return v == null ? ( i > 2 ? 1 : 0 ) : v; + if ( v != null ) { + return v; + } + return i > 2 ? 1 : 0; } ); if ( rgba[ 3 ] === 1 ) { @@ -1739,7 +1778,7 @@ color.fn = jQuery.extend( color.prototype, { v = i > 2 ? 1 : 0; } - // Catch 1 and 2 + // catch 1 and 2 if ( i && i < 3 ) { v = Math.round( v * 100 ) + "%"; } @@ -1762,7 +1801,7 @@ color.fn = jQuery.extend( color.prototype, { return "#" + jQuery.map( rgba, function( v ) { - // Default to 0 when nulls exist + // default to 0 when nulls exist v = ( v || 0 ).toString( 16 ); return v.length === 1 ? "0" + v : v; } ).join( "" ); @@ -1773,7 +1812,7 @@ color.fn = jQuery.extend( color.prototype, { } ); color.fn.parse.prototype = color.fn; -// Hsla conversions adapted from: +// hsla conversions adapted from: // https://code.google.com/p/maashaack/source/browse/packages/graphics/trunk/src/graphics/colors/HUE2RGB.as?r=5021 function hue2rgb( p, q, h ) { @@ -1815,7 +1854,7 @@ spaces.hsla.to = function( rgba ) { h = ( 60 * ( r - g ) / diff ) + 240; } - // Chroma (diff) == 0 means greyscale which, by definition, saturation = 0% + // chroma (diff) == 0 means greyscale which, by definition, saturation = 0% // otherwise, saturation is based on the ratio of chroma (diff) to lightness (add) if ( diff === 0 ) { s = 0; @@ -1846,16 +1885,17 @@ spaces.hsla.from = function( hsla ) { ]; }; + each( spaces, function( spaceName, space ) { var props = space.props, cache = space.cache, to = space.to, from = space.from; - // Makes rgba() and hsla() + // makes rgba() and hsla() color.fn[ spaceName ] = function( value ) { - // Generate a cache for this space if it doesn't exist + // generate a cache for this space if it doesn't exist if ( to && !this[ cache ] ) { this[ cache ] = to( this._rgba ); } @@ -1864,7 +1904,7 @@ each( spaces, function( spaceName, space ) { } var ret, - type = jQuery.type( value ), + type = getType( value ), arr = ( type === "array" || type === "object" ) ? value : arguments, local = this[ cache ].slice(); @@ -1885,19 +1925,24 @@ each( spaces, function( spaceName, space ) { } }; - // Makes red() green() blue() alpha() hue() saturation() lightness() + // makes red() green() blue() alpha() hue() saturation() lightness() each( props, function( key, prop ) { - // Alpha is included in more than one space + // alpha is included in more than one space if ( color.fn[ key ] ) { return; } color.fn[ key ] = function( value ) { - var vtype = jQuery.type( value ), - fn = ( key === "alpha" ? ( this._hsla ? "hsla" : "rgba" ) : spaceName ), - local = this[ fn ](), - cur = local[ prop.idx ], - match; + var local, cur, match, fn, + vtype = getType( value ); + + if ( key === "alpha" ) { + fn = this._hsla ? "hsla" : "rgba"; + } else { + fn = spaceName; + } + local = this[ fn ](); + cur = local[ prop.idx ]; if ( vtype === "undefined" ) { return cur; @@ -1905,7 +1950,7 @@ each( spaces, function( spaceName, space ) { if ( vtype === "function" ) { value = value.call( this, cur ); - vtype = jQuery.type( value ); + vtype = getType( value ); } if ( value == null && prop.empty ) { return this; @@ -1922,18 +1967,17 @@ each( spaces, function( spaceName, space ) { } ); } ); -// Add cssHook and .fx.step function for each named hook. +// add cssHook and .fx.step function for each named hook. // accept a space separated string of properties color.hook = function( hook ) { var hooks = hook.split( " " ); - each( hooks, function( i, hook ) { + each( hooks, function( _i, hook ) { jQuery.cssHooks[ hook ] = { set: function( elem, value ) { var parsed, curElem, backgroundColor = ""; - if ( value !== "transparent" && ( jQuery.type( value ) !== "string" || - ( parsed = stringParse( value ) ) ) ) { + if ( value !== "transparent" && ( getType( value ) !== "string" || ( parsed = stringParse( value ) ) ) ) { value = color( parsed || value ); if ( !support.rgba && value._rgba[ 3 ] !== 1 ) { curElem = hook === "backgroundColor" ? elem.parentNode : elem; @@ -1959,8 +2003,7 @@ color.hook = function( hook ) { elem.style[ hook ] = value; } catch ( e ) { - // Wrapped to prevent IE from throwing errors on "invalid" values like - // 'auto' or 'inherit' + // wrapped to prevent IE from throwing errors on "invalid" values like 'auto' or 'inherit' } } }; @@ -1982,7 +2025,7 @@ jQuery.cssHooks.borderColor = { expand: function( value ) { var expanded = {}; - each( [ "Top", "Right", "Bottom", "Left" ], function( i, part ) { + each( [ "Top", "Right", "Bottom", "Left" ], function( _i, part ) { expanded[ "border" + part + "Color" ] = value; } ); return expanded; @@ -2018,7 +2061,32 @@ colors = jQuery.Color.names = { _default: "#ffffff" }; -} )( jQuery ); + +/*! + * jQuery UI Effects 1.13.2 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Effects Core +//>>group: Effects +/* eslint-disable max-len */ +//>>description: Extends the internal jQuery effects. Includes morphing and easing. Required by all other effects. +/* eslint-enable max-len */ +//>>docs: http://api.jqueryui.com/category/effects-core/ +//>>demos: http://jqueryui.com/effect/ + + +var dataSpace = "ui-effects-", + dataSpaceStyle = "ui-effects-style", + dataSpaceAnimated = "ui-effects-animated"; + +$.effects = { + effect: {} +}; /******************************************************************************/ /****************************** CLASS ANIMATIONS ******************************/ @@ -2050,6 +2118,12 @@ $.each( } ); +function camelCase( string ) { + return string.replace( /-([\da-z])/gi, function( all, letter ) { + return letter.toUpperCase(); + } ); +} + function getElementStyles( elem ) { var key, len, style = elem.ownerDocument.defaultView ? @@ -2062,7 +2136,7 @@ function getElementStyles( elem ) { while ( len-- ) { key = style[ len ]; if ( typeof style[ key ] === "string" ) { - styles[ $.camelCase( key ) ] = style[ key ]; + styles[ camelCase( key ) ] = style[ key ]; } } @@ -2236,12 +2310,12 @@ $.fn.extend( { ( function() { -if ( $.expr && $.expr.filters && $.expr.filters.animated ) { - $.expr.filters.animated = ( function( orig ) { +if ( $.expr && $.expr.pseudos && $.expr.pseudos.animated ) { + $.expr.pseudos.animated = ( function( orig ) { return function( elem ) { return !!$( elem ).data( dataSpaceAnimated ) || orig( elem ); }; - } )( $.expr.filters.animated ); + } )( $.expr.pseudos.animated ); } if ( $.uiBackCompat !== false ) { @@ -2310,6 +2384,7 @@ if ( $.uiBackCompat !== false ) { // Firefox incorrectly exposes anonymous content // https://bugzilla.mozilla.org/show_bug.cgi?id=561664 try { + // eslint-disable-next-line no-unused-expressions active.id; } catch ( e ) { active = document.body; @@ -2372,7 +2447,7 @@ if ( $.uiBackCompat !== false ) { } $.extend( $.effects, { - version: "1.12.1", + version: "1.13.2", define: function( name, mode, effect ) { if ( !effect ) { @@ -2588,7 +2663,7 @@ function _normalizeArguments( effect, options, speed, callback ) { } // Catch (effect, callback) - if ( $.isFunction( options ) ) { + if ( typeof options === "function" ) { callback = options; speed = null; options = {}; @@ -2602,7 +2677,7 @@ function _normalizeArguments( effect, options, speed, callback ) { } // Catch (effect, options, callback) - if ( $.isFunction( speed ) ) { + if ( typeof speed === "function" ) { callback = speed; speed = null; } @@ -2636,7 +2711,7 @@ function standardAnimationOption( option ) { } // Complete callback - if ( $.isFunction( option ) ) { + if ( typeof option === "function" ) { return true; } @@ -2663,7 +2738,7 @@ $.fn.extend( { var el = $( this ), normalizedMode = $.effects.mode( el, mode ) || defaultMode; - // Sentinel for duck-punching the :animated psuedo-selector + // Sentinel for duck-punching the :animated pseudo-selector el.data( dataSpaceAnimated, true ); // Save effect mode for later use, @@ -2671,7 +2746,7 @@ $.fn.extend( { // as the .show() below destroys the initial state modes.push( normalizedMode ); - // See $.uiBackCompat inside of run() for removal of defaultMode in 1.13 + // See $.uiBackCompat inside of run() for removal of defaultMode in 1.14 if ( defaultMode && ( normalizedMode === "show" || ( normalizedMode === defaultMode && normalizedMode === "hide" ) ) ) { el.show(); @@ -2681,7 +2756,7 @@ $.fn.extend( { $.effects.saveStyle( el ); } - if ( $.isFunction( next ) ) { + if ( typeof next === "function" ) { next(); } }; @@ -2716,11 +2791,11 @@ $.fn.extend( { } function done() { - if ( $.isFunction( complete ) ) { + if ( typeof complete === "function" ) { complete.call( elem[ 0 ] ); } - if ( $.isFunction( next ) ) { + if ( typeof next === "function" ) { next(); } } @@ -2829,22 +2904,24 @@ $.fn.extend( { width: target.innerWidth() }, startPosition = element.offset(), - transfer = $( "

" ) - .appendTo( "body" ) - .addClass( options.className ) - .css( { - top: startPosition.top - fixTop, - left: startPosition.left - fixLeft, - height: element.innerHeight(), - width: element.innerWidth(), - position: targetFixed ? "fixed" : "absolute" - } ) - .animate( animation, options.duration, options.easing, function() { - transfer.remove(); - if ( $.isFunction( done ) ) { - done(); - } - } ); + transfer = $( "
" ); + + transfer + .appendTo( "body" ) + .addClass( options.className ) + .css( { + top: startPosition.top - fixTop, + left: startPosition.left - fixLeft, + height: element.innerHeight(), + width: element.innerWidth(), + position: targetFixed ? "fixed" : "absolute" + } ) + .animate( animation, options.duration, options.easing, function() { + transfer.remove(); + if ( typeof done === "function" ) { + done(); + } + } ); } } ); @@ -2938,7 +3015,7 @@ var effect = $.effects; /*! - * jQuery UI Effects Blind 1.12.1 + * jQuery UI Effects Blind 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -2953,7 +3030,6 @@ var effect = $.effects; //>>demos: http://jqueryui.com/effect/ - var effectsEffectBlind = $.effects.define( "blind", "hide", function( options, done ) { var map = { up: [ "bottom", "top" ], @@ -2994,7 +3070,7 @@ var effectsEffectBlind = $.effects.define( "blind", "hide", function( options, d /*! - * jQuery UI Effects Bounce 1.12.1 + * jQuery UI Effects Bounce 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -3009,7 +3085,6 @@ var effectsEffectBlind = $.effects.define( "blind", "hide", function( options, d //>>demos: http://jqueryui.com/effect/ - var effectsEffectBounce = $.effects.define( "bounce", function( options, done ) { var upAnim, downAnim, refValue, element = $( this ), @@ -3090,7 +3165,7 @@ var effectsEffectBounce = $.effects.define( "bounce", function( options, done ) /*! - * jQuery UI Effects Clip 1.12.1 + * jQuery UI Effects Clip 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -3105,7 +3180,6 @@ var effectsEffectBounce = $.effects.define( "bounce", function( options, done ) //>>demos: http://jqueryui.com/effect/ - var effectsEffectClip = $.effects.define( "clip", "hide", function( options, done ) { var start, animate = {}, @@ -3141,7 +3215,7 @@ var effectsEffectClip = $.effects.define( "clip", "hide", function( options, don /*! - * jQuery UI Effects Drop 1.12.1 + * jQuery UI Effects Drop 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -3156,7 +3230,6 @@ var effectsEffectClip = $.effects.define( "clip", "hide", function( options, don //>>demos: http://jqueryui.com/effect/ - var effectsEffectDrop = $.effects.define( "drop", "hide", function( options, done ) { var distance, @@ -3196,7 +3269,7 @@ var effectsEffectDrop = $.effects.define( "drop", "hide", function( options, don /*! - * jQuery UI Effects Explode 1.12.1 + * jQuery UI Effects Explode 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -3206,14 +3279,13 @@ var effectsEffectDrop = $.effects.define( "drop", "hide", function( options, don //>>label: Explode Effect //>>group: Effects -// jscs:disable maximumLineLength +/* eslint-disable max-len */ //>>description: Explodes an element in all directions into n pieces. Implodes an element to its original wholeness. -// jscs:enable maximumLineLength +/* eslint-enable max-len */ //>>docs: http://api.jqueryui.com/explode-effect/ //>>demos: http://jqueryui.com/effect/ - var effectsEffectExplode = $.effects.define( "explode", "hide", function( options, done ) { var i, j, left, top, mx, my, @@ -3293,7 +3365,7 @@ var effectsEffectExplode = $.effects.define( "explode", "hide", function( option /*! - * jQuery UI Effects Fade 1.12.1 + * jQuery UI Effects Fade 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -3308,7 +3380,6 @@ var effectsEffectExplode = $.effects.define( "explode", "hide", function( option //>>demos: http://jqueryui.com/effect/ - var effectsEffectFade = $.effects.define( "fade", "toggle", function( options, done ) { var show = options.mode === "show"; @@ -3326,7 +3397,7 @@ var effectsEffectFade = $.effects.define( "fade", "toggle", function( options, d /*! - * jQuery UI Effects Fold 1.12.1 + * jQuery UI Effects Fold 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -3341,7 +3412,6 @@ var effectsEffectFade = $.effects.define( "fade", "toggle", function( options, d //>>demos: http://jqueryui.com/effect/ - var effectsEffectFold = $.effects.define( "fold", "hide", function( options, done ) { // Create element @@ -3401,7 +3471,7 @@ var effectsEffectFold = $.effects.define( "fold", "hide", function( options, don /*! - * jQuery UI Effects Highlight 1.12.1 + * jQuery UI Effects Highlight 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -3416,7 +3486,6 @@ var effectsEffectFold = $.effects.define( "fold", "hide", function( options, don //>>demos: http://jqueryui.com/effect/ - var effectsEffectHighlight = $.effects.define( "highlight", "show", function( options, done ) { var element = $( this ), animation = { @@ -3444,7 +3513,7 @@ var effectsEffectHighlight = $.effects.define( "highlight", "show", function( op /*! - * jQuery UI Effects Size 1.12.1 + * jQuery UI Effects Size 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -3459,7 +3528,6 @@ var effectsEffectHighlight = $.effects.define( "highlight", "show", function( op //>>demos: http://jqueryui.com/effect/ - var effectsEffectSize = $.effects.define( "size", function( options, done ) { // Create element @@ -3536,6 +3604,8 @@ var effectsEffectSize = $.effects.define( "size", function( options, done ) { to.top = ( original.outerHeight - to.outerHeight ) * baseline.y + pos.top; to.left = ( original.outerWidth - to.outerWidth ) * baseline.x + pos.left; } + delete from.outerHeight; + delete from.outerWidth; element.css( from ); // Animate the children if desired @@ -3621,7 +3691,7 @@ var effectsEffectSize = $.effects.define( "size", function( options, done ) { /*! - * jQuery UI Effects Scale 1.12.1 + * jQuery UI Effects Scale 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -3636,7 +3706,6 @@ var effectsEffectSize = $.effects.define( "size", function( options, done ) { //>>demos: http://jqueryui.com/effect/ - var effectsEffectScale = $.effects.define( "scale", function( options, done ) { // Create element @@ -3662,7 +3731,7 @@ var effectsEffectScale = $.effects.define( "scale", function( options, done ) { /*! - * jQuery UI Effects Puff 1.12.1 + * jQuery UI Effects Puff 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -3677,7 +3746,6 @@ var effectsEffectScale = $.effects.define( "scale", function( options, done ) { //>>demos: http://jqueryui.com/effect/ - var effectsEffectPuff = $.effects.define( "puff", "hide", function( options, done ) { var newOptions = $.extend( true, {}, options, { fade: true, @@ -3689,7 +3757,7 @@ var effectsEffectPuff = $.effects.define( "puff", "hide", function( options, don /*! - * jQuery UI Effects Pulsate 1.12.1 + * jQuery UI Effects Pulsate 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -3704,7 +3772,6 @@ var effectsEffectPuff = $.effects.define( "puff", "hide", function( options, don //>>demos: http://jqueryui.com/effect/ - var effectsEffectPulsate = $.effects.define( "pulsate", "show", function( options, done ) { var element = $( this ), mode = options.mode, @@ -3739,7 +3806,7 @@ var effectsEffectPulsate = $.effects.define( "pulsate", "show", function( option /*! - * jQuery UI Effects Shake 1.12.1 + * jQuery UI Effects Shake 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -3754,7 +3821,6 @@ var effectsEffectPulsate = $.effects.define( "pulsate", "show", function( option //>>demos: http://jqueryui.com/effect/ - var effectsEffectShake = $.effects.define( "shake", function( options, done ) { var i = 1, @@ -3799,7 +3865,7 @@ var effectsEffectShake = $.effects.define( "shake", function( options, done ) { /*! - * jQuery UI Effects Slide 1.12.1 + * jQuery UI Effects Slide 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -3814,7 +3880,6 @@ var effectsEffectShake = $.effects.define( "shake", function( options, done ) { //>>demos: http://jqueryui.com/effect/ - var effectsEffectSlide = $.effects.define( "slide", "show", function( options, done ) { var startClip, startRef, element = $( this ), @@ -3861,7 +3926,7 @@ var effectsEffectSlide = $.effects.define( "slide", "show", function( options, d /*! - * jQuery UI Effects Transfer 1.12.1 + * jQuery UI Effects Transfer 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -3876,7 +3941,6 @@ var effectsEffectSlide = $.effects.define( "slide", "show", function( options, d //>>demos: http://jqueryui.com/effect/ - var effect; if ( $.uiBackCompat !== false ) { effect = $.effects.define( "transfer", function( options, done ) { @@ -3887,7 +3951,7 @@ var effectsEffectTransfer = effect; /*! - * jQuery UI Focusable 1.12.1 + * jQuery UI Focusable 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -3901,7 +3965,6 @@ var effectsEffectTransfer = effect; //>>docs: http://api.jqueryui.com/focusable-selector/ - // Selectors $.ui.focusable = function( element, hasTabindex ) { var map, mapName, img, focusableIfVisible, fieldset, @@ -3948,10 +4011,10 @@ function visible( element ) { element = element.parent(); visibility = element.css( "visibility" ); } - return visibility !== "hidden"; + return visibility === "visible"; } -$.extend( $.expr[ ":" ], { +$.extend( $.expr.pseudos, { focusable: function( element ) { return $.ui.focusable( element, $.attr( element, "tabindex" ) != null ); } @@ -3961,17 +4024,16 @@ var focusable = $.ui.focusable; - // Support: IE8 Only // IE8 does not support the form attribute and when it is supplied. It overwrites the form prop // with a string, so we need to find the proper form. -var form = $.fn.form = function() { +var form = $.fn._form = function() { return typeof this[ 0 ].form === "string" ? this.closest( "form" ) : $( this[ 0 ].form ); }; /*! - * jQuery UI Form Reset Mixin 1.12.1 + * jQuery UI Form Reset Mixin 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -3985,7 +4047,6 @@ var form = $.fn.form = function() { //>>docs: http://api.jqueryui.com/form-reset-mixin/ - var formResetMixin = $.ui.formResetMixin = { _formResetHandler: function() { var form = $( this ); @@ -4000,7 +4061,7 @@ var formResetMixin = $.ui.formResetMixin = { }, _bindFormResetHandler: function() { - this.form = this.element.form(); + this.form = this.element._form(); if ( !this.form.length ) { return; } @@ -4034,7 +4095,7 @@ var formResetMixin = $.ui.formResetMixin = { /*! - * jQuery UI Support for jQuery core 1.7.x 1.12.1 + * jQuery UI Support for jQuery core 1.8.x and newer 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -4043,77 +4104,73 @@ var formResetMixin = $.ui.formResetMixin = { * */ -//>>label: jQuery 1.7 Support +//>>label: jQuery 1.8+ Support //>>group: Core -//>>description: Support version 1.7.x of jQuery core - - - -// Support: jQuery 1.7 only -// Not a great way to check versions, but since we only support 1.7+ and only -// need to detect <1.8, this is a simple check that should suffice. Checking -// for "1.7." would be a bit safer, but the version string is 1.7, not 1.7.0 -// and we'll never reach 1.70.0 (if we do, we certainly won't be supporting -// 1.7 anymore). See #11197 for why we're not using feature detection. -if ( $.fn.jquery.substring( 0, 3 ) === "1.7" ) { - - // Setters for .innerWidth(), .innerHeight(), .outerWidth(), .outerHeight() - // Unlike jQuery Core 1.8+, these only support numeric values to set the - // dimensions in pixels - $.each( [ "Width", "Height" ], function( i, name ) { - var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ], - type = name.toLowerCase(), - orig = { - innerWidth: $.fn.innerWidth, - innerHeight: $.fn.innerHeight, - outerWidth: $.fn.outerWidth, - outerHeight: $.fn.outerHeight - }; +//>>description: Support version 1.8.x and newer of jQuery core - function reduce( elem, size, border, margin ) { - $.each( side, function() { - size -= parseFloat( $.css( elem, "padding" + this ) ) || 0; - if ( border ) { - size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0; - } - if ( margin ) { - size -= parseFloat( $.css( elem, "margin" + this ) ) || 0; - } - } ); - return size; - } - $.fn[ "inner" + name ] = function( size ) { - if ( size === undefined ) { - return orig[ "inner" + name ].call( this ); - } +// Support: jQuery 1.9.x or older +// $.expr[ ":" ] is deprecated. +if ( !$.expr.pseudos ) { + $.expr.pseudos = $.expr[ ":" ]; +} - return this.each( function() { - $( this ).css( type, reduce( this, size ) + "px" ); - } ); - }; +// Support: jQuery 1.11.x or older +// $.unique has been renamed to $.uniqueSort +if ( !$.uniqueSort ) { + $.uniqueSort = $.unique; +} + +// Support: jQuery 2.2.x or older. +// This method has been defined in jQuery 3.0.0. +// Code from https://github.com/jquery/jquery/blob/e539bac79e666bba95bba86d690b4e609dca2286/src/selector/escapeSelector.js +if ( !$.escapeSelector ) { + + // CSS string/identifier serialization + // https://drafts.csswg.org/cssom/#common-serializing-idioms + var rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g; + + var fcssescape = function( ch, asCodePoint ) { + if ( asCodePoint ) { - $.fn[ "outer" + name ] = function( size, margin ) { - if ( typeof size !== "number" ) { - return orig[ "outer" + name ].call( this, size ); + // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER + if ( ch === "\0" ) { + return "\uFFFD"; } - return this.each( function() { - $( this ).css( type, reduce( this, size, true, margin ) + "px" ); - } ); - }; - } ); + // Control characters and (dependent upon position) numbers get escaped as code points + return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + } - $.fn.addBack = function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter( selector ) - ); + // Other potentially-special ASCII characters get backslash-escaped + return "\\" + ch; + }; + + $.escapeSelector = function( sel ) { + return ( sel + "" ).replace( rcssescape, fcssescape ); }; } +// Support: jQuery 3.4.x or older +// These methods have been defined in jQuery 3.5.0. +if ( !$.fn.even || !$.fn.odd ) { + $.fn.extend( { + even: function() { + return this.filter( function( i ) { + return i % 2 === 0; + } ); + }, + odd: function() { + return this.filter( function( i ) { + return i % 2 === 1; + } ); + } + } ); +} + ; /*! - * jQuery UI Keycode 1.12.1 + * jQuery UI Keycode 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -4147,19 +4204,8 @@ var keycode = $.ui.keyCode = { }; - - -// Internal use only -var escapeSelector = $.ui.escapeSelector = ( function() { - var selectorEscape = /([!"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g; - return function( selector ) { - return selector.replace( selectorEscape, "\\$1" ); - }; -} )(); - - /*! - * jQuery UI Labels 1.12.1 + * jQuery UI Labels 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -4173,10 +4219,13 @@ var escapeSelector = $.ui.escapeSelector = ( function() { //>>docs: http://api.jqueryui.com/labels/ - var labels = $.fn.labels = function() { var ancestor, selector, id, labels, ancestors; + if ( !this.length ) { + return this.pushStack( [] ); + } + // Check control.labels first if ( this[ 0 ].labels && this[ 0 ].labels.length ) { return this.pushStack( this[ 0 ].labels ); @@ -4199,7 +4248,7 @@ var labels = $.fn.labels = function() { ancestors = ancestor.add( ancestor.length ? ancestor.siblings() : this.siblings() ); // Create a selector for the label based on the id - selector = "label[for='" + $.ui.escapeSelector( id ) + "']"; + selector = "label[for='" + $.escapeSelector( id ) + "']"; labels = labels.add( ancestors.find( selector ).addBack( selector ) ); @@ -4211,7 +4260,7 @@ var labels = $.fn.labels = function() { /*! - * jQuery UI Scroll Parent 1.12.1 + * jQuery UI Scroll Parent 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -4225,7 +4274,6 @@ var labels = $.fn.labels = function() { //>>docs: http://api.jqueryui.com/scrollParent/ - var scrollParent = $.fn.scrollParent = function( includeHidden ) { var position = this.css( "position" ), excludeStaticParent = position === "absolute", @@ -4246,7 +4294,7 @@ var scrollParent = $.fn.scrollParent = function( includeHidden ) { /*! - * jQuery UI Tabbable 1.12.1 + * jQuery UI Tabbable 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -4260,8 +4308,7 @@ var scrollParent = $.fn.scrollParent = function( includeHidden ) { //>>docs: http://api.jqueryui.com/tabbable-selector/ - -var tabbable = $.extend( $.expr[ ":" ], { +var tabbable = $.extend( $.expr.pseudos, { tabbable: function( element ) { var tabIndex = $.attr( element, "tabindex" ), hasTabindex = tabIndex != null; @@ -4271,7 +4318,7 @@ var tabbable = $.extend( $.expr[ ":" ], { /*! - * jQuery UI Unique ID 1.12.1 + * jQuery UI Unique ID 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -4285,7 +4332,6 @@ var tabbable = $.extend( $.expr[ ":" ], { //>>docs: http://api.jqueryui.com/uniqueId/ - var uniqueId = $.fn.extend( { uniqueId: ( function() { var uuid = 0; @@ -4310,7 +4356,7 @@ var uniqueId = $.fn.extend( { /*! - * jQuery UI Accordion 1.12.1 + * jQuery UI Accordion 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -4320,9 +4366,9 @@ var uniqueId = $.fn.extend( { //>>label: Accordion //>>group: Widgets -// jscs:disable maximumLineLength +/* eslint-disable max-len */ //>>description: Displays collapsible content panels for presenting information in a limited amount of space. -// jscs:enable maximumLineLength +/* eslint-enable max-len */ //>>docs: http://api.jqueryui.com/accordion/ //>>demos: http://jqueryui.com/accordion/ //>>css.structure: ../../themes/base/core.css @@ -4330,9 +4376,8 @@ var uniqueId = $.fn.extend( { //>>css.theme: ../../themes/base/theme.css - var widgetsAccordion = $.widget( "ui.accordion", { - version: "1.12.1", + version: "1.13.2", options: { active: 0, animate: {}, @@ -4343,7 +4388,9 @@ var widgetsAccordion = $.widget( "ui.accordion", { }, collapsible: false, event: "click", - header: "> li > :first-child, > :not(li):even", + header: function( elem ) { + return elem.find( "> li > :first-child" ).add( elem.find( "> :not(li)" ).even() ); + }, heightStyle: "auto", icons: { activeHeader: "ui-icon-triangle-1-s", @@ -4574,7 +4621,11 @@ var widgetsAccordion = $.widget( "ui.accordion", { var prevHeaders = this.headers, prevPanels = this.panels; - this.headers = this.element.find( this.options.header ); + if ( typeof this.options.header === "function" ) { + this.headers = this.options.header( this.element ); + } else { + this.headers = this.element.find( this.options.header ); + } this._addClass( this.headers, "ui-accordion-header ui-accordion-header-collapsed", "ui-state-default" ); @@ -4937,7 +4988,7 @@ var safeActiveElement = $.ui.safeActiveElement = function( document ) { /*! - * jQuery UI Menu 1.12.1 + * jQuery UI Menu 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -4955,9 +5006,8 @@ var safeActiveElement = $.ui.safeActiveElement = function( document ) { //>>css.theme: ../../themes/base/theme.css - var widgetsMenu = $.widget( "ui.menu", { - version: "1.12.1", + version: "1.13.2", defaultElement: "
    ", delay: 300, options: { @@ -4984,6 +5034,7 @@ var widgetsMenu = $.widget( "ui.menu", { // Flag used to prevent firing of the click handler // as the event bubbles up through nested menus this.mouseHandled = false; + this.lastMousePosition = { x: null, y: null }; this.element .uniqueId() .attr( { @@ -4998,6 +5049,8 @@ var widgetsMenu = $.widget( "ui.menu", { // them (focus should always stay on UL during navigation). "mousedown .ui-menu-item": function( event ) { event.preventDefault(); + + this._activateItem( event ); }, "click .ui-menu-item": function( event ) { var target = $( event.target ); @@ -5027,36 +5080,15 @@ var widgetsMenu = $.widget( "ui.menu", { } } }, - "mouseenter .ui-menu-item": function( event ) { - - // Ignore mouse events while typeahead is active, see #10458. - // Prevents focusing the wrong item when typeahead causes a scroll while the mouse - // is over an item in the menu - if ( this.previousFilter ) { - return; - } - - var actualTarget = $( event.target ).closest( ".ui-menu-item" ), - target = $( event.currentTarget ); - - // Ignore bubbled events on parent items, see #11641 - if ( actualTarget[ 0 ] !== target[ 0 ] ) { - return; - } - - // Remove ui-state-active class from siblings of the newly focused menu item - // to avoid a jump caused by adjacent elements both having a class with a border - this._removeClass( target.siblings().children( ".ui-state-active" ), - null, "ui-state-active" ); - this.focus( event, target ); - }, + "mouseenter .ui-menu-item": "_activateItem", + "mousemove .ui-menu-item": "_activateItem", mouseleave: "collapseAll", "mouseleave .ui-menu": "collapseAll", focus: function( event, keepActiveItem ) { // If there's already an active item, keep it active // If not, activate the first item - var item = this.active || this.element.find( this.options.items ).eq( 0 ); + var item = this.active || this._menuItems().first(); if ( !keepActiveItem ) { this.focus( event, item ); @@ -5082,7 +5114,7 @@ var widgetsMenu = $.widget( "ui.menu", { this._on( this.document, { click: function( event ) { if ( this._closeOnDocumentClick( event ) ) { - this.collapseAll( event ); + this.collapseAll( event, true ); } // Reset the mouseHandled flag @@ -5091,6 +5123,46 @@ var widgetsMenu = $.widget( "ui.menu", { } ); }, + _activateItem: function( event ) { + + // Ignore mouse events while typeahead is active, see #10458. + // Prevents focusing the wrong item when typeahead causes a scroll while the mouse + // is over an item in the menu + if ( this.previousFilter ) { + return; + } + + // If the mouse didn't actually move, but the page was scrolled, ignore the event (#9356) + if ( event.clientX === this.lastMousePosition.x && + event.clientY === this.lastMousePosition.y ) { + return; + } + + this.lastMousePosition = { + x: event.clientX, + y: event.clientY + }; + + var actualTarget = $( event.target ).closest( ".ui-menu-item" ), + target = $( event.currentTarget ); + + // Ignore bubbled events on parent items, see #11641 + if ( actualTarget[ 0 ] !== target[ 0 ] ) { + return; + } + + // If the item is already active, there's nothing to do + if ( target.is( ".ui-state-active" ) ) { + return; + } + + // Remove ui-state-active class from siblings of the newly focused menu item + // to avoid a jump caused by adjacent elements both having a class with a border + this._removeClass( target.siblings().children( ".ui-state-active" ), + null, "ui-state-active" ); + this.focus( event, target ); + }, + _destroy: function() { var items = this.element.find( ".ui-menu-item" ) .removeAttr( "role aria-disabled" ), @@ -5422,7 +5494,7 @@ var widgetsMenu = $.widget( "ui.menu", { this._removeClass( currentMenu.find( ".ui-state-active" ), null, "ui-state-active" ); this.activeMenu = currentMenu; - }, this.delay ); + }, all ? 0 : this.delay ); }, // With no arguments, closes the currently active menu - if nothing is active @@ -5458,11 +5530,7 @@ var widgetsMenu = $.widget( "ui.menu", { }, expand: function( event ) { - var newItem = this.active && - this.active - .children( ".ui-menu " ) - .find( this.options.items ) - .first(); + var newItem = this.active && this._menuItems( this.active.children( ".ui-menu" ) ).first(); if ( newItem && newItem.length ) { this._open( newItem.parent() ); @@ -5490,21 +5558,27 @@ var widgetsMenu = $.widget( "ui.menu", { return this.active && !this.active.nextAll( ".ui-menu-item" ).length; }, + _menuItems: function( menu ) { + return ( menu || this.element ) + .find( this.options.items ) + .filter( ".ui-menu-item" ); + }, + _move: function( direction, filter, event ) { var next; if ( this.active ) { if ( direction === "first" || direction === "last" ) { next = this.active [ direction === "first" ? "prevAll" : "nextAll" ]( ".ui-menu-item" ) - .eq( -1 ); + .last(); } else { next = this.active [ direction + "All" ]( ".ui-menu-item" ) - .eq( 0 ); + .first(); } } if ( !next || !next.length || !this.active ) { - next = this.activeMenu.find( this.options.items )[ filter ](); + next = this._menuItems( this.activeMenu )[ filter ](); } this.focus( event, next ); @@ -5522,7 +5596,13 @@ var widgetsMenu = $.widget( "ui.menu", { } if ( this._hasScroll() ) { base = this.active.offset().top; - height = this.element.height(); + height = this.element.innerHeight(); + + // jQuery 3.2 doesn't include scrollbars in innerHeight, add it back. + if ( $.fn.jquery.indexOf( "3.2." ) === 0 ) { + height += this.element[ 0 ].offsetHeight - this.element.outerHeight(); + } + this.active.nextAll( ".ui-menu-item" ).each( function() { item = $( this ); return item.offset().top - base - height < 0; @@ -5530,7 +5610,7 @@ var widgetsMenu = $.widget( "ui.menu", { this.focus( event, item ); } else { - this.focus( event, this.activeMenu.find( this.options.items ) + this.focus( event, this._menuItems( this.activeMenu ) [ !this.active ? "first" : "last" ]() ); } }, @@ -5546,7 +5626,13 @@ var widgetsMenu = $.widget( "ui.menu", { } if ( this._hasScroll() ) { base = this.active.offset().top; - height = this.element.height(); + height = this.element.innerHeight(); + + // jQuery 3.2 doesn't include scrollbars in innerHeight, add it back. + if ( $.fn.jquery.indexOf( "3.2." ) === 0 ) { + height += this.element[ 0 ].offsetHeight - this.element.outerHeight(); + } + this.active.prevAll( ".ui-menu-item" ).each( function() { item = $( this ); return item.offset().top - base + height > 0; @@ -5554,7 +5640,7 @@ var widgetsMenu = $.widget( "ui.menu", { this.focus( event, item ); } else { - this.focus( event, this.activeMenu.find( this.options.items ).first() ); + this.focus( event, this._menuItems( this.activeMenu ).first() ); } }, @@ -5585,14 +5671,15 @@ var widgetsMenu = $.widget( "ui.menu", { .filter( ".ui-menu-item" ) .filter( function() { return regex.test( - $.trim( $( this ).children( ".ui-menu-item-wrapper" ).text() ) ); + String.prototype.trim.call( + $( this ).children( ".ui-menu-item-wrapper" ).text() ) ); } ); } } ); /*! - * jQuery UI Autocomplete 1.12.1 + * jQuery UI Autocomplete 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -5610,9 +5697,8 @@ var widgetsMenu = $.widget( "ui.menu", { //>>css.theme: ../../themes/base/theme.css - $.widget( "ui.autocomplete", { - version: "1.12.1", + version: "1.13.2", defaultElement: "", options: { appendTo: null, @@ -5638,6 +5724,7 @@ $.widget( "ui.autocomplete", { requestIndex: 0, pending: 0, + liveRegionTimer: null, _create: function() { @@ -5775,11 +5862,6 @@ $.widget( "ui.autocomplete", { this.previous = this._value(); }, blur: function( event ) { - if ( this.cancelBlur ) { - delete this.cancelBlur; - return; - } - clearTimeout( this.searching ); this.close( event ); this._change( event ); @@ -5795,31 +5877,24 @@ $.widget( "ui.autocomplete", { role: null } ) .hide() + + // Support: IE 11 only, Edge <= 14 + // For other browsers, we preventDefault() on the mousedown event + // to keep the dropdown from taking focus from the input. This doesn't + // work for IE/Edge, causing problems with selection and scrolling (#9638) + // Happily, IE and Edge support an "unselectable" attribute that + // prevents an element from receiving focus, exactly what we want here. + .attr( { + "unselectable": "on" + } ) .menu( "instance" ); this._addClass( this.menu.element, "ui-autocomplete", "ui-front" ); this._on( this.menu.element, { mousedown: function( event ) { - // prevent moving focus out of the text field + // Prevent moving focus out of the text field event.preventDefault(); - - // IE doesn't prevent moving focus even with event.preventDefault() - // so we set a flag to know when we should ignore the blur event - this.cancelBlur = true; - this._delay( function() { - delete this.cancelBlur; - - // Support: IE 8 only - // Right clicking a menu item or selecting text from the menu items will - // result in focus moving out of the input. However, we've already received - // and ignored the blur event because of the cancelBlur flag set above. So - // we restore focus to ensure that the menu closes properly based on the user's - // next actions. - if ( this.element[ 0 ] !== $.ui.safeActiveElement( this.document[ 0 ] ) ) { - this.element.trigger( "focus" ); - } - } ); }, menufocus: function( event, ui ) { var label, item; @@ -5850,9 +5925,11 @@ $.widget( "ui.autocomplete", { // Announce the value in the liveRegion label = ui.item.attr( "aria-label" ) || item.value; - if ( label && $.trim( label ).length ) { - this.liveRegion.children().hide(); - $( "
    " ).text( label ).appendTo( this.liveRegion ); + if ( label && String.prototype.trim.call( label ).length ) { + clearTimeout( this.liveRegionTimer ); + this.liveRegionTimer = this._delay( function() { + this.liveRegion.html( $( "
    " ).text( label ) ); + }, 100 ); } }, menuselect: function( event, ui ) { @@ -5962,7 +6039,7 @@ $.widget( "ui.autocomplete", { _initSource: function() { var array, url, that = this; - if ( $.isArray( this.options.source ) ) { + if ( Array.isArray( this.options.source ) ) { array = this.options.source; this.source = function( request, response ) { response( $.ui.autocomplete.filter( array, request.term ) ); @@ -6034,7 +6111,7 @@ $.widget( "ui.autocomplete", { _response: function() { var index = ++this.requestIndex; - return $.proxy( function( content ) { + return function( content ) { if ( index === this.requestIndex ) { this.__response( content ); } @@ -6043,7 +6120,7 @@ $.widget( "ui.autocomplete", { if ( !this.pending ) { this._removeClass( "ui-autocomplete-loading" ); } - }, this ); + }.bind( this ); }, __response: function( content ) { @@ -6203,7 +6280,7 @@ $.widget( "ui.autocomplete", { var editable = element.prop( "contentEditable" ); if ( editable === "inherit" ) { - return this._isContentEditable( element.parent() ); + return this._isContentEditable( element.parent() ); } return editable === "true"; @@ -6247,8 +6324,10 @@ $.widget( "ui.autocomplete", $.ui.autocomplete, { } else { message = this.options.messages.noResults; } - this.liveRegion.children().hide(); - $( "
    " ).text( message ).appendTo( this.liveRegion ); + clearTimeout( this.liveRegionTimer ); + this.liveRegionTimer = this._delay( function() { + this.liveRegion.html( $( "
    " ).text( message ) ); + }, 100 ); } } ); @@ -6256,7 +6335,7 @@ var widgetsAutocomplete = $.ui.autocomplete; /*! - * jQuery UI Controlgroup 1.12.1 + * jQuery UI Controlgroup 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -6277,7 +6356,7 @@ var widgetsAutocomplete = $.ui.autocomplete; var controlgroupCornerRegex = /ui-corner-([a-z]){2,6}/g; var widgetsControlgroup = $.widget( "ui.controlgroup", { - version: "1.12.1", + version: "1.13.2", defaultElement: "
    ", options: { direction: "horizontal", @@ -6394,7 +6473,7 @@ var widgetsControlgroup = $.widget( "ui.controlgroup", { } ); } ); - this.childWidgets = $( $.unique( childWidgets ) ); + this.childWidgets = $( $.uniqueSort( childWidgets ) ); this._addClass( this.childWidgets, "ui-controlgroup-item" ); }, @@ -6478,7 +6557,7 @@ var widgetsControlgroup = $.widget( "ui.controlgroup", { var result = {}; $.each( classes, function( key ) { var current = instance.options.classes[ key ] || ""; - current = $.trim( current.replace( controlgroupCornerRegex, "" ) ); + current = String.prototype.trim.call( current.replace( controlgroupCornerRegex, "" ) ); result[ key ] = ( current + " " + classes[ key ] ).replace( /\s+/g, " " ); } ); return result; @@ -6541,7 +6620,7 @@ var widgetsControlgroup = $.widget( "ui.controlgroup", { } ); /*! - * jQuery UI Checkboxradio 1.12.1 + * jQuery UI Checkboxradio 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -6560,9 +6639,8 @@ var widgetsControlgroup = $.widget( "ui.controlgroup", { //>>css.theme: ../../themes/base/theme.css - $.widget( "ui.checkboxradio", [ $.ui.formResetMixin, { - version: "1.12.1", + version: "1.13.2", options: { disabled: null, label: null, @@ -6574,8 +6652,7 @@ $.widget( "ui.checkboxradio", [ $.ui.formResetMixin, { }, _getCreateOptions: function() { - var disabled, labels; - var that = this; + var disabled, labels, labelContents; var options = this._super() || {}; // We read the type here, because it makes more sense to throw a element type error first, @@ -6595,12 +6672,18 @@ $.widget( "ui.checkboxradio", [ $.ui.formResetMixin, { // We need to get the label text but this may also need to make sure it does not contain the // input itself. - this.label.contents().not( this.element[ 0 ] ).each( function() { + // The label contents could be text, html, or a mix. We wrap all elements + // and read the wrapper's `innerHTML` to get a string representation of + // the label, without the input as part of it. + labelContents = this.label.contents().not( this.element[ 0 ] ); - // The label contents could be text, html, or a mix. We concat each element to get a - // string representation of the label, without the input as part of it. - that.originalLabel += this.nodeType === 3 ? $( this ).text() : this.outerHTML; - } ); + if ( labelContents.length ) { + this.originalLabel += labelContents + .clone() + .wrapAll( "
    " ) + .parent() + .html(); + } // Set the label option if we found label text if ( this.originalLabel ) { @@ -6641,9 +6724,6 @@ $.widget( "ui.checkboxradio", [ $.ui.formResetMixin, { if ( checked ) { this._addClass( this.label, "ui-checkboxradio-checked", "ui-state-active" ); - if ( this.icon ) { - this._addClass( this.icon, null, "ui-state-hover" ); - } } this._on( { @@ -6678,7 +6758,7 @@ $.widget( "ui.checkboxradio", [ $.ui.formResetMixin, { _getRadioGroup: function() { var group; var name = this.element[ 0 ].name; - var nameSelector = "input[name='" + $.ui.escapeSelector( name ) + "']"; + var nameSelector = "input[name='" + $.escapeSelector( name ) + "']"; if ( !name ) { return $( [] ); @@ -6690,7 +6770,7 @@ $.widget( "ui.checkboxradio", [ $.ui.formResetMixin, { // Not inside a form, check all inputs that also are not inside a form group = $( nameSelector ).filter( function() { - return $( this ).form().length === 0; + return $( this )._form().length === 0; } ); } @@ -6811,7 +6891,7 @@ var widgetsCheckboxradio = $.ui.checkboxradio; /*! - * jQuery UI Button 1.12.1 + * jQuery UI Button 1.13.2 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -6829,9 +6909,8 @@ var widgetsCheckboxradio = $.ui.checkboxradio; //>>css.theme: ../../themes/base/theme.css - $.widget( "ui.button", { - version: "1.12.1", + version: "1.13.2", defaultElement: "" ).addClass( this._triggerClass ). - html( !buttonImage ? buttonText : $( "" ).attr( - { src:buttonImage, alt:buttonText, title:buttonText } ) ) ); + + if ( this._get( inst, "buttonImageOnly" ) ) { + inst.trigger = $( "" ) + .addClass( this._triggerClass ) + .attr( { + src: buttonImage, + alt: buttonText, + title: buttonText + } ); + } else { + inst.trigger = $( "" : "" ); - - buttonPanel = ( showButtonPanel ) ? "
    " + ( isRTL ? controls : "" ) + - ( this._isInRange( inst, gotoDate ) ? "" : "" ) + ( isRTL ? "" : controls ) + "
    " : ""; + controls = ""; + if ( !inst.inline ) { + controls = $( "